DRY CSS: How to Use Declarations Just Once, Effectively
Published on OctĀ 26, 2017 (updated JulĀ 23, 2024), filed under development, css, optimization (feed). (Share this on Mastodon orĀ Bluesky?)
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.
Contents
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, at-rule level? (I work on a file/
@media
level, that is, I typically DRY everything within the largest scope possible.) - Be sure to format code consistently, as
border:0;
,border: 0;
, andborder: none;
would all mean the same but make our task of finding duplicates unnecessarily complicated. - Search for duplicate declarations:
- For new style sheets: after the initial setup has been done.
- For new features and bug fixes: after the respective work has been 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, 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.
- Be 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.)
- Be sure to check the correct order of the selectors for the rules that now handle the formerly duplicate declarations.
- Be 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;
}
Editing
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 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;
andfont-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 matchborder: 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.
Tool Needs
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 (long: Jens Oliver Meiert), and Iām a web developer, manager, and author. Iāve been working as a technical lead and engineering manager for companies youāve never heard of and companies you use every day, Iām an occasional contributor to web standards (like HTML, CSS, WCAG), and I write and review books for OāReilly and Frontend Dogma.
I love trying things, not only in web development and engineering management, but also in other areas like philosophy. Here on meiert.com I share some of my experiences and views. (I value you being critical, interpreting charitably, and giving feedback.)