Practical CSS Guidelines To Use In All Your Projects

Szymon Licau

Practical CSS Guidelines To Use In All Your Projects -- featured image

Writing lasting and maintainable CSS can often be tricky. Its cascading nature, a huge variety of possible approaches, units, properties, and values can often lead to inconsistent code of varying quality and behavior.

Add to that an abundance of frameworks and approaches to choose from, external dependencies with their own mixture of both — which we often need to override by providing styles of our own with higher specificity — can quickly invite trouble.

The purpose of this article is to touch upon some of the subjects from the perspective of writing maintainable, high-quality code and explaining their importance and impact on our projects. I'll go over them without diving too deep and introduce some good practices which overall should be fairly framework agnostic.

Variables

The first topic in question is variables and constants. Just as in any typical growing codebase we should extract common, application-specific values like colors, dimensions, font sizes, device breakpoints, z-index values to variables. This way refactoring the code is easier and we make double sure we re-use the same values when necessary.

On the note of z-indexes — this way we can have a nice, ordered list of all the values we use in one place to make double sure what will be displayed over what. No more figuring out what number the z-index value should be!

However, in the case of variables, we may actually have at least two choices. We can use the widely supported CSS custom properties to hold our variables, or, if we use Sass, we can use Sass variables. Or we can even mix both if necessary.

