How I organize CSS in large projects using UFOCSS — Part 2

Naming convention

The main purpose of CSS naming conventions is to make the CSS selectors as informative and readable as possible, but defining a convention is not really a piece of cake. Naming is by far one the most debated activities in computer science. We can definitely benefit of a naming convention that helps us to write maintainable and scalable code. On the contrary, poorly written CSS can quickly drive us crazy and turn into a nightmare.

Think about naming convention as a spoken language which is basically made of a set of shared and well known rules. This is what enables people to communicate and exchange useful information.

As described in the previous article How I organize CSS in large projects using UFOCSS — Part 1, we can define five CSS layers of abstraction: Base, Layout, Object, Utility, Vendor (at this step, we can simply ignore the Vendor layer, since this is where we override the vendors’ styles, so we are not supposed to add new CSS classes here). The use of a naming convention should help us to easily understand which layer a CSS selector refers to.

Base Rules

The Base layer includes the CSS for bare typography HTML elements and the helper classes strictly related to HTML tags such as .h1.small.mark.

We expect to find here some really basic and global rules, such as:

/* UNDER _B_TYPOGRAPHY.SCSS */
/* Heading and heading-like style rules */
h1, .h1 { }
/* small and small-like style rules */
small, .small { }
/* paragraph and paragraph-like style rules */
p, .p { }

I generally add these rules under a file called _b_typography.scss.

If your style guide requires a lot of modifiers for some elements such as anchors or buttons, it may be a good idea to split these rules in to multiple files, in order to avoid long lines of code and ensure code readability.

As specified before, these files will include only classes strictly related to HTML tags, like .anchor-primary or .button-primary. For this reason, we don’t need any namespaces here to identify the belonging CSS layer.

Layout Rules

The Layout layer contains the main grid classes and other class-based selectors which define the skeleton of the website.

As a convention, you can use here the l_ namespace, which highlights that the class belongs to the layout CSS layer.

Also, I think it’s a good practice to choose a class name which includes the name of the file that includes it. Back to the example shown in my previous article, this way you can easily presume that the classes .l_columns_1.l_columns_2.l_columns_3, … are certainly placed under a file called _l_columns.scss, as shown below (please note that this is just a sample case to highlight that all the style rules added under this level are supposed to affect only the layout):

/* UNDER _L_COLUMNS.SCSS */
$min-cols: 2;
$max-cols: 6;
.l_columns-1 {
display: grid;
grid-row-gap: 30px;
grid-column-gap: 30px;
}
@for $i from $min-cols through $max-cols {

/* This will generate the classes .l_columns-2, .l_columns-3, ..., .l_columns-6 in the compiled CSS */
.l_columns-#{$i} { 
@extend .l_columns-1;
grid-template-columns: repeat($i, 1fr);
}
}

Object Rules

The object layer is the one that contains cosmetic CSS. Objects are the real core of any projects, so we can simply use the name of the object itself. Consider that:

  1. We’re using the _o_ prefix just to name files, since this will ensure files to be sorted alphabetically, in the same order of imports, so making clear what comes first in the code (always good to know when it comes to cascading!)
  2. We’re not using the _o_ namespace for classes to highlight that it is an object, since this would turn out to be unnecessary verbose.
  3. We’re not using different conventions for atomic elements and components, since the class name itself should be explanatory enough to clarify which kind of element we’re dealing with (we can easily presume that a class like .label-success is being used for atomic UI elements like labels, whether a class like .card-primary more likely refers to a component). They will be all included under the CSS Object Layer.

As a convention, you can use the plural to name files, i.e. _o_cards.scss, whereas you can use the singular to feature the class name, since it refers to an object entity, no matter how many time it’s going to be used in your codebase.

/* UNDER _O_CARDS.SCSS */
.card-primary { /* Style rules here */ }
.card-secondary { /* Style rules here */ }
....

Utility Rules

As for the other CSS layers, we’re using the _u_ prefix to name files, in order to make them alphabetically sorted. Under this layer, you may find:

  • Global Status Modifier classes: we will use the SMACSS is- or has- convention for these classes (more details further).
  • Single-rules classes: it refers to classes that contain a single rule or a very simple and universal pattern. Think about.float-right.align-center, or .font-small for single rules; .visually-hidden for patterns. We don’t need any namespaces here, since their name is pretty explanatory and it clearly leads to the style rule it contains.
  • Debug and Test classes: they can be used for debug reasons or for testing functionalities. In this case you can use the namespaces like _dev_, _debug_ or _test_ to make clear that they are placed under the utility layer. For instance, I place under this layer a file called .u_test.scss where I include some utility classes that I use just to check that the installed PostCSS plugins are properly working, like shown below:
/* UNDER _U_TEST.SCSS */
/* Testing presetenv */
.test_preset-env {
background: color(red alpha(-10%));
}
/* Testing presetenv pseudo selectors facilities */
.test_preset-env:matches(.test, .main) {
color: white;
}
/* UNDER _U_DEV.SCSS */
/* Use it to highlight the actual element size */
.debug_box-sizing
{
border: 1px dashed red;
}

The Litmus Test of UFO Naming Convention

If you want to check that everything is working properly, just follow this simple step by step test.

Open your main.scss that will look like below:

