DRY CSS: How to Use Declarations Just Once, Effectively

If the optimization of CSS is of particular import to you, I’ve collected several concepts in a brief book: CSS Optimization Basics .

Using declarations just once (“UDJO”?) is one way to control repetition in style sheets. It’s not a silver bullet, as we’ve seen with recent data, but it’s so powerful as to make for a key style sheet optimization method. As I’ve been applying and studying it for almost a decade, I’ll here go over the process, the process of using declarations just once.

General Steps

Before we go into all the details for how to DRY our style sheets through unique declarations, note that it’s a method that’s not trivial automate for reasons of the cascade. Even though this won’t readily turn into an automatable recipe I’ll try not to be too Jens-style sloppy.

Write CSS, the regular and natural way. Decide on DRY boundaries: section (functionally separate, usually labeled CSS parts) or file, component, @media level? (I work on a file/ @media level, that is, I typically DRY everything within the largest scope possible.) Make sure to format code consistently, as border:0; , border: 0; , and border: none; would all mean the same but make our task of finding duplicates unnecessarily complicated. Search for duplicate declarations: For new style sheets: after initial setup is done.

For new features and bug fixes: after respective work is done.

Tip: If version control highlighting for file changes is not enough, temporarily indent changed declarations to only check for their very repetition. Dissolve duplicate declarations: Check each declaration (in new style sheets) or each changed declaration for re-occurrence within the set boundary (when limiting de-duplication to sections, take care to limit search scope to these sections).

For each duplicate declaration (the actual work): Determine which respective rule should come first in the style sheet (for this one has to have an unwritten or written standard for how to order selectors). If this first rule contains additional declarations, i.e. declarations that haven’t been checked yet or that aren’t duplicates, copy the entire rule and paste it after the original; keep the discovered duplicate in the first rule and remove the other declarations, and vice-versa in the second rule, so that that rule is like the old rule just without the declaration found to be used more than once. Copy the selectors of the other rules that contain respective duplicate declaration to the rule that comes first. Make sure to remove the duplicate declarations whose selectors have just been copied up in the style sheet, and to remove the entire rule if the rule only consisted of the now moved duplicate declaration. (Repeat.)

Make sure to check the correct order of the selectors for the rules that now handle the formerly duplicate declarations.

Make sure to check for the correct placement of the rules that now combine formerly duplicate declarations. (Repeat steps 4 and 5.) Test.

The process is not simple, and yet it’s also not as intimidating as it at first looks. We’ll dive a bit deeper and also look at examples.

Special Cases

There are two scenarios that deserve extra attention.

Split files: Split style sheets can be useful, especially when they get joined again in production, but when it comes to DRYing up declarations, they constitute a “hard” barrier—it’s prohibitively much work to find and clean up duplicate declarations across files. If we work with a CSS code base of a small to medium size, switching to one style sheet may be useful, but when we deal with a complex style sheet setup, or just remind ourselves that some repetition is okay, we may also accept a file barrier. Strictness of approach, or exception handling: When we’re really strict about avoidance of declaration repetition (that is, we want to avoid repetition entirely), we will still run into exceptions to the rule. These exceptions, apart from structure-related ones like self-imposed section or hard @media and file boundaries, or language-related ones as when order is decisive (cascade), often pop up as issues of support. Selector hacks are such an exception, where some selectors are specifically there to prevent another user agent from interpreting respective rule. In such cases we cannot consolidate duplicate declarations because if we combined the affected selectors, we change the scope of the selector hack and it wouldn’t work as intended anymore.

Examples

Let’s work through some plain vanilla non-preprocessor but otherwise representative style sheets to DRY up. We’ll look at “file” level optimization, for one could simply imagine respective code as a section or module within a file to take the very same action (but without the same overall level of DRYness).

Optimizing I

The first example is from Engadget, where we’ve found 92% declaration repetition in the past; random but at least alphabetically sorted. We’ll assume the selector order to be to our liking, and we won’t change nor comment on class names and such.

