Nate Watson
Posted on May 8, 2021
One of the magical things about the web is that there are numerous ways people will access the sites we build. Whether it's on a phone, in a "reader mode", with images disabled, using a screen-reader or without a mouse, we can (and should) write code that ensures our sites work beautifully for all these different configurations.
With text particularly, there are a number of browser options that people might use to make content more accessible to them. This ranges from increasing font sizes for more legible text, swapping to a different font completely, or even swapping the text content itself via a translation tool.
Code that worsens support for these features is unfortunately common across the web, but goes unnoticed when testing against common accessibility tools such as screen-readers or automated auditors. Fortunately some small changes to how we write CSS can easily bring support for various text-related settings, and once you set things up, it'll quickly become second-nature on all your projects. So let's dive in and have a look at some guidelines for writing accessible CSS!
Tip 1: Use relative font size for nice font scaling
Font size is one of the most prominent accessibility settings in most browsers, and a good number of users set it something other than the default. Stats gathered by the Internet Archive in 2018 showed that just over 3% of their website visitors were using a non-default font size setting, which is a pretty decent number of people!
What might be more surprising is that setting your CSS font sizes using pixels (i.e. with the px
unit) causes that setting to do absolutely nothing. The problem with pixel units is that they tell the browser exactly how big something should be without allowing for any other factors (like a user having "very large" fonts selected in their browser settings) to come into play.
This is where relative font size units really shine. Rather than telling the browser "make my text exactly this big", they instead describe how big the text should be relative to some other element on the page. In the case of the em
unit, it's relative to the parent element's font size, and for the rem
unit it's relative to the font size of page's top-most, or root, element (almost always the <html>
element).
By default, that root element's font size comes directly from the browser's font size setting. A browser with its fonts set to "very large" might give pages a root font-size of 24px by default, while one with "very small" might result in a default of 10px.
A lot of pages will override that default by setting a pixel-based font size on the HTML element, for example:
/** Don't do this **/
html {
font-size: 18px;
}
This is often framed as providing a "sensible default", but it prevents the rest of the page from knowing the user's preferred font size. If you're approaching your sites from an accessible perspective, the user preference is almost always the most sensible default.
In short, avoid setting a pixel-based font-size on the HTML (or :root
) element.
Setting font sizes
Now that we've got our root element respecting the browser font size, we need to extend that to all the other other elements on our site. Fortunately with a little bit of math (or an SCSS mixin) we can do this fairly effortlessly in a way that will be invisible to most users.
The key thing to know is that the default root font size is 16px in most browsers. That means text with its font size set to 1rem
will display as 16px (1 times the root font size), 2rem
will be 32px, 0.5rem
will be 8px, and so on, for users who haven't changed the font size setting. With a little bit of maths, you can convert all your existing pixel sizes to an rem-equivalent:
sizeInRems = sizeInPixels / 16
Which should let you update any existing font size declarations to be root-relative:
- font-size: 16px;
+ font-size: 1rem;
- font-size: 24px;
+ font-size: 1.5rem;
// and so on
(If you're using SCSS, read on for a mixin that will do some of this for you).
By not overriding the root font size and switching to relative sizes your text will now scale to match each user's preference - if they've left the setting untouched, everything will look exactly as it did before. But if they've chosen to change the setting - to something larger or something smaller - your website's text will now scale to reflect that preference:
A note on line-height
It's important to also switch your line-height values to proportional units, otherwise the values won't scale to match any changes to the calculated font size. For people who increase the root font size, this can cause overlapping lines of text that are pretty difficult to decipher:
In fact, this is something you should do even if your font-size values are still using pixel units. In addition to settings that change the root font size, most browsers have an option to force a minimum font size (usually found under an "advanced font options" menu or similar). This will override any font-size values that are too small, but won't affect any fixed-unit line-heights, resulting in the same overlapping shown above.
The best way to fix this is to switch to unitless line-height values. Line-heights are one of the few CSS properties where it's valid to leave off the units completely (so no px
, no em
, no %
), with unitless values being interpreted as "the unitless number multiplied by the element's own font size".
A mixin to help
If you're like me, pixel values are how you and the people you work with communicate - design tools usually give values in pixels, the browser inspector shows computed sizes in pixels and they're a unit anyone who stares at a screen for eight hours a day probably has baked into their mind. Thinking in terms of rems
can feel less instinctive, and having to pull out a calculator to convert units is no fun.
Fortunately, you can write a nice little SCSS mixin (or equivalent in your favourite CSS pre-processor) to do all the hard work for you. The one I use looks something like:
@mixin font-size($font-size, $line-height: null) {
font-size: 1rem * $font-size / 16px; // Assuming 16px as the "standard" root font size
@if $line-height {
line-height: $line-height / $font-size; // Gives a unitless value
}
}
...and then you can write font sizes using pixels (specifically the pixel size you want to be used when the browser is set to its default font sizes), and the outputted CSS will use the appropriate relative units:
// Input:
.paragraph {
@include font-size(24px, 30px);
}
// Output:
.paragraph {
font-size: 1.5rem;
line-height: 1.25;
}
Tip 2: Avoid hardcoded element dimensions - give text space to grow
If you browse the web with increased font sizes, it's common to encounter situations where text is too big for its container:
In fact this isn't just an issue when increasing the font size. Anything that changes the amount of text or dimensions of the letters can make this happen, which includes things like translating a page, using a different typeface (whether intentionally or due to a webfont not loading) or changing the letter spacing.
The source of issues like this is hardcoding the dimensions of elements that contain text:
.Button {
font-size: 1rem;
height: 50px;
width: 250px;
}
// html: <button class="Button">Learn more about the rubber plant</button>
In this case our button is going to look bad if the text ever becomes taller than 50px or wider than 250.
We're essentially telling the element to not shrink or grow, so anything that doesn't fit will simply overflow out of the element - as shown in the earlier screenshot.
Often hardcoded dimensions like this can be removed completely or, if you need to prevent the element becoming too small, replaced with min-width/min-height alternatives. If the dimensions play a part in vertically centering the text content, padding is often a suitable alternative:
.Button {
font-size: 1rem;
min-width: 250px;
padding: 10px 20px;
}
After applying this approach to the button and card in the earlier screenshot, we end up with something much nicer:
Tip 3: Specify web font weights and styles
Just as the font sizes you specify might be overridden, the font-families named in your CSS may not end up being displayed on the page. There are a number of situations where this will happen:
- A webfont fails to load, causing a fallback to be used,
- Webfonts get blocked completely (for speed, security, to save data, etc.),
- A user finds a particular typeface easier to read, so uses an extension to force it to be used everywhere,
- The page is automatically translated into a language that uses characters not available in the webfont.
The first step to handling these situations gracefully is something most people already do: specifying fallback fonts, or a "font stack":
font-family: Fira, Arial, sans-serif;
This tells the browser to use Fira if it's available, otherwise use Arial, and if that's not available, use the browser's default sans-serif font family. The fallback fonts should be similar to the preferred font, so the page will still look reasonable regardless of which one ends up being used.
If you're using webfonts there's one more thing you should do to really make your text great, regardless of the font family that ends up being used. Attributes such as font weight and italics often carry additionally (and intentional) meaning, or help differentiate between pieces of text on the page. That makes it important to preserve them no matter which font family gets used. To do this, we need to look at our @font-face
declarations. These might look something like:
@font-face {
font-family: 'Fira';
src: url('fira.woff') format('woff'),
url('fira.ttf') format('truetype');
}
Different font weights (bold, light, etc.) and styles (e.g. italics) have to be loaded from separate font files, and therefore require separate @font-face declarations. It's common to see these each given unique names, like:
@font-face {
font-family: 'Fira';
src: url('fira.woff') format('woff'),
url('fira.ttf') format('truetype');
}
@font-face {
font-family: 'FiraBold';
src: url('fira-bold.woff') format('woff'),
url('fira-bold.ttf') format('truetype');
}
Which might be used something like:
.bold {
font-family: FiraBold, Arial, sans-serif;
}
The issue with this approach is that the browser doesn't really know that our 'FiraBold' font is bold - if it fails to load (or is swapped out for a different font), the fallback font will display in a regular weight, no different to the text around it.
To fix this, we need to be explicit about the weights and styles when declaring and when using our fonts. Rather than giving each variation a unique font-family
value, we should use the same font-family value across all the variations, and explicitly specify the weight and style of each. For the above example this would look like:
@font-face {
font-family: 'Fira';
font-weight: 400;
font-style: normal;
src: url('fira.woff') format('woff'),
url('fira.ttf') format('truetype');
}
@font-face {
font-family: 'Fira';
font-weight: 700;
font-style: normal;
src: url('fira-bold.woff') format('woff'),
url('fira-bold.ttf') format('truetype');
}
The final step is to update any CSS where you're using the font variations, so that font-weight
and font-style
CSS properties are used to select variations of the same font-family:
.bold {
- font-family: FiraBold, Arial, sans-serif;
+ font-family: Fira, Arial, sans-serif;
+ font-weight: 700;
}
Now if the fonts on your page are overridden, fail to load, or are unable to display some text, the weight and style of the text will be preserved:
Bonus tip: Don't use icon fonts
Icons fonts are easy to set-up and use, but they have a number of issues, including breaking completely for users who override fonts (or any other situation where webfonts aren't loaded). That's not to say you shouldn't use icons though (far from it - well used icons are great for accessibility and usability), but approaches that don't involve fonts are the best way to go.
People much smarter than me have written various articles on the topic, so I'm going to delegate to them for this one:
- Seriously, Don’t Use Icon Fonts by Tyler Sticka - an older article, but still entirely relevant.
- Accessible SVG Icons with Inline Sprites by Marco Hengstenberg
- Accessible SVG Icons by Chris Coyier
And we're done!
Supporting numerous different browser configurations can seem like a daunting task, but hopefully these tips have shown that some small changes to how you write CSS can have a big impact. If you're starting a fresh project, try to make these patterns the default! You'll quickly see that they're no extra effort, and visitors to your site will love you for it.
Further reading
These are some pages I bookmarked while writing this post - you might find the useful too:
- Accessible Font Sizing, Explained - Andrés Galante
- People don't change the default 16px font size in their browser (You wish!) - Nicolas Hoizey
- Responsive Type and Zoom - Adrian Roselli
- Writing CSS with Accessibility in Mind - Manuel Matuzovic
- How To Set Weights And Styles With The @font-face Declaration - Laura Franz
- Death to icon fonts - talk by Seren Davies at EpicFEL 2015
- Can Fonts Really Help Those With Dyslexia? - Madeleine Morley
Posted on May 8, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.