/* UNDER MAIN.SCSS */
/* Import asbtracts here */
@import _function.scss;
@import _mixins.scss;
.....
/* Import bases style */
@import _b_basename.scss;
.....
/* Import layout style */
@import _l_layoutname.scss;
....
/* Import objects style */
@import _o_objectname.scss;
....
/* Import utility style */
@import _u_utilitynames.scss;
.....
  1. Start from the bottom, by commenting out all the Utility @import, save and compile your CSS. If you look at the page now, you should see that both layout and makeup are pretty the same. There will be just some variations (for instance, screen reader visible-only elements will be shown in the page).
  2. Comment out all the Objects @import to find out that your components have completely lost their own make-up, but they still keep a look and feel consistent with the visual design. The layout is not still broken and everything is placed exactly where it is supposed to be.
  3. You are ready now to comment out all the Layout @import. What should happen now is that your page looses its skeleton rules and all the elements come back to their natural flow behavior, whether the look and feel is still consistent with the style guide.
  4. Last step: you are ready to comment out all the Basic @import. At this point you will have lost all the style rules. Take a look at your compiled stylesheet to check that it is empty and it’s not importing any style rules from the abstracts (remember: abstract files must contain just working tools, they will not compile any CSS rules).

If everything happens as described, it means that your test is passed successfully! ✅

How to choose the proper name

When choosing a class name, It’s a good practice to ask ourselves some questions to clarify whether:

  1. it highlights a Hierarchy Level
  2. it applies to elements which have a standalone meaning
  3. it applies a Make-up Modifier
  4. it applies a Status Modifier
  5. it inherits global rules and/or extend them

Hierarchy Level

As a good practice, I find useful to emphasize the hierarchical level between elements of the same family.

Most of the times, the style guide highlights a visual hierarchy between elements, an order of importance which drives user attention by using different sizes, colors, fonts, …

Whenever a visual hierarchy exists, It’s really useful to translate it into the code, and this can be obtained by simply using class names like .button-primary.button-secondary and so on, depending on the level of depth of the hierarchy.

This also makes themes customization a piece of cake when working with white labels: most of the times the hierarchy doesn’t change among themes, and we may simple benefit from using maps holding a collection of key/value pairs, where the key expresses the hierarchical level and what actually changes is just the relative value (I’m supposing to use SASS here or equivalent CSS preprocessors).

Elements with no standalone meaning

When choosing a class name, especially when working with components, it is important to figure out whether a child element can live alone, outside that scope, or it is tied to its block and it doesn’t have any standalone meanings.

In the former case, it can be considered as a block itself with its own naming convention that we should be able to use everywhere.

On the contrary, whenever an element has no standalone meaning (i.e. input checkbox, form input, accordion titles, …) we can apply the BEM convention to use classes formed as block name plus two underscores plus element name: .block__elem.

This is how BEM define the Elements:

Parts of a block and have no standalone meaning. Any element is semantically tied to its block.

The advantage of this conventions is to avoid nesting selectors and inheriting style rules from generic classes that are out of scope.

States and Makeup Modifiers

Most elements can have alternative styles. A make-up modifier is a mere style variation, which doesn’t communicate anything to the user. Most of the times, we use make-up modifiers to basically extend the default style of an element.

On the other side, a status modifier should communicate something more: it should tell us that some interactions have taken place through the page. It’s always the result of an action and it’s a kind of messenger which tells us: “Hey! Something happened here, so I look different now!”

Using the proper naming conventions can emphasize this crucial difference between make-up modifiers and status modifiers. As specified in my previous article, the real core of UFOCSS is to make the stylesheets as readable and informative as possibile by merging the most user-friendly conventions out there.

I’ve started using BEM recently and I find the modifier naming convention to be really useful when dealing with make-up modifiers. BEM suggests to use CSS classes formed as block’s or element’s name plus two dashes, i.e .block--modifier or .block__element--modifier.I apply this convention whenever I need to apply just a makeup variation, i.e. .buttom-primary--large.

Although BEM tells us to use modifiers class conventions to change appearance, behavior or state, I prefer to apply SMACSS convention when it comes to states. Take the case of a message element to be shown after submitting a form. It may be in a success or error status, based on the response. We can simply use a class starting with is- or has-.

The main problem with this approach is related with inheritance and extending rules. In fact, whether this naming convention works perfectly whenever we just want to inherit and/or extend the global rules defined by generic utility classes (.is-success in this case), it can cause some issues if we just want to apply a set of status rules that are strictly tied to an element, meaning that you don’t need to inherit anything and you don’t want other objects to be affected by these rules.

In this case, I find really useful to merge BEM and SMACSS conventions by using a class like.{componentname}--is-{status-modifier}i.e. .message-primary--is-success.

To summarize, you may have three kinds of modifiers classes:

  1. .{componentname}--{makeup-modifier}: use this naming convention if you just need to add a make-up variation.
  2. .is-{status-modifier}: use this naming convention if you need to inherit and/or extend global status rules defined in the utility layer.
  3. .{componentname}--is-{status-modifier}: use this naming convention if you need a status modifier which doesn’t have to inherit and/or extend any rules from other global classes and you don’t want other objects to be affected by this class.

Here is a simple use case of this naming convention:

Progressively enhanced CSS and Feature detection

Last, but not least! So many times we need to detect devices and browser’s support for one or more specific CSS features. What may be the right place for these enhanced style rules?

We may reasonably place the enhanced style rules directly under the object class, since this makes clear how an object progressively improves based on the actual browser and/or device support.

.myObject {
/* Cross browsers and devices style rules here */
  /**
* Progressively Enhanced CSS Future Queries
* Detecting browser's support for one or more specific CSS features
**/


@supports (display: grid) {
display: grid;
}
 /**
* Progressively Enhanced CSS Future Media Queries
* Detecting devices features through level 4 media features
**/


@media (pointer: fine) {
/* Style rules for devices supporting fine pointer */
}
@media (hover) {
/* Style rules applied whether the user's primary input mechanism can hover over elements */
}
}

Remember that older browsers that encounter a declaration or rule that they don’t understand just skip it completely without applying it or throwing an error. The same is true for CSS features like media query, and @supports blocks — if they are not supported, the browser just ignores them. Instead, the modern browsers will benefit from these advanced CSS features, by serving an improved user experience.