Font Family and @supports

I’m working on a website where I want to use Apple’s “New York” typeface (the serif counterpart to their “San Francisco” sans-serif).

Remembering my documented investigation into leveraging system fonts on the web, I knew I could use font-family: ui-serif — part of CSS fonts level 4 — to accomplish the task.

On macOS, I had it working in Safari but not in Chrome or Firefox. That was to be expected, as support for level 4 generic font family names, like ui-serif, is currently limited to Safari.

In my CSS, I was trying to query support for ui-serif with @supports but it wasn’t working as I expected. For illustration’s sake, this was my mental model:

/* For browsers that DO NOT support `ui-serif`
   (i.e. Chrome and Firefox), do this: */
body {
  background-color: red;
}

/* For browsers that DO support `ui-serif`
   (i.e. Safari) do this: */
@supports (font-family: ui-serif) {
  body {
    background-color: green;
  }
}

Given the code above and the current support for the ui-serif generic family name, I expected Chrome and Firefox to have a red background and Safari to have a green one. Instead, they all had green backgrounds.

“How is that possible?” I thought. “Those browsers currently do not support ui-serif, yet the @supports syntax is triggering true for that syntax…”

I threw together a Codepen example and asked on Twitter.

I got some useful responses that helped refine my mental model for @supports. To explain, let me take a step back and look at @supports for one second.

@supports is incredibly useful when you need to test support for new CSS features. For example, you might want to use flex layout but still provide a fallback for older browsers with a float-based layout. In that scenario, you could do something like this:

/* For older browsers, use float-based layout */
.container > * {
    display: block;
    float: left;
}

/* For newer browsers, use flex-based layout */
@supports (display: flex) {
  .container {
    display: flex;
  }
  .container > * {
    float: none;
  }
}

In this particular case – @supports (display: <value>) — the property display has a specific set of enumerated values that are considered valid by the browser. While the value flex might not be supported in every browser, it is supported in the latest spec. Whereas a value like flexx won’t work in any browser because it’s not valid in any spec (unless, of course, one day the W3C decides to create a new layout mechanism named flexx).

The point being: display is a property with a finite set of values — block, flex, grid, etc. — that are considered valid by a reading of the latest specification (and browsers can vary in their support for each of those enumerated values).

In contrast, the font-family property is a bit different and more open-ended. It has to consider any value valid since it accepts font family names and a font’s name could be, well, anything.

@supports (font-family: Georgia) {
  /* supported */
}

@supports (font-family: asdfjasdfkljasdofijasdf) {
  /* supported */
}

@supports (font-family: ui-serif) {
  /* supported */
}

The above are all considered valid syntax for the font-family property because any of those could be the name of a font! It’s not like CSS maintains a master, enumerated list of every single font in the entire world (can you imagine?). As Bramus replied me on Twitter:

at-supports returns true when it can parse the given declaration — it answers “is this syntax OK?”

For font-family it is true for system-ui because that is a valid <generic-name>. Same for any type of gibberish, as that could be valid <family-name>

For the property font-family, I imagine the browser parsing the value and determining whether it’s looking at a generic font family keyword (like ui-serif or sans-serif) or a specific font family’s name corresponding to a font usable on the client (like "Georgia" or "Noto Sans").

Ultimately, I was hoping for a way to query support for a generic font family keyword in CSS (like ui-serif) but I don’t think it’s doable. Nonetheless, @kn_wler pointed out a JS solution:

document.fonts.check('12px ui-serif');

This all clicked via the short convos I was having on Twitter (thanks Twitter peeps) and I could’ve left it at that, but typing this out and trying to explain it in a blog post helps me actually learn from these folks and refine my mental model for how CSS works. Happy blogging everybody!