Building in 10k: CSS Structure and Sandboxing
Editor’s note: This is the fourth in a series of posts from the team that built the 10k Apart contest site, exploring the process of building for interoperability, accessibility, and progressive enhancement in less than 10kB.
In the previous post in this series, Stephanie Stimac discussed her design process for the 10k Apart site. Once the team had settled on a direction, it was up to me to realize her vision in CSS.
What’s the “no style” experience like?
Whenever I begin to design an experience in CSS, my first consideration is how usable it is without any CSS at all or with only a browser’s default styles. Why does that matter? Because CSS is a dependency that may not be met.
First off, some browsers don’t support CSS—text-based browsers (e.g., Lynx) and non-visual browsers (e.g., every screen reader), for example. Those scenarios may not be your target, but they are worthy of consideration.
Then there’s the possibility that the browsers supports styles, but they aren’t being applied. There are a number of scenarios where this could occur:
- A user may disable styles to improve performance or reduce page weight. When I was rocking my Treo 650, I used Blazer’s Fast Mode extensively, disabling CSS, images, and JavaScript in order to make the Web usable on my 2G connection. Numerous browsers (especially mobile ones) offer similar bandwidth-saving/performance-boosting features.
- A network hiccup or CDN outage can cause requests for your CSS to go unanswered.
- An improperly configured firewall can block requests for your CSS.
- If your styles are sequestered within a media query and the browser does not support media queries or doesn’t implement that particular media query, they won’t be applied.
- If the selectors you use are not supported by the browser, your styles may not apply.
Any of these could result in your users experiencing your creation with only default browser styles applied. That may seem like a scary notion, but it’s also something you can celebrate. Maybe on April 9th (a.k.a. CSS Naked Day).
Thankfully, good markup practices go a long way. I talked at length about the markup decisions I made in the second post in this series and I’m happy to report that those choices serve us well in both the “no style” and “default style” scenarios.
How can I progressively enhance the design using CSS?
As the “no styles” scenario proves, the design you ideally want to get in front of your users is not necessarily what they will experience. We need to come to terms with that. We don’t control where our pages go or how our users experience or interact with them. We need to banish the thought of creating a single, ideal, monolithic experience that is identical for every person because the harsh reality is that one experience may not work for everyone. We’re all different, so our designs need to be flexible. We need to view experience as a continuum.
The CSS we write offers suggestions and guidelines for how a browser could lay elements out on the screen, but that is all they are… suggestions. Users can author their own stylesheets that tailor our content to their needs. Users can change the font size in their browser or their operating system. Users can reduce the brightness of their screen to save battery life. Users can set their operating system into high-contrast mode. Users can and will do all of these things (and more!) at the same time. If we want to reach them, we need to be flexible in how we deliver our design vision.
There are numerous techniques that enable this sort of flexibility in your CSS, but one of the most fundamental is understanding how the CSS language is designed. One of the core features of the language is a concept known as “fault tolerance”. At its most fundamental, fault tolerance governs how a browser recovers from errors it encounters in the CSS files we author. Those errors can be legitimate mistakes—a missing colon, semicolon, or curly brace—or they can simply be a language feature the browser doesn’t understand. Understanding how fault tolerance works in practical terms can help you write CSS that progressively enhances the visual design of your sites while ensuring every user has access to the content.
We already covered the “no style” experience, so I’ll jump in with a simple example of this from the 10k Apart site:
.gist table { margin-bottom: 0; }
In this excerpt, we see a rule set with three declarations:
color: #046
– This declaration sets the foreground color of links to a dark, dusty blue.border-bottom: 1px dotted
– This declaration sets a 1px wide dotted border on the bottom of the links (a little less garish than an underline). Theborder-bottom
property is shorthand forborder-bottom-width
,border-bottom-style
, andborder-bottom-color
, but I haven’t set a color which means the border-color will be inherited from color (the foreground color).border-bottom-color: rgba(0, 68, 102, 0.25)
– This declaration sets the color of the border to the same blue, but at 25% opacity using RGBa.
I’m betting most of you are wondering why I didn’t write the border-bottom-color
value into the border-bottom declaration
. There’s a method to my madness: I want to deliver a color to every browser, even if it isn’t the ideal color we’d like them to get.
Not every browser out there supports RGBa (though many do). If I included the RGBa value in the bottom-border property
and a browser came along that didn’t support RGBa, it would consider that declaration to have an error in it. The rules of fault tolerance instruct browsers to ignore errors and move on, so the link would be colored blue, but it would not have a bottom border. The border is a useful visual indication of a link and losing it reduces the usability of the site. By setting border-bottom
first and inheriting the foreground blue color, I guarantee that every browser (at least every one that supports borders) will include a border underneath the text. It may be a little darker than I’d ideally like it to be, but at least it’s there. By setting the RGBa border-bottom-color
separately, I isolate the potential support issue and if a browser loads the page but doesn’t support RGBa, it can throw away that declaration and the default foreground color will still apply.
The rules of fault tolerance may seem a little strange at first, but they actually make a whole lot of sense. Browsers ignore the most discrete piece of CSS that they can and move on, sometimes saving us from our own typos, other times enabling us to deliver new features to modern browsers without sacrificing the experience of older ones.
Let’s look at another excerpt:
.gist table { margin-bottom: 0; }
It’s worth noting that this code contains a hack, of sorts. The *:-o-prefocus part of the selector in the second rule set is a vendor-prefixed selector created by Opera (hence “-o-”) to enable us to apply specific styles to their browser. We’ll circle back to that, but first I want to go through what this code does:
code { … }
– This rule set changes the font size ofcode
elements to 1.6875rem (roughly 22px, scaled relative to the html element’s font size).*:-o-prefocus, code { … }
– This rule set forces allcode
elements to inherit their size from their container.
I set the code
elements a little larger than the body copy to give them an x-height that is roughly-equivalent to the surrounding text. The problem was that Opera, for whatever reason, was making the code
elements huge. Clown shoe huge. So I opted to use a tool that Opera offers to direct specific styles to its browsers: *:-o-prefocus
.
The “hack” is not really what’s interesting here… the way it works is. You’ll notice that it’s part of a compound selector. The first selector in the compound selector is the hacky Opera-specific bit, the second is a standard element selector. The Opera-specific bit is actually a red herring as it doesn’t select anything. If you were to say
.gist table { margin-bottom: 0; }
nothing would happen. So why does this work? As you’ll recall, the rules of fault tolerance require that browsers ignore anything they don’t understand. In this case, the selector is not understood by any browser but Opera (and even then it’s Opera 9.5 and up). So, returning to the original excerpt, Opera 9.5 and higher will set code
elements to inherit their font size from their parent, but every other browser will ignore the entire rule set.
That seems odd right? I mean there’s a comma between the two selectors. True, but the comma means “or”; browsers treat the whole thing as a single selector. If they don’t understand part of it, they throw away the entire rule set.
This example is a bit specific, but it shows the power of the technique. It’s also one that was used to great effect in Egor Kloos’ Gemination entry to the CSS Zen Garden. In his CSS, he used attribute selectors to deliver one set of rules to IE6 (the then-current version of IE) and another set to the other browsers available at the time (Mozilla, Opera, and Safari):
.gist table { margin-bottom: 0; }
Using this technique, he delivered two wildly different designs (and shamed IE6 in the process). It worked because IE6 didn’t understand attribute selectors, so it ignored all of the rule sets that included them. When IE7 came along and included support for attribute selectors, it got the fancier layout automatically. Egor’s design was perfectly future friendly!
So now we’ve covered fault tolerance for progressively enhancing on the declaration level and the rule set level, but what if you want to do something a little more substantial? That’s where at-rules come in.
Consider the following:
.gist table { margin-bottom: 0; }
In this excerpt, the padding is set to one value for all scenarios and then changed to another value if the browser width is 20em (~320px) or larger. Browsers that understand media queries and meet the minimum width requirement will set their left and right padding to 1.375rem. Browsers that either don’t support media queries or support them, but don’t meet the criteria will apply the .5em padding. Done and done.
Recently, CSS was granted an incredibly useful tool for progressive enhancement in the form of another at rule: feature queries (a.k.a. @supports
). Like the @media
block, the @supports
block acts as a wrapper around a collection of rule sets and enables you to selectively apply them only if your test condition evaluates as true. Queries can be made for a property or a property-value pair. As with @media
blocks, you can also check for lack of support using the “not” keyword too. In the 10k Apart site, I used @supports
rather sparingly to check for flexbox support in one specific area:
.gist table { margin-bottom: 0; }
In this particular instance, I only wanted these rules applied if flexbox was supported. If not, I applied an alternate set of rules.
It’s worth noting that, as of this writing, @supports
is still quite new, but it is—er—supported in more browsers with each new release. While you can use the “not” keyword, positive test cases like the one you see above are your best option. They are forward-compatible and only modern browsers currently understand @supports
anyway. Older browsers will simply ignore the whole thing, making backward compatibility testing less useful. Although, as with media queries, lack of feature query support is, in fact, the first feature query.
Isolating collections of rule sets isn’t limited to @media
and @supports
either. You can also use media queries in the media
attribute assigned to link
-ed stylesheets. I used this approach to provide one set of simple, small screen friendly default styles (d.min.css
) to all browsers and another set of more advanced styles to only modern browsers (a.min.css
):
.gist table { margin-bottom: 0; }
The reason I chose this approach (as opposed to including all of the rules in one stylesheet) is that it speeds up the download experience for folks on older browsers—consider it a polite acknowledgement of all the problems they already have to deal with. All I had to do was set the media
of the advanced stylesheet to “only screen”. The “only” keyword is part of the media queries spec and browsers that don’t know what media queries are see “only” and treat it as an error. They don’t even bother downloading the file and stick with the linear, small screen experience. Modern browsers get both CSS files and pick up a host of additional media queries to handle the responsive side of things within the advanced stylesheet.
How do I organize my styles across so many breakpoints?
Since I’m on the topic of media queries and breakpoints, I thought it might be fun to take a little detour and discuss the underlying architecture I used for the CSS. If you’re not into that sort of thing, feel free to skip this section.
Over the last 15 years I’ve been working with CSS, my approach to organizing things has evolved considerably. Back in the day before preprocessors, I used to work in huge stylesheets. I used CSS flags to help me navigate through hundreds, if not thousands of lines of CSS. It worked for a while.
Then I began breaking things up into component-based stylesheets which I’d load back into the main one using @import
. Sadly @import
has severe performance and caching issues, so I returned to the world of mammoth stylesheets.
Then came Sass. I was apprehensive about preprocessors at first, but then I realized I could return to a saner world of smaller, more manageable stylesheets that Sass would combine them into a single file automatically. I was free to use @import
without my users suffering from all of their performance problems. In a lot of ways Sass’ use of @import
for concatenation was the gateway drug to preprocessing for me.
When it came to managing responsive designs, I’ve tried a few different approaches. At first, I created separate CSS files based on the project’s breakpoints. It made the stylesheets I was managing smaller, but everything was still jumbled together to a large extent. Then I discovered Breakup. Breakup is a Sass library that enables you to configure a set of named breakpoints and then reference those in your Sass files to organize your code. It’s a simple concept that’s been done many times over, but what I like about Breakup over similar solutions is its output control. Let me explain…
I have a set of helper Sass files that I use for storing variables, mixins, and the like. I defined my default breakpoints in variables.scss
(the values for those breakpoint variables are irrelevant, so don’t worry about them):
.gist table { margin-bottom: 0; }
With that list in place, I can invoke a breakpoint like this:
.gist table { margin-bottom: 0; }
That may not seem particularly interesting in and of itself, but Breakup’s output control is impressive. By implementing the breakpoints in this manner, I can maintain all of the CSS applicable to a particular component in one file. That helps with overall organization (which I’ll talk more about in a minute). Then, I can import all of the component files into both the default and advanced stylesheets, using Breakup to selectively include only the bits I want for each.
For the advanced CSS file, it’s pretty simple. I just set the included breakpoints in a variable and then import the component files:
.gist table { margin-bottom: 0; }
Here I’m saying I want the advanced CSS file to include all of the named breakpoints, with the exception of the “global” one. Fairly straightforward.
The default CSS file setup is a little more complicated and (dare I say) magical:
.gist table { margin-bottom: 0; }
Here I’m saying I want to include only the “global” blocks from my component CSS files (which are imported at the bottom). The $breakup-breakpoints-allow-naked
variable is a list of breakpoints that should have their media queries (or @media
declarations) stripped when $breakup-naked
is set to true. Those 3 steps ensure my “global” blocks play nicely with older browsers.
I’ve taken things a step further in terms of overall organization, using Sass partials and “roll-up” files to make the include process a little easier. It starts with my folder structure. Within my Sass source folder, I have the two .scss
files (a.scss
and d.scss
) and five sub-directories:
./base
– Contains Sass partials CSS pertaining to general HTML elements;./components
– Contains Sass partials pertaining to components (a.k.a. patterns) used in the design, like the gallery component I’ve mentioned in previous posts;./helpers
– Contains Sass partials devoted to overall Sass configuration, functions, mixins, placeholders, and variables;./pages
– Contains Sass partials devoted to pages with unique design considerations, like the homepage; and./vendors
– Contains 3rd party Sass libraries like Breakup.
Within each of these sub-directories, there is a “roll-up” file and a collection of partial files. For example, in the case of the helpers
directory, the contents are
__helpers.scss
,_config.scss
,_functions.scss
,_mixins.scss
,_placeholders.scss
, and_variables.scss
.
Rather than having to include each of the single underscore partials in a.scss
and d.scss
directly, I use a file named the same as the directory, prefixed by two underscores (in this case, __helpers.scss
) to contain the individual @imports
for everything else in the directory:
.gist table { margin-bottom: 0; }
With that setup for each of the subdirectories, a.scss
and d.scss
remain a little more manageable. Here’s a.scss
, for instance:
.gist table { margin-bottom: 0; }
Using the “roll-up” files (like __helpers.scss
) enables me to manage each aspect of the CSS on a much more modular basis. If I need to add a new component or remove a vendor library I’m not using anymore, I can do it in one place rather than having to track it down in multiple files. It may be overkill for projects you’re working on, but it gives me peace of mind.
If you’re like me, you’re probably wondering what an actual component Sass files look like. Here’s a simplified example from ./components/_cookie-banner.html:
.gist table { margin-bottom: 0; }
With this setup, the rule sets and such inside “global” end up in d.min.css
without any sort of @media
wrapper and the rule sets in “full” end up in a.min.scss
.
I find this approach to modularizing my CSS to be very clean and clear. It also aligns well with how I think about CSS, but it may not be the right approach for you. I’d love to hear about your approach to CSS organization in the comments.
Mobile-first or desktop-first?
There are a number of ways to make a design responsive. With existing sites, it’s often easy to bolt-on responsive-ness by adding in media queries using max-width
values. You’ll often hear this approach called “desktop first” or “large screen first”. The problem with this approach is that it views the small screen as less than the “full” experience. It also requires that users of small screen devices download all of the CSS for the desktop experience even though they will use a small subset of the rule sets they receive. It also makes your CSS files larger than they need to be.
Consider this simple example: two div
elements that we want to stack on small screens and place side-by-side on larger screens. Simple enough. Taking the large screen first approach, I might write something like this:
.gist table { margin-bottom: 0; }
The first two rule sets are aimed at the large screen view and float the two elements next to one another. Then I have the media query, which removes the floats and resets the widths of the two elements. Contrast that with the small screen first approach:
.gist table { margin-bottom: 0; }
In this instance, the floating behavior and width assignment is isolated to a larger screen view and we save a few bits by not having to undo styles set earlier in the document. This approach is additive, starting with the default layout behaviors and only tweaking them when it’s necessary rather than starting by defining everything and then having to undo that work. In other words, approaching CSS by thinking of the small screen first progressively enhances the design as you have more screen real estate to work with.
I may have gone a little overboard, but I challenged myself to make the 10k Apart design work well from around 240px wide up to around 960px wide (that’s where the content stops—the header and footer go full-width). Not only did I accomplish that using the small screen first approach, I was able to do it within the 10kB limit.
It’s worth noting that there were instances where I needed to isolate certain styles to one specific breakpoint rather than letting them apply from there on up. Sure I could undo those styles in the next breakpoint, but undoing styles can lead to code bloat as we covered a moment ago. In those instances, I combined media queries to “sandbox” the rules. Here’s an excerpt from the gallery component that demonstrates this:
.gist table { margin-bottom: 0; }
What this code does (in concert with a few other rule sets) is set up a 2-column grid for the gallery within the “small” breakpoint. There is a set of float-related rules for browsers lacking flexbox support. I also provided two width
options for this scenario: one percentage-based value for browsers that don’t support calc()
and a more precise one for those that do. At the end of the first rule set, I have the flexbox configuration. The cool thing about flexbox is that browsers that implement it will ignore away float-related values. They’ll also ignore width if your set flex-basis
(which I have).
The second media query is my “sandbox”. I wanted the basic float setup to bubble up to larger screen sizes, but I did not want the same thing to happen with the clearing and margin removal, since those only applied in the 2-column grid scenario. By bookending the media query with a min-width
equal to the current media query and a max-width
equal to 1px less than the next one, I was able to isolate that rule set.
This isn’t a technique you’re likely to need very often, but it’s a handy one to have around as it keeps you from having to undo styles later on.
Can I improve the experience for high-contrast experiences?
While working on the design, my colleague David Storey pointed out that I should consider folks who use Windows’ High Contrast Mode. Despite being aware of the feature, I hadn’t thought much about it. David provided me with a couple of screenshots and they weren’t horrible, but I wouldn’t call the experience great either.
Interestingly, there is a way to target Edge and IE when Windows is using a high-contrast theme: -ms-high-contrast
. You can use this feature in a media query just like you’d use min-width
:
.gist table { margin-bottom: 0; }
You can even apply specific rules within certain pre-defined themes:
.gist table { margin-bottom: 0; }
I used these vendor-specific media queries sparingly, tweaking colors here and there and turning off backgrounds and excess ornamentation. In the end, I’m pretty happy with the result.
What did we learn?
I covered a lot of territory in this piece, so here’s a cheat sheet covering the highlights for you:
- Consider “no style” and “default style” scenarios — Remember that you don’t control where and how your content makes it to your users;
- Understand how fault tolerance applies to CSS — Understanding how CSS works will help you write more flexible styles that enable more folks to use your sites;
- Provide fallbacks — It’s exciting to use newer CSS features like flexbox and
calc()
, but make sure folks without those features aren’t left behind; - Take pity on older browsers — Chances are if they can’t handle media queries, they’ll probably have a problem with complex layouts… don’t even send them that stuff;
- Organize yourself — You don’t have to use the tools or approach I do, but keeping your code well-organized will pay dividends when it comes to writing cleaner, leaner CSS;
- Start designing the small screen first — Use default styles to your advantage and avoid un-doing previous declarations in order to keep your CSS lean;
- Sandbox rules when necessary – Starting small screen first means your design is cumulative… if you don’t want a rule set’s effects to spread beyond the immediate media query, sandbox it with a
min-
and amax-
value. - Enhance the experience wherever possible – Add styles for users that rely on high contrast rendering modes, print, and so on.
Where to next?
With the design applied, it was time to add some interactivity and improve the experience with JavaScript. I was getting close to the 10kB limit at this point, so it was time to get crafty. Stay tuned!
— Aaron Gustafson, Web Standards Advocate
Leave a Reply