How to structure your CSS better as components
In the last post we had a look at the evolution of early CSS frameworks and browser style resets that helped developers control the differences of old browsers.
However making the site look pretty and consistent is just part of the story: Even more valuable is how the CSS can be maintained.
Indeed, the ratio of time spent reading versus writing is well over 10 to 1. We are constantly reading old code as part of the effort to write new code. ... [Therefore,] making it easy to read makes it easier to write.
~ Robert C. Martin, writer of the Clean Code book
In this post we're going to take a look at how we can write maintainable CSS. The first step is to look at the website as group of components, an idea popularized by CSS frameworks like Twitter Bootstrap.
Reusable components
With Twitter Bootstrap there was a slight change of general mindset backwards: From having schematic HTML with applied styles, to extending the schematics of HTML with reusable CSS classes.
Instead of having the <button>
tag that looked like a button, we'd have a CSS class .btn
that would make any element styled like a button.
This meant that we could still have a button-button: <button class="btn">
, but if we'd like to schematically have a link that would look like a button, we could do: <a class="btn">
.
This worked fine as there was a consistent naming in the CSS classes: Buttons, wells, rows, columns, accordions, popups, containers etc. But what happened when we ran out of names? Well, we'd often get creative with prefixes and postfixes:
.content .content-inner-wrapper .outer-container .container {
padding-top: 10px;
}
Web developers would often run into a situation when there wasn't any good name for the CSS component in hand; especially with older browsers you'd often ned to use an extra <div>
or two to work with the CSS padding-box model.
The CSS' global nature had turned against itself. What used to be a strength would now be a burden to handle.
BEM
About the same time when Yahoo created their YUI library (which by the way had one of the first CSS resets) and Eric Mayer published his own reset.css
, a Russian search giant Yandex was starting their own CSS framework.
By the year 2012 they had come to a well-structured CSS naming convention: BEM. BEM stands for "Block", "Element" and "Modifier". It is still to date used to overcome the challenges of global CSS.
BEM breaks up the naming to modular components that they call blocks. Blocks are a way of creating the main encapsulating entity that you're creating, for example newsletter-signup
, blogpost
or product-description
.
Now blocks usually have some elements inside them. In the product-description
example we could have an element table
or img
to describe some part of the product.
The single image might still have different states it can be in. In BEM they are called modifiers. Modifier in the image example could be e.g. left-side
or fullwidth
.
To stitch tall of this together, we'd have a CSS class like this:
.product-description__img--fullwidth {
display: block;
width: 100%;
float: none;
clear: both;
height: auto;
}
BEM is particularly great if you have a "traditional" website where you don't want to use any preprocessor, but you want to make sure many people can edit the same CSS file, making sure there's no conflicts.
Also using BEM is great when you want to have a consistent styling in your CSS. It is by far the most strict style of writing CSS, where for example SMACSS is more of a vague guideline for a way to name your CSS files.
Conflicts in CSS classes using BEM are rare, but some might still occur over time. Still usage of BEM is a good way to take control over your CSS, as it brings you a natural way of thinking the whole website as a collection of independent components. When you use BEM in conjunction with keeping the CSS in sync with lightweight template files, you should be able to remove any dead code when you renew your components over time.
CSS Modules
Now while BEM is good at keeping the components' names unique, you still might run into some edge cases.
Let's say you have a component called product-description
. Now you have a task to create a new version of that same component from scratch. What would be the new block name for the component? procuct-description-new
? You can see how this escalates with future iterations.
Of course this is an edge case, but there might still be a better way to do CSS. Would it help if we would use a post processor to create an unique hash after all the CSS class names? This would allow us to use the same product-description
as the component's name — but the result would still have unique names, and it would guarantee no conflicts in the codebase.
Building your own CSS Modules
Let's write a quick JS script to do this. First we find the CSS in our CSS files:
const matchCss = /([^{]+)({[^}]*})/gm
This regular expression will split the CSS code in two groups: Selector and CSS content. For example the CSS rule .hello { color: red; }
would split into groups .hello
and { color: red; }
.
Now let's create a simple hashing function:
function computeHash(data) {
const hash = crypto.createHash('md5')
hash.update(data)
return hash.digest('hex')
}
This creates a simple MD5 hash from the data we pass it. To add the hashes, we could perform a simple replace function:
function addHash(selectors, content) {
const hash = computeHash(content)
return selectors
.split(/\s+/g)
.filter(v => v)
.map(selector => `${selector}_${hash}`)
.join(' ')
.concat(' ' + content)
}
function addHashes(cssContent) {
return cssContent.replace(
matchCss,
(match, selectors, content) =>
addHash(selectors, content)
)
}
This function takes CSS content as input, looks for CSS blocks, hashes the content of a single CSS block and adds the hash to each of the blocks' selectors.
So for example the following css:
.hello-world {
background: #C0FFEE;
padding: 10px;
font-family: monospace;
font-size: 20px;
}
Would convert to:
.hello-world_1013c7061696b380717e6ec57be3013b {
background: #C0FFEE;
padding: 10px;
font-family: monospace;
font-size: 20px;
}
This way we can be sure, that the CSS class names are always unique. We could write and update components that always guarantee to have unique names, eliminating the need of naming conventions like BEM altogether.
To actually use these obfuscated class names in our HTML, we'd need to render the class names in dynamically. For that we could do a function that maps the original names to the obfuscated names:
function mapNamesFromCss(cssContent) {
const nameMap = {}
let match = matchCss.exec(cssContent)
while (match) {
const [content, selectors] = match
const hash = computeHash(content)
selectors
.split(/\s+/g)
.filter(v => v)
.map(sel => sel.replace('.', ''))
.forEach(selector => {
nameMap[selector] = `${selector}_${hash}`
})
match = matchCss.exec(cssContent)
}
return nameMap
}
Now if we run the original css through the function, we can use the plain CSS class names to fetch the obfuscated key names in our code:
const css = `.hello-world {
background: #C0FFEE;
padding: 10px;
font-family: monospace;
font-size: 20px;
}`
const cssClassNameMap = mapNamesFromCss(
css
)
/*
cssClassNameMap = {
'hello-world':
'hello-world_1013c7061696b380717e6ec57be3013b'
}
*/
function renderHelloWorldComponent() {
const div = document.createElement(
'div'
)
const hashedClass =
cssClassNameMap['hello-world']
div.className = hashedClass
return div
}
So the cssClassNameMap
variable now holds a javascript object, which has one key hello-world
that you can use as a key to fetch the hashed value of that css class: hello-world_1013c7061696b380717e6ec57be3013b
.
And this is the way a library called CSS Modules work. The library has plugins for Webpack and Browserify, so you can plug in the library to your existing workflow. CSS Modules work with regular JavaScript imports, so you can do external CSS files and import them to your JS code.
So we can use the previous code like this:
import cssClassNameMap from './hello-world.css'
function renderHelloWorldComponent() {
const div = document.createElement(
'div'
)
const hashedClass =
cssClassNameMap['hello-world']
div.className = hashedClass
return div
}
The beauty of using CSS Modules is, that all the CSS will be specific to a single component. So instead of writing global CSS and hoping that some new component doesn't mess with our existing global CSS, we're writing local CSS that is only used by a specific component in our code. But it doesn't restrict you to use only the local CSS: You can still use any CSS framework, grid, or reset in conjunction with your CSS Modules.
Also module bundlers like Webpack only ever include the CSS that you import, so you never end up having dead CSS after you ditch your old components.
Takeaway
I have always enjoyed the simplicity of plain CSS. It's easy to understand and you can manage to style about anything by memorizing a handful of CSS properties.
However not every project is small and simple, and maintaining a huge amount of styles is not where CSS' one-size-fits-all solution would be the perfect solution. Fortunately CSS has evolved as the standards and browsers have progressed, and so have the different tools around CSS. Now we're in a situation where there's plenty of tools and different styles so you can find the perfect way to style your HTML components for every project.
In the next blog post we're going to have a look at a new technology called the Shadow DOM, which promises to solve some of CSS' initial problems. Subscribe to the mailing list below to receive a notification when the new blog posts are out!
Read the next post here: