DEV Community

Nate Watson
Nate Watson

Posted on

CSS tweaks for more accessible text

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.

Increased font size, overridden font families and automatically translated content on a webpage. In all cases the text has enough room, remains readable and maintains bold and italics

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.

Setting the browser font size to "Very large" does not change the size of text that has its font size set using px units - the page looks exactly the same as when the browser font size is set to "Medium"

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;
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

(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:

With default browser settings a pixel-based line height looks perfectly fine. Increasing the browser font size setting causes the size of the letters to increase, but the vertical space between the lines remains unchanged, causing the lines to overlap each other.

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
    }
}
Enter fullscreen mode Exit fullscreen mode

...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;
}
Enter fullscreen mode Exit fullscreen mode

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:

At default browser font sizes all text on the page fits inside its visual containers - a card sit nicely around the main text and a button has text sitting inside the button's outline. Increasing the font size results in text spilling out the bottom of the card and the button text overlapping the button's borders.

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>
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

After applying this approach to the button and card in the earlier screenshot, we end up with something much nicer:

At both default and increased font sizes all text on the page now fits inside its visual containers. At the larger font size the button text wraps over three lines and the container/outline has grown to accommodate this

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');
} 
Enter fullscreen mode Exit fullscreen mode

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');
} 
Enter fullscreen mode Exit fullscreen mode

Which might be used something like:

.bold {
    font-family: FiraBold, Arial, sans-serif;
}
Enter fullscreen mode Exit fullscreen mode

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.

Webpage shown with bold text adding emphasis to a warning and italics being used on a Latin word. With fonts overridden the bold and italics text look no different to the text around them.

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');
} 
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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:

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:

Top comments (0)