.arrow-left { border-color: transparent; border-style: solid; border-width: 10px 10px 10px 0; height: 0; width: 0; } .arrow-down { border-bottom: 10px solid transparent; border-left: 10px solid transparent; border-right: 10px solid transparent; border-top: 10px solid #2b2d32; bottom: 0; height: 0; left: 20px; width: 0; } .faq-list .faq-item-title { cursor: pointer; } .faq-list .faq-item-title:after { border-left: 5px solid transparent; border-right: 5px solid transparent; border-top: 5px solid #111; content: ''; display: inline-block; float: right; height: 0; opacity: .5; vertical-align: top; width: 0; } #contact input:focus, #contact textarea:focus { border: 1px solid #3398db; } .flickity-slider>.table { table-layout: fixed; } ::selection { background: #9b59b6; color: #fff; } .videoWrapper { height: 0; padding-bottom: 56.25%; position: relative; } .videoWrapper iframe { height: 100%; left: 0; position: absolute; top: 0; width: 100%; } .i-rail-followus__socials { display: table; } .i-rail-followus__tw { vertical-align: sub; }

When we look at our list above we notice that we’re already prepared to check on and dissolve duplicate declarations. I wish this was a step to be singled out and demonstrated on its own, but until I warm up to the idea of a video (in progress) I’ll just write down what we’d be doing here, to show the resulting code.

As we’re just at the beginning of this DRY optimization process, what we’re going to do is work our way down the style sheet, looking at each declaration. The first declaration we check is border-color: transparent; . Is this found elsewhere again? No. Next, border-style: solid; ? Unique. Same for border-width: 10px 10px 10px 0; . Next, height: 0; ? Not unique.

Therefore, in this case, our DRYing begins with height: 0; . We see that this declaration is used in three other rules: .arrow-down , .faq-list .faq-item-title:after , and .videoWrapper . As we don’t have a rule that would use these four selectors, we copy them to start a new rule, right before the rule where we found the first occurrence ( .arrow-left ). In other words (and the wording here may be slightly different from the wording right at the beginning of this post), we write a new rule just for the height: 0; declaration:

.arrow-left, .arrow-down, .faq-list .faq-item-title:after, .videoWrapper { height: 0; }

Now we remove height: 0; from all other rules; as no rule consisted solely of this declaration, we don’t get to delete any of them (yet). (I typically do this in one step: When I find an “uncontroversial” duplicate I spawn a new rule, look for the other occurrences, and on each hit remove the to-be-moved declaration and copy the selector(s) to be pasted above. This comes with practice.)

We continue in the .arrow-left rule, with the declaration that came after height: 0; : width: 0; . Duplicate? Yes. And this is a good one, for when we check where else width: 0; is used, we see it’s almost but not identical to the selectors that use height: 0; . That is, we’ll start a new rule with width: 0; , making sure that we removed all former occurrences:

.arrow-left, .arrow-down, .faq-list .faq-item-title:after { width: 0; }

This rule comes right after the one we created for height: 0; , and before the .arrow-left rule that we’ve just successfully checked and dried. I find this resulting order to be useful, because I prefer it when rules come in order of importance and impact, and as we’ve been merging rules we’ve made them more impactful—so to come first.

Next up we’ll work through the .arrow-down rule. No duplicates, however some will have noticed that like the .arrow-left rule, this rule could have written more elegantly: border: 10px solid transparent; border-top-color: #2b2d32; . Again, this was not our concern just here.

On to .faq-list .faq-item-title . No repetition. In fact, this style sheet fragment was fairly easy, as we find no additional duplicates anymore:

.arrow-left, .arrow-down, .faq-list .faq-item-title:after, .videoWrapper { height: 0; } .arrow-left, .arrow-down, .faq-list .faq-item-title:after { width: 0; } .arrow-left { border-color: transparent; border-style: solid; border-width: 10px 10px 10px 0; } .arrow-down { border-bottom: 10px solid transparent; border-left: 10px solid transparent; border-right: 10px solid transparent; border-top: 10px solid #2b2d32; bottom: 0; left: 20px; } .faq-list .faq-item-title { cursor: pointer; } .faq-list .faq-item-title:after { border-left: 5px solid transparent; border-right: 5px solid transparent; border-top: 5px solid #111; content: ''; display: inline-block; float: right; opacity: .5; vertical-align: top; } #contact input:focus, #contact textarea:focus { border: 1px solid #3398db; } .flickity-slider>.table { table-layout: fixed; } ::selection { background: #9b59b6; color: #fff; } .videoWrapper { padding-bottom: 56.25%; position: relative; } .videoWrapper iframe { height: 100%; left: 0; position: absolute; top: 0; width: 100%; } .i-rail-followus__socials { display: table; } .i-rail-followus__tw { vertical-align: sub; }

Optimizing II

For our second example I took eBay. This time let’s also just go for basic re-formatting (indentation, alphabetical declaration sorting), yet again we encounter things to avoid (presentational class names, notably, or lack of fallback fonts—though that lack may, for fonts like Arial, not be an issue in practice after all).

.sh-GifCont { color: #999; font-family: Verdana !important; font-size: 10px; font-weight: normal; padding: 0 10px 4px 10px; } .sh-GetFastImg { background-image: url('http: //ir.ebaystatic.com/pictures/aw/pics/de/viewitem/spr4VI.png'); background-position: 0 -178px; background-repeat: no-repeat; float: left; height: 16px; margin-right: 4px; width: 56px; } .sh-float-l { float: left; } .sh-FrZip { padding: 10px 0 0 15px; width: 12%; } .sh-FrDelLoc { padding: 10px 15px 0 10px; width: 10%; } .sh-FrCnt { padding-left: 10px; padding-right: 0; text-align: left; } .sh-FrZipCnt { padding: 0 0 0 15px; } .sh-FrDelLocCnt { padding: 0; } .sh-FrBtn { padding: 5px 15px 10px 8px; } .sh-FrDelSite { padding: 6px 0 0; } .vi-frs-sh-FrSlctr { display: inline; padding: 6px 15px 0 10px; } .sh-FrZipDiv { display: inline; padding: 6px 15px 0 0; } .sh-FrTxt { color: #333; font-family: Arial; font-size: 12px; font-weight: normal; padding-left: 15px; } .sh-FrLrnMore { display: inline; padding: 10px 10px 10px 15px; } .sh-FrQuote { display: inline; } .sh-FrLnk { margin-top: 5px; } .sh-Tbl { padding: 10px; } .sh-TblCnt { color: #333; font-family: Arial; font-size: 12px; font-weight: normal; padding-left: 15px; } .sh-TblHdr { color: #5d5d5d; font-family: Verdana; font-size: 12px; font-weight: normal; padding-left: 15px; } .sh-Info { color: #999; font-family: Arial; font-size: 11px; font-weight: normal; } .sh-FrSbTxt { color: #999; font-family: arial; font-size: 11px; font-weight: normal; padding-left: 15px; } .sh-FreightHdr { color: #333; font-family: Verdana !important; font-size: 10px; font-weight: normal; padding: 5px 0 5px 23px; } .sh-Freight-Hdr { color: #333; font-family: Verdana !important; font-size: 10px; font-weight: normal; padding-left: 13px; } .sh-Cnt { color: #5d5d5d; font-family: Arial; font-size: small; font-weight: normal; padding-left: 13px; } .vi-frs-sh-TxtCnt { color: #333; font-family: Arial; font-size: 12px; font-weight: normal; } .sh-BtnTxt { color: #333; font-family: Verdana,Tahoma,Arial; font-size: 12px; font-weight: normal; height: 24px; margin: 0; padding: 0 3px; position: relative; text-decoration: none; top: 0; } .sh-bubble-position { float: left; padding-top: 5px; } .sh-del-lrge b { font-size: 15px; } .sh-gspFirstLine { color: #333; font-family: Arial; font-size: 15px; padding: 25px 10px 5px 0; } .sh-gspSecondLine { color: #777; font-family: Arial; font-size: 12px; padding: 0 10px 15px 0; }

We can tell what the first step is: Has color: #999; been used more than once? It has been. And so we create its own rule first:

.sh-GifCont { color: #999; }

and add the selectors for the other two occurrences:

.sh-GifCont, .sh-Info, .sh-FrSbTxt { color: #999; }

What helps us, again, is the assumption that our current style sheet already uses the selector order we want and that rule shifts are unproblematic. (I trust that it’s evident why being clear about selector order is useful and how one would resolve cascade issues—by not moving the affected rules or not merging them, respectively.)

font-family: Verdana !important; is up next; it’s likewise used three times. Then font-size: 10px; —and here, and that’s why this is all great to go over some actual code, we notice something we need to pay attention to (to avoid selector repetition): This declaration is used by the same selectors we’ve just grouped for the “Verdana” declaration. And so instead of

.sh-GifCont, .sh-FreightHdr, .sh-Freight-Hdr { font-family: Verdana !important; } .sh-GifCont, .sh-FreightHdr, .sh-Freight-Hdr { font-size: 10px; }

we merge the rules to:

.sh-GifCont, .sh-FreightHdr, .sh-Freight-Hdr { font-family: Verdana !important; font-size: 10px; }

In this manner we’ll work through the entire style sheet, until we get:

.sh-GifCont, .sh-Info, .sh-FrSbTxt { color: #999; } .sh-GifCont, .sh-FreightHdr, .sh-Freight-Hdr { font-family: verdana !important; font-size: 10px; } .sh-GifCont, .sh-FrTxt, .sh-TblCnt, .sh-TblHdr, .sh-Info, .sh-FrSbTxt, .sh-FreightHdr, .sh-Freight-Hdr, .sh-Cnt, .vi-frs-sh-TxtCnt, .sh-BtnTxt { font-weight: normal; } .sh-GifCont { padding: 0 10px 4px 10px; } .sh-GetFastImg, .sh-float-l, .sh-bubble-position { float: left; } .sh-GetFastImg { background-image: url('http: //ir.ebaystatic.com/pictures/aw/pics/de/viewitem/spr4VI.png'); background-position: 0 -178px; background-repeat: no-repeat; height: 16px; margin-right: 4px; width: 56px; } .sh-FrZip { padding: 10px 0 0 15px; width: 12%; } .sh-FrDelLoc { padding: 10px 15px 0 10px; width: 10%; } .sh-FrCnt { padding-left: 10px; padding-right: 0; text-align: left; } .sh-FrZipCnt { padding: 0 0 0 15px; } .sh-FrDelLocCnt { padding: 0; } .sh-FrBtn { padding: 5px 15px 10px 8px; } .sh-FrDelSite { padding: 6px 0 0; } .vi-frs-sh-FrSlctr, .sh-FrZipDiv, .sh-FrLrnMore, .sh-FrQuote { display: inline; } .vi-frs-sh-FrSlctr { padding: 6px 15px 0 10px; } .sh-FrZipDiv { padding: 6px 15px 0 0; } .sh-FrTxt, .sh-TblCnt, .sh-FreightHdr, .sh-Freight-Hdr, .vi-frs-sh-TxtCnt, .sh-BtnTxt, .sh-gspFirstLine { color: #333; } .sh-FrTxt, .sh-TblCnt, .sh-Info, .sh-FrSbTxt, .sh-Cnt, .vi-frs-sh-TxtCnt, .sh-gspFirstLine, .sh-gspSecondLine { font-family: arial; } .sh-FrTxt, .sh-TblCnt, .sh-TblHdr, .vi-frs-sh-TxtCnt, .sh-BtnTxt, .sh-gspSecondLine { font-size: 12px; } .sh-FrTxt, .sh-TblCnt, .sh-TblHdr, .sh-FrSbTxt { padding-left: 15px; } .sh-FrLrnMore { padding: 10px 10px 10px 15px; } .sh-FrLnk { margin-top: 5px; } .sh-Tbl { padding: 10px; } .sh-TblHdr, .sh-Cnt { color: #5d5d5d; } .sh-TblHdr { font-family: verdana; } .sh-Info, .sh-FrSbTxt { font-size: 11px; } .sh-FreightHdr { padding: 5px 0 5px 23px; } .sh-Freight-Hdr, .sh-Cnt { padding-left: 13px; } .sh-Cnt { font-size: small; } .sh-BtnTxt { font-family: verdana,tahoma,arial; height: 24px; margin: 0; padding: 0 3px; position: relative; text-decoration: none; top: 0; } .sh-bubble-position { padding-top: 5px; } .sh-del-lrge b, .sh-gspFirstLine { font-size: 15px; } .sh-gspFirstLine { padding: 25px 10px 5px 0; } .sh-gspSecondLine { color: #777; padding: 0 10px 15px 0; }

This now looked like a lot of complicated work—but all it takes is intent and practice as well as the understanding that the complicated and lengthy work is only done in completely unsorted, naturally WET style sheets. Once we’re clear about our coding standards and our optimization steps, updating and maintaining style sheets is rather easy. All you do is make sure you cross-check your edits.

Let’s actually go over this very briefly, too, in a third example, a snippet from Code Responsibly. Just because it was convenient.

h1, h2 { color: #000; font-family: futurastd-book, futura, 'droid sans', 'helvetica neue', helvetica, sans-serif; font-weight: 400; line-height: 1.13; } h1 { font-size: 1.86em; margin: 0 0 .53em; } h2 { counter-increment: counter; font-size: 1.5em; margin: 1em 0 0; }

On top of the convenience, let’s also construct a simple update: We’ll assume that we need a different h1 margin and it happens to be margin: 1em 0 0; . Now some of us will immediately spot what we can and should do. But for the sake of showing one possible practice, here is what I’d do in an otherwise more complex style sheet.

First, make the change, mark it somehow (my editor—typically IntelliJ IDEA—shows changes and makes it easy to spot them but I might still temporarily indent new or updated declarations), and test it:

h1, h2 { color: #000; font-family: futurastd-book, futura, 'droid sans', 'helvetica neue', helvetica, sans-serif; font-weight: 400; line-height: 1.13; } h1 { font-size: 1.86em; margin: 1em 0 0; } h2 { counter-increment: counter; font-size: 1.5em; margin: 1em 0 0; }

Second, after testing has been completed and successful, I’d check all those changed lines whether they’d now need to be DRYed—the process we’ve gone over a few times now. Here we’d all discover that the margin is identical for both level 1 and level 2 headings.

Third, following a rough way of lumping this into distinct steps, we’d now all create a h1, h2 rule but also notice that those already have their own rule, and hence merge them into one. That leads to the following end result for the snippet:

h1, h2 { color: #000; font-family: futurastd-book, futura, 'droid sans', 'helvetica neue', helvetica, sans-serif; font-weight: 400; line-height: 1.13; margin: 1em 0 0; } h1 { font-size: 1.86em; } h2 { counter-increment: counter; font-size: 1.5em; }

And that, I hope, gives a brief taste that maintaining style sheets is far less complicated and intimidating than bringing them in order. Because that, and I had given myself a good bucket of my own medicine when I wrote about the excess of repetition in commercial websites and for that DRYed Yandex, can at first be quite some labor. Important labor, yes, but also quite some dull labor. Nobody said optimization would need to be easy.

Tips

Loosely I wish to share a few extra tips (as I do update articles on meiert.com, I may amend these tips over time).

Search case-insensitively. In multi-developer environments and due to the the fact that we may habitually or accidentally press Shift it can happen that declarations consist of the same letters, but different case. It’s conceivable that this difference matters (some parts of our style sheets, like generated content or URLs, may be case-sensitive), but as it’s easier for us to spot the differences that matter than the ones that don’t we should search case-insensitively. eBay provided us with a good example: Independent of the missing fallbacks, some of their font declarations are different in case—we find font-family: Arial; and font-family: arial; . We want to consolidate these, and you can tell that the DRY version uses just one declaration in, of course, one spelling (lowercase).

Close every (pre-production) declaration with a semicolon. To save us from overly broad searches, however, we should make sure to always include the semicolon (those who close all declarations—my recommendation for operation files—are at an advantage here). This makes it easier to just copy and move declarations around—but also spares us from a ton of false positives (and there will be a few). The (by all means useful) !important mark may be a great example for this—when we only search for “unclosed” declarations, border: 0 will surely match border: 0 !important , and so are there an endless number of other factually different declarations that can come up. That makes our work a lot harder and more error-prone.

Use a standard selector order. Not only do we then limit style sheet entropy, but, as any new rules “automatically” end up in clearly defined spots, we also have a natural defense against selector duplication—and we don’t yet want to also write posts like this to also keep our selectors dry. Consider using my selector order draft, helping us, the web development community, to standardize one, or building your own.

While we look into the efficiency of this approach and ways to optimize it further, there are also things that our tools could do to help. In list form as here, too, we may end up with more needs over time:

Editors could help us DRY our style sheets by highlighting duplicate declarations. Personally I think a little “⚠” at the end of the line, configurable in each editor’s settings, would be awesome, along with a way to ignore/turn off notifications for particular lines. This would make our job of keeping style sheets DRY a lot easier—not only to track redundancy but at all get an idea of how problematic the case is. As we have seen, in some cases style sheets consist of 90% declaration repetition.

❧ At the end of this comprehensive look at how to get to and how to work with declaration-DRY style sheets I’m sure there are still some open loops; and yet I wish there are fewer than with what we looked at before, when for many years, we neglected this way of optimizing our CSS. (From my view, DRY CSS would have granted us a more sober look at variables and other features we later added to the specs.)

The open loops should be addressed, and I wish that you can help us work on them. For one, as the Yandex case should have shown, declaration DRYness is not a silver bullet. It helps to make our CSS more compact and more manageable, but there are edge cases in which it can also make them more complex. We will benefit from looking into these edge cases (like the Yandex one). For another, one or the other inaccuracy will have slipped in. I noticed during my writing that I lacked the attention to guarantee absolute precision on the needed steps for an algorithm to make a style sheet DRY, or to keep it there. Maybe that has been useful so that the list does not deter anyone. But maybe it’s imprecise in some important aspect. Please help looking at these issues.

Finally, then, I deem “UDJO” a crucial way of working with CSS. In fact, I see no other single optimization approach apart from following one’s own set of coding standards than this one—because without a systematic approach to writing CSS, and UDJO grants that, all our regular work on CSS creates entropy, and entropy doesn’t lead to quality code, which in turn is what we aim for as professionals. Let’s pay more attention to quality code, let’s pay more attention to DRY CSS, and let’s see what we can automate of it without neglecting our craft.

About Me

I’m Jens Oliver Meiert, and I’m a web developer and author. I love trying things (sometimes involving philosophy, art, or adventure). Here on meiert.com I share some of my views and experiences.

If you have a question or suggestion about what I write, please leave a comment or a message.