The Web Content Accessibility Guidelines (WCAG), an organization that defines standards for web content accessibility, does not specify a minimum font size for the web.
But we know there’s such a thing as text that is too small to be legible, just as text that can be too large to consume. So, how can we make sure our font sizes are accessible? What sort of best practices can we rely on to make for an accessible reading experience?
The answer: it’s not up to us. It Depends™. We’ll get into some specific a bit later but, for now, let’s explore the WCAG requirements for fonts.
Sizing, contrast, and 300 alphabets
First, resizing text. We want to provide users with low vision a way to choose how fonts are displayed. Not in a crazy way. More like the ability to increase the size by 200% while maintaining readability and avoiding content collisions and overlaps.
Secondly, there’s contrast. This is why I said “it depends” on what makes an accessible font size. Text has to follow a contrast ratio of at least 4.5:1, with the exception of a large-scale text that should have a contrast ratio of at least 3:1. You can use tools like WebAIM’s Contrast Checker to ensure your text meets the guidelines. Stacy Arrelano’s deep dive on color contrast provides an excellent explanation of how contrast ratios are calculated.
There are around 300 alphabets in the world. Some characters are simple and readable in smaller sizes, others are incredibly complex and would lose vital details at the same size. That’s why specs cannot define a font size that meets the specification for contrast ratios.
And when we talk about “text” and “large text” sizes, we’re referring to what the spec calls “the minimum large print size used for those languages and the next larger standard large print size.” To meet AAA criteria using Roman text, for example, “large” is 18 points. Since we live in a world with different screen densities, specs measure sizes in points, not pixels, and in some displays, 18pt is equal to 24px. For other fonts, like CJK (Chinese, Japanese, Korean) or Arabic languages, the actual size in pixel would be different. Here’s the word “Hello” compared next to three other languages:
Hello สวัสดี مرحبا 你好
In short, WCAG specifies contrast instead of size.
Here is the good news: a browser’s default styles are accessible and we can leverage them to build an accessible font size strategy. Let’s see how.
Think about proportions, not size
The browser first loads its default styles (also known as the “User Agent stylesheet”), then those cascade to the author’s styles (the ones we define), and they both cascade and get overwritten by the user’s styles.
As Adrian Sandu mentions in his article about rem CSS units:
[…] there is an empirical study run by the people behind the Internet Archive showing that there is a significant amount of users who change their default font size in the browser settings.
We don’t fully control the font-family
property, either. The content might be translated, the custom font family might fail to load, or it might even be changed. For example, OpenDyslexic is a typeface created to increase readability for readers with dyslexia. In some situations, we may even explicitly allow switching between a limited set of fonts.
Therefore, when defining fonts, we have to avoid hindering the ability of a user or a device to change our styles and let go of assumptions: we just don’t know where our content is going to land and we can’t be sure about the exact size, language, or font that’s used to display content.
But there is one thing that we can control: proportions.
By using CSS relative units, we can set our content to be proportional to whatever the environment tells it to be. WCAG recommends using em units to define font size. There are several publications discussing the benefits of using ems and rems and it’s beyond the scope of this article. What I’d say here is to use rems and ems for everything, even for other properties besides font-size (with the exception of borders, where I use pixels).
Avoid setting a base font-size
My recommendation is to avoid setting font-size
on the :root
, <html>
or <body>
elements in favor of letting the browser’s default size serve as a baseline from where we can cascade our own styles. Since this default is accessible, the content will also be accessible. The WACAG 2.2 working draft states that:
When using text without specifying the font size, the smallest font size used on major browsers for unspecified text would be a reasonable size to assume for the font.
Of course, there is an exception to the rule. When using an intricate, thin, or super short x-height font, for example, you might consider bumping up the font size base to get the correct contrast. Remember that the spec defines contrast, not size:
Fonts with extraordinarily thin strokes or unusual features and characteristics that reduce the familiarity of their letter forms are harder to read, especially at lower contrast levels.
In the same manner, a user might change the base font size to fit their needs. A person with low vision would want to choose a larger size, while someone with an excellent vision can go smaller to gain real estate on their screens.
It’s all about proportions: we define how much larger or smaller parts of the content should be by leveraging the default base to set the main text size.
:root {
/* Do not set a font-size on a :root, body nor html level */
/* Let your main text size be decided by the browser or the user settings */
}
.small {
font-size: .8rem;
}
.large {
font-size: 2rem;
}
What about headings?
Since headings create a document outline that helps screenreaders navigate a document, we aren’t defining type selectors for heading sizes. Heading order is a WCAG criteria: the heading elements should be organized in descending order without skipping a level, meaning that an h4
should come right after an h3
.
Sometimes resetting the font sizing of all headings to 1rem
is a good strategy to make the separation of the visual treatment from the meaning mandatory.
How can we work with pixels?
Both rem
or em
sizing is relative to something else. For example, rem
calculates size relative to the <html>
element, where em
is calculated by the sizing of its own element. It can be confusing, particularly since many of us came up working exclusively in pixels.
So, how can we still think in pixels but implement relative units?
More often than not, a typographical hierarchy is designed in pixels. Since we know about user agent stylesheets and that all major browsers have a default font size of 16px
, we can set that size for the main text and calculate the rest proportionately with rem
units.
Browser Name | Base Font Size |
---|---|
Chrome v80.0 | 16px |
FireFox v74.0 | 16px |
Safari v13.0.4 | 16px |
Edge v80.0 (Chromium based) | 16px |
Android (Samsung, Chrome, Firefox) | 16px |
Safari iOS | 16px |
Kindle Touch | 26px (renders as 16px since it’s a high density screen) |
Now let’s explore three methods for using relative sizing in CSS by converting those pixels to rem units.
Method 1: The 62.5% rule
In order to seamlessly convert pixels to rem, we can set the root sizing to 62.5%
. That means 1rem
equals 10px
:
:root {
font-size: 62.5%; /* (62.5/100) * 16px = 10px */
--font-size--small: 1.4rem; /* 14px */
--font-size--default: 1.6rem; /* 16px */
--font-size--large: 2.4rem; /* 24px */
}
.font-size--small {
font-size: var(--font-size--small);
}
.font-size--default {
font-size: var(--font-size--default);
}
.font-size--large {
font-size: var(--font-size--large);
}
calc()
function
Method 2: Using the We can also calculate sizes with CSS calc()
by dividing the pixel value by the font base we assume most browsers have:
:root {
--font-size--small: calc((14/16) * 1rem); /* 14px */
--font-size--default: calc((16/16) * 1rem); /* 16px */
--font-size--large: calc((24/16) * 1rem); /* 24px */
}
.font-size--small {
font-size: var(--font-size--small);
}
.font-size--default {
font-size: var(--font-size--default);
}
.font-size--large {
font-size: var(--font-size--large);
}
Method 3: Using a “pixel-to-rem” function
Similar to calc()
, we can leverage a preprocessor to create a “pixel-to-rem” function. There are implementations of this in many flavors, including this Sass mixin and styled-components polish.
:root {
--font-size--small: prem(14); /* 14px */
--font-size--default: prem(16); /* 16px */
--font-size--large: prem(24); /* 24px */
}
.font-size--small {
font-size: var(--font-size--small);
}
.font-size--default {
font-size: var(--font-size--default);
}
.font-size--large {
font-size: var(--font-size--large);
}
It’s even possible to create a “pixel-to-rem” function with vanilla CSS:
Embrace a diverse web!
The bottom line is this: we don’t have control over how content is consumed. Users have personal browser settings, the ability to zoom in and out, and various other ways to customize their reading experience. But we do have best CSS best practices we can use to maintain a good user experience alongside those preferences:
- Work with proportions instead of explicit sizes.
- Rely on default browser font sizes instead of setting it on the
:root
,<html>
or<body>
. - Use
rem
units to help scale content with a user’s personal preferences. - Avoid making assumptions and let the environment decide how your content is being consumed.
Special thanks to Franco Correa for all the help writing this post.
Mixing rem font-styling with px-based layouts and media queries is tricky, they don’t scale together. The text that looked fine at 1rem size inside a 500px-wide box will look way too large, the container didn’t change at all.
In our experience users are more likely to set a default zoom level for their browsers (or just zoom on the page) instead of setting a default don’t-size (which were about 2% of users).
Rems were great workaround when browser zoom behavior wasn’t consistent. They aren’t worth it anymore.
The biggest visual text accessibility wins can found in the guides from WCAG.
Meaningful text with ARIA labels and states shouldn’t be ignored in this discussion as well, unless we’re just talking about visual a11y.
Would you recommend using a rem-based layout then?
I would recommend all-or-none, not mixing absolute and relative values.
Thank you for gathering this info. I am also a proponent of never setting a base font size, deferring to user preferences instead.
To build a bit on some of the advice here, when setting font sizes:
be careful with any sizing based on viewport units (
vw
orvh
);avoid changing font size across viewport size media queries;
line length matters, so be careful about letting lines of text get too long;
until you have a good test process, avoid
min()
,max()
, andclamp()
;be aware that type set in
px
will be unaffected when users change the font size in the browser settings, so maybe avoidpx
completely.I have examples of how these can be problematic in my post Responsive Type and Zoom.
A quick note on OpenDyslexic from a 2013 study with 48 participants (there are other studies that support this):
That being said, variable fonts could be helpful to those with dyslexia (so please experiment!).
Can you elaborate on “avoid changing font size across viewport size media queries”?
D, if your type changes size with viewport size changes, when a user zooms the type will not scale consistently if that zoom triggers a media query. See the first link in my comment for examples.
I increase the root font size on desktop via a media query.
Is this a problem?
I threw that MQ on a pen and maximized my window to 1,920 × 1,080. As I zoomed up to 150% the text got bigger. When I hit 175% the text got smaller than it was at 150% zoom. At 200% zoom the text size matched the 150% zoom text size. To get the text to 200% the size of the original, I had to zoom 250%. So yeah, that is a problem.
I just wrote the same and oversaw your comment. It works for me, and is also recommended here: https://betterwebtype.com/articles/2019/06/16/5-keys-to-accessible-web-typography/
I forgot to mention that you should use em for media query breakpoints. There they always refer to the base font size of the browser, not to font size you might set to the root element, otherwise you could get infinite loops.
What I’ve taken to doing recently is specifying font sizes in both
px
andrem
(assuming a base font-size of16px
):This duplication does add a little to CSS file size, but it allows you to design/code in pixels (including an easy reference to what that px size was), quickly convert to
rem
(perhaps using a pre-processor if you want), and gives a fallback in case a browser doesn’t supportrem
(admittedly all mainstream browsers do, but I’m always concerned above some lesser used device — especially an AT device — may not).I like to scale the whole site up on large screens by changing the root font size, which works fine if you use rems and Ems everywhere, including your media queries. Just make sure to not use pixels, but percent:
html {
font-size: 110%;
}
This will take the font size of the user into account, so it may be 110% of 16px, or of whatever the user chose.
Hi Andrés, thanks for sharing :)
A small note
By default, iOS will not size relative lengths based on user settings unless you use system fonts that supports dynamic type behavior.
To enable this you can define -apple-system-body as the root font (I use HTML as it has lower specificity than :root)