Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add createFontStack for metrics-based font fallbacks #117

Merged
merged 12 commits into from
Jan 30, 2023
47 changes: 47 additions & 0 deletions .changeset/tall-scissors-draw.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
---
'@capsizecss/core': minor
---

Add `createFontStack` for metrics-based font family fallbacks.

Creates metrics-based `@font-face` declarations to improve the alignment of font family fallbacks, which can dramatically improve the [Cumulative Layout Shift](https://web.dev/cls/) metric for sites that depend on a web font.

### Example usage

```ts
import { createFontStack } from '@capsizecss/core';
import lobster from '@capsizecss/metrics/lobster';
import helveticaNeue from '@capsizecss/metrics/helveticaNeue';
import arial from '@capsizecss/metrics/arial';

const { fontFamily, fontFaces } = createFontStack([
lobster,
helveticaNeue,
arial,
]);

```

The returned values are the computed font family and the generated font face declarations:
```ts
// `fontFamily`
"Lobster, 'Lobster Fallback: Helvetica Neue', 'Lobster Fallback: Arial'"
```

```css
/* `fontFaces` */
@font-face {
font-family: 'Lobster Fallback: Helvetica Neue';
src: local('Helvetica Neue');
ascent-override: 115.1741%;
descent-override: 28.7935%;
size-adjust: 86.8251%;
}
@font-face {
font-family: 'Lobster Fallback: Arial';
src: local('Arial');
ascent-override: 113.5679%;
descent-override: 28.392%;
size-adjust: 88.053%;
}
```
158 changes: 147 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ npm install @capsizecss/core
- [Line height](#line-height)
- [Font Metrics](#font-metrics)
- [Core](#core)
- [createFontStack](#createfontstack)
- [precomputeValues](#precomputevalues)
- [getCapHeight](#getcapheight)
- [Integrations](#integrations)
Expand Down Expand Up @@ -163,21 +164,157 @@ This metadata is extracted from the metrics tables inside the font itself. There

## Core

The core package also provides access to lower level values for a specific font and font size combination.
The core package also provides a few other metrics-based features for improving typography on the web:

### createFontStack

Creates metrics-based `@font-face` declarations to improve the alignment of font family fallbacks, which can dramatically improve the [Cumulative Layout Shift](https://web.dev/cls/) metric for sites that depend on a web font.

#### Usage

Consider the following example, where the desired web font is [Lobster](https://fonts.google.com/specimen/Lobster), falling back to `Helvetica Neue` and then `Arial`, e.g. `font-family: Lobster, 'Helvetica Neue', Arial`.

1. Import `createFontStack` from the core package:

```ts
import { createFontStack } from '@capsizecss/core';
```

2. Import the font metrics for each of the desired fonts (see [Font Metrics](#font-metrics) above):

```ts
import lobster from '@capsizecss/metrics/lobster';
import helveticaNeue from '@capsizecss/metrics/helveticaNeue';
import arial from '@capsizecss/metrics/arial';
```

3. Create your font stack passing the metrics as an array, using the same order as you would via the `font-family` CSS property.

```ts
const { fontFamily, fontFaces } = createFontStack([
lobster,
helveticaNeue,
arial,
]);
```

The returned value contains the generated font face declarations as well as the computed `fontFamily` with the appropriately ordered font aliases.

#### Usage in CSS stylesheet or a style tag

The returned values can be templated into a stylesheet or a `style` block. Here is an example [handlebars](https://handlebarsjs.com/) template:

```html
<style type="text/css">
.heading {
font-family: {{ fontFamily }}
}

{{ fontFaces }}
</style>
```

This will produce the following CSS:

```css
.heading {
font-family: Lobster, 'Lobster Fallback: Helvetica Neue',
'Lobster Fallback: Arial';
}
michaeltaranto marked this conversation as resolved.
Show resolved Hide resolved

@font-face {
font-family: 'Lobster Fallback: Helvetica Neue';
src: local('Helvetica Neue');
ascent-override: 115.1741%;
descent-override: 28.7935%;
size-adjust: 86.8251%;
}
@font-face {
font-family: 'Lobster Fallback: Arial';
src: local('Arial');
ascent-override: 113.5679%;
descent-override: 28.392%;
size-adjust: 88.053%;
}
```

#### Usage with CSS-in-JS framework
michaeltaranto marked this conversation as resolved.
Show resolved Hide resolved

If working with a CSS-in-JS library, the returned `fontFaces` can be provided as a JavaScript style object by providing `styleObject` as a `fontFaceFormat` option.

Here is an example using [Emotion](https://emotion.sh/):

```tsx
import { Global } from '@emotion/core';

const { fontFaces, fontFamily } = createFontStack(
[lobster, helveticaNeue, arial],
{
fontFaceFormat: 'styleObject',
},
);

export const App = () => (
<>
<Global styles={fontFaces} />
<p css={{ fontFamily }}>...</p>
</>
);
```

> Also useful as a source for further manipulation given it is a data structure that can be iterated over or extended.

#### Providing additional `font-face` properties

Additional properties can be added to the generated `@font-face` declarations via the `fontFaceProperties` option:

```ts
const { fontFamily, fontFaces } = createFontStack(
[lobster, helveticaNeue, arial],
{
fontFaceProperties: {
fontDisplay: 'swap',
},
},
);
```

This will result in the following additions to the declarations:

```diff
@font-face {
font-family: 'Lobster Fallback: Helvetica Neue';
src: local('Helvetica Neue');
ascent-override: 115.1741%;
descent-override: 28.7935%;
size-adjust: 86.8251%;
+ font-display: swap;
}
@font-face {
font-family: 'Lobster Fallback: Arial';
src: local('Arial');
ascent-override: 113.5679%;
descent-override: 28.392%;
size-adjust: 88.053%;
+ font-display: swap;
}
```

### precomputeValues

Returns all the information required to create styles for a specific font size given the provided font metrics. This is useful for integrations with different styling solutions.
Returns all the information required to create leading trim styles for a specific font size given the provided font metrics. This is useful for integrations with different styling solutions.

Accepts the same [options](#options) as [createStyleObject][#createstyleobject] and [createStyleString][#createstylestring].
michaeltaranto marked this conversation as resolved.
Show resolved Hide resolved

```ts
import { precomputeValues } from '@capsizecss/core';
import arialMetrics from '@capsizecss/metrics/arial';

const capsizeValues = precomputeValues({
fontSize: 24,
fontMetrics: {
...
}
})
fontSize: 16,
leading: 24,
fontMetrics: arialMetrics,
});

// => {
// fontSize: string,
Expand All @@ -193,13 +330,12 @@ Return the rendered cap height for a specific font size given the provided font

```ts
import { getCapHeight } from '@capsizecss/core';
import arialMetrics from '@capsizecss/metrics/arial';

const actualCapHeight = getCapHeight({
fontSize: 24,
fontMetrics: {
...
}
})
fontMetrics: arialMetrics,
});

// => number
```
Expand Down
13 changes: 11 additions & 2 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,21 @@
"cap height",
"typography",
"line gap",
"leading"
"leading",
"fallback font",
"font metrics",
"ascent override",
"descent override",
"line gap override",
"size adjust",
"content layout shift",
"cls"
],
"license": "MIT",
"dependencies": {},
"devDependencies": {
"@babel/core": "^7.19.6",
"@emotion/css": "^11.1.3"
"@emotion/css": "^11.1.3",
"csstype": "^3.1.1"
michaeltaranto marked this conversation as resolved.
Show resolved Hide resolved
}
}