Tag: Font

Using CSS Custom Properties to Adjust Variable Font Weights in Dark Mode

Black isn’t always slimming.

When recently testing a dark mode option for one of my sites, I experienced first-hand the issue that Robin Rendle addresses in this article. All of my page text — headings and body copy — appeared to bulk up when I switched to dark mode. And it didn’t matter what fonts I used or which browsers I tried. The same thing happened with all of them.

For example, here’s what happens with Adobe’s Source Sans Pro in Chrome for Windows:

See those blurry edges when we switch to dark mode?

It’s not an illusion. The light characters really are heavier against dark backgrounds. We can zoom in to see better:

The characters really are thicker in dark mode!

And it becomes really obvious when we invert the dark mode portions of those images:

We can really see the difference when putting the characters side-by-side on the same white background.
We can really see the difference when putting the characters side-by-side on the same white background.

One solution

Since variable fonts enjoy wide browser support, we can use them to help us address this issue. The three panels below demonstrate a solution we’ll be working toward:

The top shows us some light text on a dark background. The middle panel shows what happens in dark mode without changing any font weight settings. And the bottom panel demonstrates dark mode text that we’ve thinned out a bit. That third panel is adjusted to match the weight of its light counterpart, which is what we’re trying to accomplish here.

Here’s how we can get this improved effect:

  1. Reduce font-weight properties in dark mode via one of the following methods:
    1. Manually changing each font-weight assignment directly in a dark mode media query.
    2. Creating a single --font-weight-multiplier custom property that changes its value in dark mode, and by which we can then multiply by each element’s default font-weight value.
    3. Same thing, but instead of calculating each element’s font-weight property individually, we take advantage of CSS variable scoping and the universal selector (*) to apply our multiplier calculation everywhere at once.
  2. Adjust a variable font’s grade (“GRAD”) axis. Not all variable fonts support this specific feature, but Roboto Flex, does. Altering this axis value changes the font’s apparent weight, without affecting the width of the letters.
  3. Adjust a variable font’s darkmode ("DRKM") axis. Dalton Maag’s aptly-named Darkmode, with its eponymous darkmode axis, is uniquely suited for this. As with the Roboto Flex’s grade axis, adjusting Darkmode’s darkmode axis changes the font’s apparent weight. But while the grade axis requires some fine-tuning of values, the darkmode axis is simply switched on (thinner) or off (regular).

The techniques in the first group work for most variable fonts. The solution Robin uses in his article is actually the very first item in the group. I’ll expand on the second and third items in the group by introducing custom properties that help us automatically adjust font weights in dark mode.

The second and third groups involve less common font-variation-settings axes. Though these strategies apply to fewer typefaces, they may be preferable when available. The trick is knowing what a variable font supports before choosing it.

I’ve made a demonstration page including all the strategies covered in this article. You can see what some different variable fonts look like in light mode, in dark mode with no adjustment, and in dark mode with our solutions for thinning out characters.

In addition to the strategies listed above, there’s always one more option: don’t do anything! If you think your fonts look good enough in light and dark mode, or you don’t have the bandwidth right now to wrestle with reflow, element resizing, browser/display inconsistencies, and extra CSS to maintain, then you may not have to change a thing. Focus on the rest of your site and leave yourself open to the possibility of revisiting this topic later.

Strategy 1: Reducing the font-weight value

Most variable text fonts have a weight axis, which lets us assign any specific font-weight value within the weight range available to that font (e.g. 0-1000, 300-800, etc.). Each technique in this strategy takes advantage of this fine control over the weight axis to reduce font-weight values in dark mode. (The need for such font-weight precision is also what renders most non-variable fonts unsuitable for this solution.)

If you’re using variable fonts you have locally, you can check their axes and value ranges at Wakamai Fondue:

At Wakamai Fondue, you can view any local font’s variable axes and ranges.

Keep in mind that, if you’re using the @font-face rule to load fonts, you should set a font-weight range for each of them at the same time:

