There is a fundamental problem in CSS with the class selector, which is why popular naming conventions such OOCSS, BEM, and Atomic CSS have become the gold standard in many projects. However all of these styles still rely on the class selector, which can still lead to issues of specificity, conflicts with other styles, and a lack of readability.
Allow me to start with a quick overview of the current architectural styles before explaining why data attributes are the best option for styling components and not the class selector.
OOCSS gets its cues from object-oriented programming. It does so by separating structure and skin. The purpose of OOCSS is to create stylesheets that are flexible, modular, and interchangeable.
.box { padding: 5px; } .box-header { font-size: 16px; color: red; } .box-body { font-size: 12px; color: #fff; background-color: red; } <div class="box"> <div class="box-header"></div> <div class="box-body"></div> </div>
BEM aims to create modular and reusable stylesheets by breaking down components into blocks, elements, and modifiers. It also heavily utilizes the class selector to differentiate between these component
.block { padding: 5px; } .block__element { font-size: 16px; color: red; } .block--modifier { font-size: 12px; color: #fff; background-color: red; } <div class="block block__element block--modifier"> </div>
Atomic CSS gave birth to utility-first frameworks that create components using single-purpose classes. This type of architecture ends by polluting your HTML file with arbitrary classes which is no different from writing inline styles. Utilities are not the problem they can be helpful. However, it still relies on the class selector to apply these styles.
<div class="flex flex-column flex-wrap flex-gap flex-grow-children"> </div>
Atomic CSS cannot scale by itself, especially when making complex components or prefixing related classes. To mitigate this problem PostCSS plugins such as autoprefixer and PurgeCSS are required. Without the help of these tools, Atomic CSS could not scale. Additionally, developers may use the @apply, which defeats the purpose of Atomic CSS. Using data attributes can fix an unorganized string of classes, more on that later.
This type of architecture does not specify how to write CSS selectors. Instead, they help organize CSS in sections. They are both compatible with OOCSS, BEM, and Atomic CSS. However, they do not fix the class selector.
To solve this problem, we need a new architecture that is free from the limitations of the class selector. This is where data attributes come in. The class selector by itself cannot be styled or extended with custom names, only with values. That gives data attributes an advantage over the class selector because the *
in data-*
can be replaced by any name. We can use this advantage to categorize our components give them base styles, and create variations with different values.
[data-box] { padding: 5px; } [data-box="header"] { font-size: 16px; color: red; } [data-box="body"] { font-size: 12px; color: #fff; background-color: red; } <div data-box> <div data-box="header"></div> <div data-box="body"></div> </div>
By using data attributes to style our components, we can create a more flexible and robust system that allows for easier customization and scalability. Unlike the class selector, we can use the data-box
attribute by itself without a value. The BEM example with data attributes:
[data-block] { padding: 5px; } [data-block~="element"] { font-size: 16px; color: red; } [data-block~="modifier"] { font-size: 12px; color: #fff; background-color: red; } <div data-block="element modifier"> </div>
Utility classes can be grouped as data attributes, eliminating the need to prefix classes such as flex-*
, flex-column
, flex-wrap
, flex-gap
, etc.
[data-flex] { display: flex; } [data-flex~="column"] { flex-direction: column; } [data-flex~="wrap"] { flex-wrap: wrap; } [data-flex~="grow-children"] > * { flex-grow: 1; } [data-flex~="gap"] { gap: 10px; } <div data-flex="column wrap grow-children gap"> </div>
Much more minimal and aesthetically pleasing. It is also possible to use JavaScript for styling certain utilities such as margins and paddings, completely removing them from the stylesheet.
<div data-margin="5px 0" data-padding="5px 10px 5px 8px"> </div>
There are some rules to be aware of before naming a custom data attribute.
If you haven't noticed by now, this website uses data attributes for styling. Not a single class ever defines a component. If you are interested in this methodology, I have started a small project to get you started. Visit Rams, a class-less framework.
A class less architecture does not mean that we should never use a class selector. Instead, we should move beyond the class and start thinking about other ways of styling. We can still use classes as secondary IDs and not as primary selectors. Class Less Architecture does not replace previous architecture styles it; enhances them with data attributes. Unlike classes, data attributes give both context [data-box]
and value [data-box="value"]
that we can use for styling. Furthermore, they are accessible by JavaScript providing both aesthetic and functionality.
When Marianne Brandt started her studies at the Bauhaus in 1923, she famously burned all her previous works recognizing how traditional they had been. Styling with class names is our tradition. We must move away from styling with classes and start thinking about other ways of doing so. Nowhere does the spec prohibits the use of data attributes for styling. Lens camera manufacturers do not specify that a 50mm or an 85mm lens should be used only for portrait photography. All lenses can shoot portrait or landscape photography regardless of the focal length. These are only conventions people have and are no strict rules to follow. I can use an 85mm lens for landscapes or a fish eye lens for portraits as much as I can use data attributes for styling.