Making type follow the reader

Users increasingly expect interfaces to respect their system-level preferences, and typography is one area where those preferences matter most. With CSS media queries and variable fonts, you can go beyond simply switching color schemes and actually tune the weight and grade of your text to match how each person reads best.

This is made possible by mapping user preferences like dark mode or contrast sensitivity to the GRAD and wght axes of a variable font. The strategy is simple at heart: a high-contrast preference gets a bolder font, a low-contrast preference gets a thinner one, and dark mode gets a slightly reduced gradation to compensate for the perceptual differences between light-on-dark and dark-on-light rendering.

Consider a lightweight demo with a short paragraph of content as the target for these adaptive styles. Before any preference-aware CSS is added, set font-size in rems rather than pixels so the browser can honor the user’s minimum font size setting; pixels squash those preferences entirely.

With the content in place, the first step is loading a variable font that exposes useful customization axes. Roboto Flex is one such font, and it provides the GRAD and wght axes needed here. The font is brought in via the standard @font-face API and then applied globally:

@font-face {
  font-family: "Roboto Flex";
  src: url('https://assets.codepen.io/2585/RobotoFlex') format('truetype');
}
@layer demo.support {
  body {
    font-family: Roboto Flex;
  }
}

A
screenshot preview of the demo so far, with the font now in Roboto Flex in both
dark and light themes.

Authoring adaptive defaults

CSS custom properties are the mechanism for wiring preference changes into the font settings. Define a default set of values at the top level that apply to users who have expressed no specific preferences:

@layer demo {
  body {
    --base-weight: 400;
    --base-grade: 0;

    font-variation-settings:
      "wght" var(--base-weight),
      "GRAD" var(--base-grade)
    ;
  }
}

Responding to high and low contrast preferences

When the environment signals a high-contrast preference via prefers-contrast: more, raise the base weight from 400 to 700:

@layer demo {
  @media (prefers-contrast: more) {
    body {
      --base-weight: 700;
    }
  }
}

In contrast, a preference for reduced contrast (prefers-contrast: less) should dial the base weight down to 200:

@layer demo {
  @media (prefers-contrast: less) {
    body {
      --base-weight: 200;
    }
  }
}

Accommodating dark mode

Dark mode has its own typographic considerations, separate from explicit contrast settings. When the color scheme preference indicates a dark theme, adjust the font settings to account for the visual flare that occurs when light text sits on dark backgrounds:

@layer demo {
  @media (prefers-color-scheme: dark) {
    body {
      --base-grade: -25;
    }
  }
}

Wiring it all together

Each media query reinforces the others. Since preferences can combine (a dark-mode user may also prefer high contrast), the rules stack on top of the defaults using custom property overrides:

@layer demo {
  body {
    --base-weight: 400;
    --base-grade: 0;

    font-variation-settings:
      "wght" var(--base-weight),
      "GRAD" var(--base-grade)
    ;
  }

  @media (prefers-contrast: more) {
    body {
      --base-weight: 700;
    }
  }

  @media (prefers-contrast: less) {
    body {
      --base-weight: 200;
    }
  }

  @media (prefers-color-scheme: dark) {
    body {
      --base-grade: -25;
    }
  }
}

For a cleaner syntax, the same cascade can be written with CSS nesting to keep each preference’s adjustments grouped inside its media query:

@layer demo {
  body {
    --base-weight: 400;
    --base-grade: 0;

    font-variation-settings:
      "wght" var(--base-weight),
      "GRAD" var(--base-grade)
    ;

    @media (prefers-contrast: more)     { --base-weight: 700 }
    @media (prefers-contrast: less)     { --base-weight: 200 }
    @media (prefers-color-scheme: dark) { --base-grade: -25 }
  }
}

The net result is type that shifts fluidly with each user’s stated needs, turning the reading experience into a point of personalization rather than a static render.