@font-face {   src: url('Highgate.woff2') format('woff2-variations');   font-family: 'Highgate';   font-weight: 100 900; }

If you neglect this step, some variable fonts may not properly reflect specific font-weight values in current Chromium browsers.

Dalton Maag Highgate’s font-weight set to 800 in Chrome without (left) and with (right) a font-weight range specified in the @font-face rule.

The basic solution: Manually entering each weight

Here’s the technique most of us may reach for. We create a dark mode media query in which we enter some font-weight values that are a bit lower than their defaults.

/* Default (light mode) CSS */  body {   font-weight: 400; }  strong, b, th, h1, h2, h3, h4, h5, h6 {   font-weight: 700; }  /* Dark mode CSS */ @media (prefers-color-scheme: dark) {   body {     font-weight: 350;   }    strong, b, th, h1, h2, h3, h4, h5, h6 {     font-weight: 600;   } }

It works, and it’s no problem to maintain — so long as we’re not planning on adding or editing any other weights at our site! But if we do start incorporating more weights, it can get unwieldy fast. Remember to enter each selector/property combo both outside and inside the prefers-color-scheme media query. We’ll have to do some manual calculations (or guesswork) to determine the dark mode property values for each element.

Creating a weight multiplier custom property and using it in a calculation when setting an element’s weight

I generally try to adhere to Mike Riethmuller’s credo that “media queries are only used to change the value of custom properties.” And that’s the improvement we make in this solution. Instead of having to enter font weights for all our elements in and out of dark mode, the only thing we’re putting in our media query is a --font-weight-multiplier custom property:

@media (prefers-color-scheme: dark) {   :root {     --font-weight-multiplier: .85;   } }

Then, for all our font-weight properties throughout the stylesheet, we’ll multiply the variable’s value by our preferred default weight value for each element — thus lowering the font weight by 15% in dark mode. If we’re not in dark mode, we’ll multiply the default weight by 1, meaning it doesn’t change at all.

Here’s what I mean. Normally, we’d use this to set a body font weight of 400:

body {   font-weight: 400; }

For this solution, we use this:

body {   font-weight: calc(400 * var(--font-weight-multiplier, 1)); }

In the var() function, notice that our variable has a fallback value of 1. Because --font-weight-multiplier is only set in dark mode, this fallback value will be used the rest of the time. So, by default, our font weight for body text stays at 400 (400*1). But in dark mode, the weight decreases to 340 (400*.85).

We’ll also do this with bold elements:

strong, b, th, h1, h2, h3, h4, h5, h6 {   font-weight: calc(700 * var(--font-weight-multiplier, 1)); }

These weights will decrease from 700 to 595 (700*.85) in dark mode.

And we can use the same technique for any other elements where we want to set the font-weight to something other than 400 by default.

I’m using a value of .85 for --font-weight-multiplier, because I’ve found that to be a good general value for most fonts (like Adobe Source Sans Pro, the free typeface I use in most of this article’s demos). But feel free to play around with that number.

Here’s how this looks put together:

/* DARK-MODE-SPECIFIC CUSTOM PROPERTIES */ @media (prefers-color-scheme: dark) {   :root {     --font-weight-multiplier: .85;   } }  /* DEFAULT CSS STYLES... */ body {   font-weight: calc(400 * var(--font-weight-multiplier, 1)); }  strong, b, th, h1, h2, h3, h4, h5, h6 {   font-weight: calc(700 * var(--font-weight-multiplier, 1)); }

Creating a weight multiplier variable and automatically calculating and applying it to all elements at once.

When using many CSS custom properties, I think many of us stick to a “set as needed and manually apply everywhere” approach. That’s what the previous solution does. We set our custom property value in the :root (and/or use a fallback value), set it again in a media query, then apply it with calc() and var() functions throughout our stylesheet each time we assign a font-weight value.

The code might look something like this:

