Practical CSS Guidelines To Use In All Your Projects

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

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);
}

7. 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

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

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

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

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

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;
}
}

Cross-browser support

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

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

Absolute units

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

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

Commonly used

  • 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 . 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

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

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

  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 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

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
}

2. 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 { }
}

3. 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

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

  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.
  4. 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.
  5. 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

They can do wonders and are very easy to understand.

Hacks, magic values, on-paper calculations

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

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

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

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

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

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

Originally published at https://www.monterail.com.

A close-knit team of 110+ experts offering Web & mobile development for startups and businesses.