While both methods can be used to achieve what we need there are a few important caveats for each.

  1. Sass variables are there only before compiling. If we inspect our styles in the browser we will only see the computed values, not the variable names.
  2. CSS variables are a bit more clunky to use (color: var(--text-gray) vs color: $text-gray).
  3. Unlike Sass variables, CSS variables cannot be used for device breakpoint values, even if setting them up on :root selector — the HTML element (@media screen and (min-width: var(--breakpoint-sm)) won't work!).
  4. CSS variables do cascade, meaning, we can override them on a given selector to what we want, and as such, the element's children will access changed values. This works really nicely for application-wide theming.
  5. CSS variables can also be used within the calc() function so do not be afraid to use them there.
  6. CSS variables can be used as a tool to create configurable components by utilizing the value fallbacks.

.button {
  // Take the value of --color-primary or if not defined, default to another color
  background-color: var(--color-primary, #c0ffee);
}
  1. You can directly change the value of your custom properties in JS

#grid {
  display: grid;
  grid-template-columns: repeat(var(--columns), 1fr);
  --columns: 5;
}

var grid = document.querySelector("#grid");

// Set the number of columns to 10
grid.style.setProperty("--columns", 10);

Specificity

The second topic I would like to touch upon is Specificity. Specificity is how the browsers decide which styles take priority and are applied and which are not, in case multiple definitions exist.

This introduces the danger that if somebody provides selectors with high specificity, then in order to override the styles you need to introduce selectors with the same (and appearing later in the code) or higher specificity.

This is often the case when we want to make some external library or a component fit our application's styles and we need to override them while keeping some of them as a base.

Therefore we should aim to keep the specificity of our styles as low as we can, so if needed - overriding the selectors is very easy to do and intuitive.

Here are a few tips on specificity:

  1. Avoid nesting selectors for your own styles and if you do, decide upon a reasonable max depth of nesting level (1 or 2 levels deep for example) and try not to go over.
  2. Take advantage of the fact that browsers take later appearance as a priority in the same specificity level selectors. The less specific the styles the earlier they should occur in stylesheets.
  3. Avoid using !important unless necessary or otherwise very hard to avoid.
  4. Do not style id selectors. Keep yourself constrained to classes or elements.
  5. Although it’s opinionated, you might consider avoiding applying styles on element selectors or being very careful about it. While they have low specificity overall, they are very generic and thus it's easy to apply a tiny bit too many styles which later on need to be overridden on class-specific selectors.

Selectors

CSS offers a very big arsenal of different selectors and it's very worthwhile to learn them and take advantage of them. Many are very powerful and allow us to write less code and take full advantage of the tools we use.

The most major distinction here is the fact that we can style elements (), elements with a given class (.footer), or elements with a given id (#footer).

The difference between those however is specificity — and as such we should avoid styling directly by id (#footer) because it's a selector with high specificity.

On the other hand, we have element selectors which have low specificity but, as we talked about, may sometimes be a bit too generic which could lead to a lot of style overrides to clear those. We should be very mindful of those.

On a special note, we also have html and body element selectors which are a great place to define the basic font used in the application, colors, or even background color.

This is also the place to tweak your base REM unit value (more on that later).

One another special selector would be :root — which targets the highest-level 'parent' element in the DOM. In the overwhelming majority of cases — that would be the html element. The difference in html vs :root however is that :root has higher specificity.

Pseudoclasses

We also have a very wide range of many different pseudoclasses, the most common being things like :hover or :first-child and :last-child.

Their specificity is that of a class and they are commonly used to style the elements in some sort of state (being hovered, being in focus, an input being invalid or disabled, a checkbox being checked and so on). There are really quite a lot of them and you might be surprised to find new ones if you browse their list.

Thanks to them, you can handle things like custom checkboxes really easily.

Pseudoelements

Just like pseudoclasses, pseudoelements are selectors that let you style a specific part of some element (for example ::first-line of a paragraph, it's ::first-letter or an input element's ::placeholder). Their specificity is that of an element selector.

There is not as many of them and most likely you have met their most common examples - ::before or ::after which allow us to do some fun neat things like custom list markers, icons before or after a span of text, a colored dot over an icon to indicate new notifications and many other.

One note here, however, is that while you could just as well write ::before and :before and both do work (because of no difference in the early spec between pseudoclasses and pseudoelements) it is recommended to use double colons (::) to differentiate between the two.

Sibling selectors

We can use them to define relationships between siblings which is very often useful when styling list items.

Most commonly used is the .a + .b selector, which applies styles on the .b class element that is placed in the DOM as a sibling placed immediately after the element with the class .a.

In the case of list items we can easily write .a + .a to easily target any element except the first one and, say, apply spacing between them by using margin-top.


.list-item + .list-item {
  margin-top: 20px;
}

// or when using Sass
.list-item {
  & + & {
    margin-top: 20px;
  }
}

The much more rarely used is the .a ~ .b selector. It applies styles on the .b class element preceded by the .a element. It might be useful in cases where applying a class on one element should change behavior on any of the subsequent elements.

Cross-browser support

Making your application work on all of the major browsers is nowadays rarely an exception.

Therefore some quick few tips on this to make your life easier:

  1. Do use style normalization like normalize.css. It allows us to set up a common ground to build your project's styles upon by mitigating the difference between different HTML tags.
  2. In case you don't know it, caniuse is your best friend. You can easily check how supported a given feature is, in what browser versions, and with what caveats.
  3. Do use autoprefixers if possible (i.e. postcss). They allow you to not worry and omit vendor prefixes for your styles (-webkit-, -moz-, -o-, -ms-). You can also configure the specific browser versions you target so it provides those only when needed.
  4. Sometimes a relatively new feature that you would greatly benefit from may not be supported in all the major browsers yet. That does not necessarily mean you cannot use it at all. Consider using polyfills in these cases.
  5. Avoid using browser-specific features if not broadly supported — in those cases always try to use polyfills or provide alternatives solutions for graceful degradation.
  6. Don’t test your interfaces only in one browser, try to mix it up sometimes.

Units

CSS offers quite a lot of units, many of which often get rarely used either because they are more familiar to those dealing with the printed medium (mm, in, cm, pt, pc) or because they are very specific in their nature (ex - x-height of the current font, ch - relative to the width of "0" - the "zero" character).

The biggest distinction between them is that we have units that are absolute and units that are relative.

Absolute units

Absolute units have a fixed length and should be considered to always be the same size.

These are cm, mm, in, pt, pc and finally px.

The px units are a bit weird though because while on low-dpi devices they are equal to one on-screen pixel of the display, it is not the case on print medium or high-dpi devices (one px can be equal to multiple device pixels; so 5.5px value kind of makes sense, although should be avoided because of the low-dpi devices and the fact that browsers might round up pixels differently).

Relative units

Relative units as the name suggests are relative based on some other length.

These are em, ex, ch, rem, fr, vw, vh, vmin, vmax and %.

Commonly used

The most commonly used units and of most interest are:

  • px
  • % (relative to the parent element)
  • em and rem
  • fr (which represents a fraction of a grid)
  • vw, vh (less so vmin and vmax)

The % may seem the most intuitive on paper but can actually be quite versatile and tricky because they relate to *some other* value on the parent.

In case of width or height — it will be a % of parent's width. In the case of font-size it will be a percent of the parent's font-size.

The em relates to the font-size of a given element's parent, so the bigger the font-size on the parent element, the bigger the value of the em. While pretty useful on paper they quickly can get unwieldy if used widely due to the cascading nature of CSS. Use them only when it makes sense to use the cascading nature and relative size to your advantage.

The rem is also based on font-size but on the font-size of the root element — the html element. This makes them a fantastic tool for making our interfaces relative to the user's preferences — their preferred font-size.

The fr is useful to us when we want to specify the sizes of certain CSS grid areas. Its size is relative to the container and to the sum of all fractions. (2fr = (2 / total fractions) * grid's width).

The vw and vh are units relative to the viewport's width or height (in percent). They are not so common but for some use cases they are irreplaceable — take some landing page's hero image for example. If we wanted it to be exactly *one screen high* it's the go-to unit to use. Although, be mindful about it and also set min-width/min-height values so as to have some constraints for weird screen sizes or landscape orientation.

Likewise vmin and vmax refer to the lesser or bigger of the two so for example 40vmin means 40% of the smaller dimension of the two.

PX vs REM

While debated long and hard throughout the years, I think we can sum it up pretty easily.

In short — use rem if you can, if you can't — be aware of what you lose with that and reconsider.

The advantage of using rem units is the fact that they can adhere to the user's preferences like no other. And this is something we should be doing. If someone wants a bigger font size for their browsing experience we can thanks to this respect it and also scale up everything as needed.

If we would set everything in px that would simply not work. We would completely ignore the user's preferences, forcing them to scale up the whole website at worst.

Mitigating REMs inconveniences

When using rem, at a glance we can see that they are a bit clunky. They are based on the root element's font-size, so html element's font-size. It's a common practice that browsers set it as 16px by default although the user can change it in their browser settings.

So, in short, if some element on a design is 200px wide, we treat it as 12.5rem wide with assumption of 16px default size. This makes writing for example 40px as 2.5rem or 1px as 0.0625rem quite inconvenient.

If we set up the font-size manually to something easy to calculate on the html element we will override the user's preference. Or will we?

There is a trick we can use here to make the base something a bit more convenient for us.


html {
  font-size: 62.5%; // 16 * 62.5% = 10; so now 1rem = 10px
}

body {
  font-size: 1.6rem; // 16px
}

This way, we respect the user's font size (if their preference is 20px, everything will be calculated with that as a base and scaled properly) and also make our units much easier to read and write.

Also, do not forget about setting the body back to the user's default — this way, the default value will cascade properly.

Be aware though that this won't make our media queries adhere to this, those will need to respect the user's defaults so REMs there by default will be equal to 16px.

Responsiveness / RWD

If the aim of the project is to provide an application that is going to be responsive and working well on mobile devices, there are a few assumptions you should have about your styles.

  1. For the most part — be very careful about fixed sizes for your elements (especially width or height).
    One simple change of assumptions is to instead of width: 250px doing max-width: 250px; width: 100% so that if there would be less space the element would still fit.
    This is especially important for images or videos.
  2. Decide upon a reasonable minimal device width (be it 320px or 375px) and assume your application should work on this one and any bigger one.
  3. If things change their order in designs on different devices, you can use order property on your flex/grid container's elements. This way you can avoid repeating HTML content and hiding/displaying on demand just because some things were reordered. In the case of grids, you can also use named areas and just change the grid template.
  4. Decide upon a list of fixed device breakpoints (usually up to 3 or 4 is more than enough) and try to stick to them.
  5. Make sure things are easily clickable on mobile devices. This however doesn't mean the elements need to be visually bigger. Adding some padding or even an absolutely positioned container to add some area works wonders.
  6. You can check if your device supports hover by a media query @media (hover: hover)!

Styles Scoping

In big projects, we often encounter a situation where a class with a given name already exists. This leads to situations where we might be forced to use a less-fitting or longer class name (if we actually do spot the conflict beforehand).

In order to avoid the styles "leaking" and styling some places of our application by accident, we may want to scope our styles in some way.

BEM

One of the ways that we can do this is by introducing BEM approach to naming our classes.

This approach will allow us to not only scope the styles we write but also be explicit about the relationship between them.

Another advantage to this is that we can keep our specificity low, while still being deliberate on what should be a part of what. (on the cost of longer class names)

On that note, there are a few small tips to get your BEM classes a bit more manageable:

  1. If you use Sass, you can save yourself plenty of keystrokes by utilizing the &

.button {
  &__icon { } //.button--icon

  &--primary { } //.button--primary

  &--primary &__icon { } //.button--primary button__icon
}

  1. Don't go crazy; it's BEM, not BEEM or BEEEM ;)
    The Block-Element relationship does not need to reflect the DOM tree (just because you would put foo__bar__baz in foo__bar in the DOM, does not mean you need to nest the class name).
    You should keep it flat and save yourself the headache (and possible refactoring in the future) of nesting the elements.
    If you feel like there should be such a relationship, it's often a good sign that you might need another block with its own elements there.

// BAD
.foo {
  &__bar {
    &__baz { }
  }
}

// GOOD
.foo {
  &__bar { }

  &__baz { }
}

// GOOD
.foo {
}

.bar {
  &__baz { }
}
  1. While opinionated, you may want to consider using modifiers only on block classes.
    This also saves us the trouble in writing our Sass styles, so we only write the modifier explicitly and do not repeat ourselves.

.foo {
  &__bar { }

  &__bar--active { } // .foo__bar--active
}

// VS

.foo {
  &__bar { }

  // Bigger specificity (which may be helpful), shorter class names
  &--active &__bar { } // .foo--active .foo__bar
}

Finally, there are of course other naming conventions (which, for example, introduce prefixes to the classes so we operate within different namespaces). I have decided to only touch upon BEM due to the fact of how commonly used it is.

For the most part though, they do try to solve the same problems so many points about BEM would still apply.

CSS in JS solutions

By default, most of the CSS in JS solutions make our styles scoped out of the box.

In the case of a framework like Vue and its single-file components, we may use the scoped attribute on our stylesheets to do the job.

Still, if the component is a bit complex, we might consider using BEM there anyway for better clarity in our templates.

Accessibility / a11y

While accessibility is a very big topic on its own, the general rule of thumb is that we should avoid making elements of our applications inaccessible by accident or just so things look pretty.

  1. Avoid removing focus styles unless you provide a suitable alternative.
    Yes, the focus outline might not be very pretty but it's on us to make focused elements look good. Just hiding the focus styles should be avoided.
  2. Try to respect the user's preferences.
    Use relative units — we can scale the entire UI appropriately based on preferred font size, or even based on the screen's width or height.
  3. Make sure your font sizes are sensible so that things are big enough to read comfortably on all devices.
    Browsers did not set 16px as standard font size for no reason. Smaller font sizes should be used carefully.
    As an example — if an input has a smaller font size than 16px set on iOS — the Safari browser will zoom in the screen for the user which is most likely an undesired behavior.
  1. Try to adhere to the WCAG color contrast standards and keep the number of culprits low (or at least avoid them on important, big blocks of text). Your dev tools can help you here, so check in your browser how you can see the contrast ratio for a given selector. Proper use of colors and contrast not only will make the application easier on the eyes, but it will also improve the usability and accessibility at the same time.
  2. Learn the difference between hiding content visually, hiding content from assistive tech, and actually hiding content for everyone. Check the a11y’s project article on the matter

Others

Utilise CSS math functions if you can

While not as much supported in some cases (clamp() being the biggest culprit), do learn and use the built in min(), max(), clamp(), and finally, the most popular - calc() functions.

They can do wonders and are very easy to understand.

Hacks, magic values, on-paper calculations

We all have been there.

No reasonable solution seems to work and the only thing that seems to be doing its job is some very specific value on some property.

Maybe in negative pixels. Or half a pixel for some reason.

These very tailored, pixel-perfect adjustments might work and may be understood by you at the time of writing but they very quickly get forgotten.

Do not forget to leave comments explaining your reasoning and what you try to do.

This will help us a) understand what it does and b) potentially refactor it much easier in the future.

Likewise, if we set a value in code that is a result of some calculation we did on paper or in our head, we lose all of that information.

Leaving a comment helps tremendously and (if possible) we could always use calc() to be implicit about what we are doing.

Avoid generic properties unless necessary or helpful

Properties like background can do a whole lot of things at once.

At a glance background: blue does the same thing as background-color: blue and saves a few keystrokes.

But what it actually does is also set others like background-position, background-image and background-size to default values.

When adding styles of higher specificity to make some changes (via another CSS selector, be it a BEM modifier or some utility class even), these properties will undo the lower-specificity values to defaults.


.foo {
  background-color: blue;
  background-image: url("https://foo.bar/image.png");
  background-repeat: no-repeat;
  background-size: cover;
}

.foo--bar {
  //this will not only change the color but reset all the other properties above to defaults
  background: red;
}

Global styles

In the case of projects where we have styles paired up with some components, there will be cases where it makes sense to introduce some more general / non-component-specific styles.

For example, we might want to provide a few classes for different kinds of links in our application but making a link component for those would be a bit overkill.

In that case, we should follow the rule that the less specific it is — the less specificity it needs.

So, in case of a project where we have a big stylesheet file that we compile with all the styles — put them at the very top.

In the case of projects with frameworks like Vue, import them early on in your main file to allow easy overrides.

Utility classes

There might be cases where there are some small properties and values you frequently put on some of your selectors.

A good example might be text-align: center, which you might often put to for example center a piece of text, or an inline-block element horizontally.

In order to help us not to repeat ourselves too much, utility classes might be a good choice, although we should be mindful of how we approach them.

Some good practices with utility classes:

  1. They should do one thing, and one thing only.
  2. Their name should specify directly what they do. text-align-center is very obvious and does not require looking it up to understand it.
  3. In case your project has a big stylesheet file it builds from importing everything else, import them at the very end so they have higher specificity by default.
  4. While the dreadful !important is usually a big no-no, utility classes might be a good use-case for it, especially if we want to be extra sure (you could still override them anyway if you really wanted to) they work as intended.
  5. While opinionated, you may like to prefix your utility classes to avoid conflicts with other class names and to distinguish them a little bit (for example: u-text-align-center);

Box sizing

An interesting fact about the box model is that we can actually tweak how it works a little bit to our needs.

There are 3 possible values for box-sizing property:

  1. content-box — the default one (usually) may not always be to our liking but has its uses. In this model, the width or height are independent of the element's border or padding, they only depend on the content. So in this model if we wanted to have, say a 40px tall element with a 1px border (and that's how we look at things for the most part, the border being a part of an element), we would actually have to set the height to 38px, which may not be that intuitive. On the other hand — it works superbly with things like wrappers that constrain our width and introduce margin or padding.
  2. padding-box is even different. This time the width or height are including the padding properties so the values of width or height we set are calculated in a way that includes the padding values within them. Sometimes this may prove useful but we still need to scratch our heads with the borders.
  3. border-box — finally, probably the most intuitive of all the models. This time the total height and width include both padding and border-width properties. This resolves the problem of looking at a design and having to calculate the borders or padding values out.

And here’s a tip: if you want to make the border-box value a default one for our project, you may consider setting it up in the following way:


// Make the default model use `border-box`
html {
  box-sizing: border-box;
}

// And cascade it to everything else while being open
// to using `content-box` or `padding-box` when needed
*,
*::before,
*::after {
  box-sizing: inherit;
}

Final words

As you can see, there is no one-size-fits-all guidebook for creating CSS styles that work well both for the project and for the people creating it (now and in the future). Hopefully though, this set of guidelines will help you decide how to approach styling in your particular situation.

And of course — if you have any questions or suggestions, just leave a comment.

Szymon Licau avatar
Szymon Licau