h1 {   font-weight: calc(800 * var(--font-weight-multiplier, 1); }  summary {   font-weight: calc(600 * var(--font-weight-multiplier, 1); }

But when we use this technique for various elements, you can see we have to do these three things every time we assign font-weight values:

  • Include the calc() function
  • Include the var() function
  • Remember the --font-weight-multiplier custom property’s name and default value

Instead, I’ve recently started inverting this approach for certain tasks, taking advantage of CSS variable scope with a “set everywhere and apply once” method. For this technique, I replace every font-weight property in the stylesheet with a --font-weight variable, keeping the name the same except for the dashes, for simplicity’s sake. I then set this value to the default weight for that particular selector (e.g. 400 for body text). Neither calc() nor var() is needed — yet. This is how we set everywhere.

Then we apply once, with a lone font-weight property in our stylesheet that sets every text element’s weight via the universal selector. Modifying our snippet above, we’d now have this:

h1 {   --font-weight: 800; }  summary {   --font-weight: 600; }  * {   font-weight: calc(var(--font-weight, 400) * var(--font-weight-multiplier, 1); }

The calc() function multiplies each of our --font-weight custom properties by our multiplier variable, and the font-weight property then applies the value to its appropriate element.

It’s unnecessary to use only a single var() for each custom property in the stylesheet. But I often like doing so when performing calculations and/or using a helper variable, as we do here. That said, while this is certainly the cleverest technique for adjusting font weights, that doesn’t mean it’s the best technique for all projects. There is at least one serious caveat.

The primary advantage of using the universal selector technique — that it applies to everything — also introduces its chief risk. There may be some elements that we don’t want thinned out! For example, if our form elements retain dark text on light backgrounds in dark mode, they may still get steamrolled by the universal selector.

There are ways to mitigate this risk. We could replace * with a long selector string containing a list of only elements to thin out (having them opt-in to the calculation). Or we could hard-code font weights for the elements that we don’t want affected (opt-out):

* {   font-weight: calc(var(--font-weight, 400) * var(--font-weight-multiplier, 1)); }  button, input, select, textarea {   font-weight: 400; }

Such fixes may ultimately make code just as complicated as the previous technique. So, you’ll have to gauge which is appropriate for your project. If you still have concerns over performance, code complexity, or think this technique might introduce undesired (even unpredictable) results, the previous technique might be safest.

The final code:

/* DEFAULT CUSTOM PROPERTIES */ :root {   --font-weight: 400;   --font-weight-multiplier: 1; } strong, b, th, h1, h2, h3, h4, h5, h6 {   --font-weight: 700; }  /* DARK-MODE-SPECIFIC CUSTOM PROPERTIES */ @media (prefers-color-scheme: dark) {   :root {     --font-weight-multiplier: .85;   } }  /* APPLYING THE CUSTOM PROPERTIES... */ * {   font-weight: calc(var(--font-weight, 400) * var(--font-weight-multiplier, 1)); }

We’re not required to set the default --font-weight: 400 and --font-weight-multiplier: 1 custom properties in the above code, because we’ve included the fallback values in the var() functions. But as code gets more complicated, I often like assigning them in a logical place, just in case I want to find and alter them later.

A final note on this strategy: we can also apply weights with the font-variation-settings property and a "wght" axis value, instead of font-weight. If you’re using a typeface with several axes, maybe you find it more manageable to do all your font tweaking that way. I know of at least one font (Type Network’s Roboto Flex, which we’ll be using later in this article) that has 13 axes!

Here’s how to apply our solution via a font-variation-settings property:

* {   --wght: calc(var(--font-weight, 400) * var(--font-weight-multiplier, 1));   font-variation-settings: "wght" var(--wght); }

Strategy 1 Addendum: Handling letter-spacing

One side effect of lowering our type weight is that, for most non-monspaced fonts, it also narrows the characters.

Here again is what happens when we lighten Source Sans Pro with our multiplier. The top two panels below show Source Sans Pro in light and dark mode by default. And the lower panel shows the lighter version.

Adobe’s Source Sans Pro in light mode, dark mode by default, and dark mode thinned out.

With no adjustments, the characters in light mode and dark mode are the same width. But when we lower the font weight, those characters now take up less space. You may not like how this change affects your flow or element sizes (e.g. narrower buttons). And some designers think it’s a good idea to add letter spacing in dark mode, anyway. So, if you want, you can create another custom property to add some space.

Implementing a custom property for letter spacing

Just like we did with our font-weight multiplier variable, we’re going to create a letter spacing variable with a default value that gets overridden in dark mode. In our default (light mode) :root, we set our new --letter-spacing custom property to 0 for now:

:root {   /* ...other custom variables... */   --letter-spacing: 0; }

Then, in our dark mode query, we raise the value to something greater than 0. I’ve entered it as .02ch here (which combines pretty well with a --font-weight-multiplier value of .85). You could even get clever and fine-tune it with some calculations based on your font weights and/or sizes, if you like. But I’ll use this hard-coded value for now:

@media (prefers-color-scheme: dark) {   :root {     /* ...other custom variables... */     --letter-spacing: .02ch;   } }

Finally, we apply it via our universal selector (with a fallback value of 0):

* {   /* ...other property settings... */   letter-spacing: var(--letter-spacing, 0); }

Note: Though I use the ch unit in this example, using em also works, if you prefer. For Source Sans Pro, a value of .009em is about equal to .02ch.

Here’s the full code for a font weight multiplier with letter spacing:

/* DEFAULT CSS CUSTOM PROPERTIES */ :root {   --font-weight: 400;   --font-weight-multiplier: 1;   --letter-spacing: 0; }  strong, b, th, h1, h2, h3, h4, h5, h6 {   --font-weight: 700; }  /* DARK MODE CSS CUSTOM PROPERTIES */ @media (prefers-color-scheme: dark) {   :root {     /* Variables to set the dark mode bg and text colors for our demo. */     --background: #222;     --color: #fff;      /* Variables that affect font appearance in dark mode. */     --font-weight-multiplier: .85;     --letter-spacing: .02ch;   } }  /* APPLYING CSS STYLES... */ * {   font-weight: calc(var(--font-weight, 400) * var(--font-weight-multiplier, 1));   letter-spacing: var(--letter-spacing, 0); }  body {   background: var(--background, #fff);   color: var(--color, #222); } 

Fonts with constant-width characters (aka multi-plexed fonts)

In addition to monospaced fonts, there are some other typefaces specifically designed so that their individual characters take up the same amount of horizontal space, regardless of weight. For example, if an “i” occupies five horizontal pixels of space at a weight of 400, and a “w” occupies thirteen pixels at the same weight, they will still occupy five and thirteen pixels, respectively, when their weights are increased to 700.

Arrow Type’s Recursive Sans is one such typeface. The following image shows how Recursive’s characters maintain the same widths in light mode, default dark mode, and dark mode with our font weight multiplier, respectively:

The characters in Arrow Type’s Recursive maintain their widths regardless of font weight.

Multi-plexed typefaces, like Recursive, are designed so you won’t need to adjust letter spacing when changing their font weights in dark mode. Your element sizes and page flow will remain intact.

Strategy 2: Adjust a variable font’s grade axis

The grade axis ("GRAD") changes a font’s apparent weight without changing its actual font-weight value or the widths of its characters. When using fonts with this axis, you may not need our font weight multiplier variable at all.

For Type Network’s free Roboto Flex font, a grade of -1 is thinnest, 0 (default) is normal, and 1 is thickest. With this font, I start by assigning its grade axis a value of around -.75 for dark mode.

Roboto Flex in light mode, dark mode default, and dark mode with “GRAD” set to -.75
:root {   --GRAD: 0; }  @media (prefers-color-scheme: dark) {   :root {     --GRAD: -.75;   } }  body {   font-variation-settings: "GRAD" var(--GRAD, 0); }

So, adjusting the grade axis seems like the perfect solution if it’s available to you, right? Well, maybe. There are a few things to keep in mind when considering it.

First, the scale for all fonts doesn’t always go from -1 to 1. Some range from 0 to 1. At least one typeface uses percents, making 100 the default. And other fonts align the grade scale with font weights, so the range may be something like 100-900. If you want to use the grade axis in the latter case, you may have to set all your font weights everywhere to a default of 400, and then use the grade axis for all weight changes. For dark mode, you’ll then want to treat grade essentially like we do in our font weight multiplier solution — applying the multiplier to the "GRAD" axis in font-variation settings.

The second caveat is that some typefaces don’t let you grade a font to a value below its default weight. So, grade can’t lighten it at all. Apple’s San Francisco typeface (which can be tested via font-family: system-ui; on Apple devices) has both of these issues. As of macOS Catalina, San Francisco has a grade axis. It’s scaled to line up with font weights, and its minimum value is 400.

San Francisco’s grade and weight axes use the same scale, but have different ranges.

Because we can’t set the grade to a value lower than 400, we can’t lighten fonts from a default of 400 in dark mode. If we want to go lower, we’ll need to lower the weight axis value, instead.

Strategy 3: Adjusting a variable font’s darkmode axis

There’s currently only one typeface with a darkmode ("DRKM") axis at the time of this writing: Dalton Maag’s Darkmode.

The darkmode axis is essentially a grade axis without any fine-tuning. Just turn it on (1) for a thinner appearance in dark mode, and leave it off (0, the default) for normal display.

Darkmode in light mode, in dark mode with “DRKM” unset, and in dark mode with “DRKM” set to 1.
:root {   --DRKM: 0; }  @media (prefers-color-scheme: dark) {   :root {     --DRKM: 1;   } }  body {   font-variation-settings: "DRKM" var(--DRKM, 0); }

I like the Darkmode font a lot. But beware that it is a commercial license that’s required for professional use. Dalton Maag offers a trial version that can be used for “academic, speculative, or pitching purposes only.” I’m hoping this typeface is a pilot for more Dalton Maag families to get a darkmode axis, and that other font foundries will then follow suit!

Other factors to consider

We’ve covered a few big strategies for working with variable fonts in a dark mode context. But, as with most things, there are other things to consider that might sway you toward one solution or another.

Dark mode on high-resolution (“retina”) screens

On screens with higher pixel densities (e.g. most modern phones, MacBooks, iMacs, etc.), the thickening effect of dark mode is often less pronounced. Therefore, you may not want to thin the fonts on these screens out as much — if at all!

If you still want to lighten fonts a bit, you can add another media query to make the effect less severe. Depending which solution you’re using, you can raise the --font-weight-multiplier value closer to 1, raise the --GRAD value closer to 0, or disable --DRKM altogether (since it’s either on or off, with no in-between).

If you add this query, remember to place it below the original prefers-color-scheme query, or it may have no effect. Media queries don’t add CSS specificity, so their order matters!

@media (prefers-color-scheme: dark) and (-webkit-min-device-pixel-ratio: 2),         (prefers-color-scheme: dark) and (min-resolution: 192dpi) {    :root {     --font-weight-multiplier: .92;     /* Or, if you're using grade or darkmode axis instead: */     /* --GRAD: -.3; */     /* --DRKM: 0; */   } }

If you don’t want to lighten fonts at all on high density screens in dark mode, you can update your original dark mode prefers-color-scheme query to the following, to omit these screens:

@media (prefers-color-scheme: dark) and (-webkit-max-device-pixel-ratio: 1.9),         (prefers-color-scheme: dark) and (max-resolution: 191dpi) {     /* Custom properties for dark mode go here. */  }

Mixing fonts with different axes (and mixing variable fonts with non-variable fonts)

If you’re using more than one typeface on your site, you’ll need to consider what effects these adjustments may have on all of them. For example, if you’re using multiple fonts with intersecting axes, you could wind up accidentally combining the effects of multiple strategies (e.g. reducing both grade and weight simultaneously):

If your stylesheet includes solutions for several typefaces/axes, then the effect on fonts that have multiple axes (like this example’s Roboto Flex, which has both grade and weight axes) may be cumulative.

If all the fonts on your site are variable and have a grade axis with a matching scale and range (e.g. if they all range from -1 to 1), that’s the solution I’d recommend. However, you’ll have to revisit this if you plan to add other fonts later that don’t meet those criteria. Same goes for the darkmode axis, too, if it becomes more widespread.

If all your fonts are variable, but they don’t all share the same axes (e.g. grade and darkmode), then using only the --font-weight-multiplier custom property may be your safest bet.

Finally, if you’re mixing variable and non-variable fonts, know that the non-variable fonts will not change appearance with any of these solutions — with some exceptions. For example, if you’re using the font weight multiplier with the font-weight property, it is possible that some — but maybe not all — of your font weights will change enough to move to the next lower weight name.

Say your site includes a font with three weights: regular (400), semi-bold (600), and bold (700). In dark mode, your bold text may lighten up enough to display as semi-bold. But your regular font will still stay regular (as that’s the lowest weight included on the site). If you want to avoid that inconsistency, you could apply your variable font weights via font-variation-settings, and not font-weight, so your non-variable fonts aren’t affected at all. They’ll just always maintain their default weight in dark mode.

In closing

It’s always a happy coincidence when complementary technologies attain common usage near the same time. With the rise in popularity of both dark mode and variable fonts, we have the luxury of using the latter to mitigate one of the challenges of the former. Using CSS custom properties in conjunction with weight, grade, and darkmode axes, we can bring some consistency to the look of our text in both light and dark modes.

You can visit my interactive demo with the fonts and axes from this article.

The post Using CSS Custom Properties to Adjust Variable Font Weights in Dark Mode appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.


, , , , , , , ,

Accessible Font Sizing, Explained

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.

A contrast ratio of 2.39 to 1 would not pass any of the WCAG checks, a ratio of 4.65 to 1 would pass only the AA check for normal text sizes and a 10.09 to 1 contrast ratio would pass both AA and AAA WCAG checks.
Example of three color contrast measurements and their WCAG test results according to WebAIM’s contrast checker.

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.

The WCAG recommended font size for large text has greater contrast than something half the size. Notice how a larger font size lets in more of the background that sits behind the text.

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); }

Method 2: Using the calc() function

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.

The post Accessible Font Sizing, Explained appeared first on CSS-Tricks.


, , ,

Developing a robust font loading strategy for CSS-Tricks

Zach Leatherman worked closely with Chris to figure out the font loading strategy for this very website you’re reading. Zach walks us through the design in this write-up and shares techniques that can be applied to other projects.

Spoiler alert: Font loading is a complex and important part of a project.

The really interesting part of this post is the way that Zach talks about changing the design based on what’s best for the codebase — or as Harry Roberts calls it, “normalising the design.” Is a user really going to notice the difference between font-weight: 400 and font-weight: 500? Well, if we can ditch a whole font file, then that could have a significant impact on performance which, in turn, improves the user experience.

I guess the conversation can instead be framed like this: Does the user experience of this font outweigh the user experience of a slightly faster website?

And this isn’t a criticism of the design at all! I think Zach shows us what a healthy relationship between designers and developers can look like: collaborating and making joint decisions based on the context and the problem at hand, rather than treating static mockups as the final, concrete source of truth.

Direct Link to ArticlePermalink

The post Developing a robust font loading strategy for CSS-Tricks appeared first on CSS-Tricks.


, , , , ,

Using a Mixin to Take the Math out of Responsive Font Sizes

Responsive Font Size (RFS) is an engine that automatically calculates and updates the font-size property on elements based on the dimensions of the browser viewport.

If you’re thinking that sounds familiar, that’s because there is a slew of tools out there that offer various approaches for fluid typography. In fact, Chris compiled a bunch of those a little while back. Check that out because it’s always good to know what’s out there and what fits best for a particular task.

RFS is different in that it makes writing code for fluid type feel a lot like writing native CSS (or, more accurately, like writing with a preprocessor) directly in the stylesheets you’re already working in — only without having to wrangle and manage a bunch of media queries. It’s even compatible with Sass, Less, Stylus and PostCSS, so it plugs into just about any stack.

Just how integrated is it? Well, let’s compare a snippet for fluid typography that uses the calc() function…

html {   font-size: 16px; }  @media screen and (min-width: 320px) {   html {     font-size: calc(16px + 6 * ((100vw - 320px) / 680));   } }  @media screen and (min-width: 1200px) {   html {     font-size: 22px;   } }

…with a similar example of how it can be done with RFS in Sass:

.title {   @include font-size(4rem); }

Which compiles to:

.title {   font-size: 4rem; }  @media (max-width: 1200px) {   .title {     font-size: calc(1.525rem + 3.3vw);   } }

Curious how that works? Let’s check it out and then go into how to set it up for a project.

The magic behind automatic re-scaling

Here’s a graph to get a better understanding of how RFS re-scales font sizes:

Every color represents a font size that gets passed to the font-size() mixin provided by RFS. The y-axis of the graph represents the font size (in px) and the x-axis represents the width of the viewport (again, in px).

Let’s focus on the green line, which is generated by applying a mixin to an element:

.title {   @include font-size(40); }

In this case, a font size of 40px is passed into the mixin. That value serves as the maximum font size of the element and reaches that size when the viewport is 1200px or wider, at which point it stays at that size.

Conversely, the font size will bottom out at 20px, never going below that mark.

Everything else? Well, that’s where the font size value is automatically calculated, using a function behind the scenes to determine the number according to the current width of the viewport.

RFS is also a little opinionated in that it limits itself to font sizes that are 20px and above. The reasoning is that smaller text (e.g. normal body text) normally does not need to flex as much and is a lot easier to manage than larger pieces of content, like titles and such. That’s very much in line with FitText, which also prefers being used on large text (even though it will not stop you from doing it).

If you’re the type of person who likes to look under the hood, the mixin for each preprocessor is available to view in the RFS GitHub repo. For example, here’s a direct link to the SCSS version. It’s a lot of math!

Note that every font size is generated in a combination of rem and vw units, but they are mapped to px in the graph to make it easier to understand. In other words, it really takes all the mathwork out of the mix.

Everything is configurable

Seriously. Every. Single. Thing.

For example, you may have wondered why the font size capped out at viewports 1200px and wider in the previous example. That can be changed, as well as a ton of other things, including:

  • Base font size: The lowest font size value.
  • Font size unit: The type of unit to use in the output value (px or em).
  • Breakpoint: The maximum width of the viewport where the font size of the element reaches its maximum value.
  • Breakpoint unit: The unit used for the media query that the mixin generates (px, em or rem).
  • Factor: This serves as a sorta volume control that informs the mixin how aggressive it should be in calculating font sizes from the maximum viewport width all the way down.
  • Rem value: This defines the value of 1rem in pixel (px) units.
  • Two dimensional: A feature that sniffs out the smallest side of a viewport and uses it to calculate the font size value. This comes in handy when, say, you’d like to keep the font from getting smaller when a device is rotated from a portrait orientation to landscape.
  • Class: Provides class names that can be added to an element in the HTML to either enable or disable fluid sizing.

So, yeah. A lot of options and flexibility here. The important thing to know is that all of these options are variables that can be defined in your stylesheets.

All this said, the default settings are pretty safe to use, and they will prevent a lot of longer words truncating from the viewport. This is especially true for some languages — like German or Dutch — that contain a lot of compound words.

Using RFS in a project

Let’s dive straight into to the code. It would be exhaustive to look at the code for each preprocessor, so I’ll be explaining everything in the .scss syntax. But if you prefer something else, you can check out the examples in other languages in the GitHub repo in the Usage section.

First and foremost, RFS needs to be installed on the project. It’s available in npm and Yarn:

## npm npm install rfs  ## Yarn yarn add rfs  ## Bower is available, but has been deprecated bower install rfs --save

Then, gotta make sure the mixin is imported with the rest of the styles, wherever you do your imports for other partials:

@import "~rfs/scss";

Now, we can start cooking with the mixin!

.title {   color: #333;   @include font-size(64px); }  .subtitle {   color: #666;   @include font-size(48px); }  .paragraph {   @include font-size(16px); }

I passed values in px, but rem units are also supported. If a value without a unit is passed, px is used by default. The font sizes are always rendered in rem (in combination with vw) to make sure the font sizes also increase when the default font size is increased in the browser (this is a feature often used by visually impaired people).

The output is:

.title {   color: #333;   font-size: 4rem; }  @media (max-width: 1200px) {   .title {     font-size: calc(1.525rem + 3.3vw);   } }  .subtitle {   color: #666;   font-size: 3rem; }  @media (max-width: 1200px) {   .subtitle {     font-size: calc(1.425rem + 2.1vw);   } }  .paragraph {   font-size: 1rem; }

Notice that the mixin is font-size(), but RFS will also let you use it in two other ways:

.title {   @include font-size(4rem);   // or   @include responsive-font-size(64px);   // or   @include rfs(64); }

RFS is baked right into Bootstrap

Here’s a little story for you.

One day, I had this incredibly impulsive idea to put RFS into Bootstrap. I actually did not use Bootstrap at that time, but believed it was a feature Bootstrap could definitely use. I made a pull request and waited a couple months to see what would happen.

In the meantime, I was getting more and more intrigued by Bootstrap and version 4 had just been released. Slowly but surely, I got more involved in contributing to the project and a whole new world opened for me when I discovered the community behind it. It was during hacktoberfest (oh yes, I got my t-shirt) in October 2018 that I got asked to join the Bootstrap team by mdo.

I believe contributing to open source projects is such a fun and rewarding thing. Andrés Galante has a great post on the topic if you’re interested in becoming a contributor.

Since then, RFS has become a project of the Bootstrap team, and on February 11th this year, we launched Bootstrap 4.3 which includes RFS right out of the box. It’s currently disabled by default, but can easily be switched on by setting the Sass variable $ enable-responsive-font-sizes: true.

But make no mistake: RFS can still be used on its own. Just cool that it’s baked right into a widely used framework.

Oh yeah, let’s talk browser support

Support is pretty darn good! In fact, RFS will work anywhere that supports media queries and viewport units. RFS will set a font size for Legacy browsers, like Internet Explorer 8, but the fluidity won’t be there. In other words, should be safe for production!

What’s next for RFS

The next major version of Bootstrap is version 5 and we’re planning to enable RFS by default. We don’t have any plans to change the way it works for now. More than likely, the $ enable-responsive-font-sizes variable will simply be set to true and that’s it.

In the future, I hope I can make use of the min() function because it would generate less CSS and make things a lot less complex. Browsers don’t seem to support this function all too well just yet, but if you’re interested in this feature, you can follow the progress in this GitHub issue.

Anything else? No, but I can leave you with a little song and dance: Na na na na, na na na na, hey hey hey goodbye!

The post Using a Mixin to Take the Math out of Responsive Font Sizes appeared first on CSS-Tricks.


, , , , , ,

Control Icons with Font Size

Here’s a nifty trick from Andy Bell that now seems a little obvious in hindsight: if you set an SVG to have a width and height of 1em then you can change the size of it with the font-size property.

Try and change the font-size on the body element below to see the icon scale with the text:

See the Pen
Font size controlled icon
by Andy Bell (@andybelldesign)
on CodePen.

You pretty much always want your icons to be aligned with text so this trick helps by creating that inherent relationship in your code. Pretty nifty, eh?

Direct Link to ArticlePermalink

The post Control Icons with Font Size appeared first on CSS-Tricks.


, , ,