Font Assets
Where font files go in the source, how they are named by role, and which formats the build copies to web, iOS and Android.
Introduction
Fonts are the files under fonts/ of an app. The build copies the WOFF and WOFF2 files to the web output and the TTF and OTF files to iOS and Android, together with the license of each font. Stylesheets in the folder go to the web output only.
The files are named by the role a font plays, not by its family, so that every brand ships the same file names with fonts of its own.
Source layout
Fonts live in source/<brand>/<app>/fonts/. The default brand holds the fonts that every brand gets, and a brand holds only the files that differ.
source/
├── default/
│ └── <app>/
│ └── fonts/
│ ├── text-normal.woff2 -> copied to the web output
│ ├── text-normal.ttf -> copied to the iOS and Android output
│ ├── text.css -> copied to the web output
│ └── text-license.txt -> copied to every platform
└── <brand>/
└── <app>/
└── fonts/
└── text-normal.woff2 -> replaces the default file of the same nameA brand file replaces the default file of the same path. A brand without a fonts/ folder gets the default fonts as they are.
Formats
Each platform keeps the formats it can load, and the build leaves the rest out.
| Format | Web | iOS | Android |
|---|---|---|---|
| WOFF2, WOFF | Copied | Dropped | Dropped |
| TTF, OTF | Dropped | Copied | Copied |
| CSS, SCSS | Copied | Dropped | Dropped |
| TXT | Copied | Copied | Copied |
The build does not convert a font from one format to another. A font that is needed on the web and in an app is added in both formats.
A font is copied with every character it has. A build with the --subset option writes the WOFF and WOFF2 files of the web again with the characters of a list of Unicode ranges only, under the same names; see Font subsets. The same option fixes an axis of a variable font at a value, where the configuration names one for the brand, and, with --split, writes a font once per range beside itself, with a rule per range in the stylesheets.
Naming
A font file is named <role>-<weight>.<ext>, with -italic after the weight for an italic. The role and the weight are the names that Chassis Tokens uses for typography, so a file name says where the font is used and stays the same when the family changes.
| Part | Names | Meaning |
|---|---|---|
| Role | text, display, code | Body and interface text, headings, monospace |
| Weight | elegant, normal, strong, mass | From the lightest to the heaviest weight of the role |
The default brand names its fonts this way:
source/default/demo/fonts/
├── code-normal.ttf
├── code-strong.ttf
├── display-normal.ttf
├── display-normal-italic.ttf
├── display-strong.ttf
├── text-normal.ttf
├── text-normal-italic.ttf
├── text-strong.ttf
└── ...A variable font holds every weight of a role in one file, so its name has no weight: <role>.<ext>, and <role>-italic.<ext> for the italic.
Which family a role holds, and which numeric weight a name stands for, is the choice of the brand. The stylesheet of the role records it for the web, see Stylesheets.
Role names are a convention, not a rule of the build. A file with another name, such as inter-regular.woff2, is copied like any other font. It follows the general naming rules of the design guidelines: lowercase letters, digits and hyphens.
Adding files
A font enters the repository as a file in fonts/, in every format its platforms need, with its license beside it.
Font files
Put the WOFF2 file of a font in the fonts/ folder of each web app, and the TTF or OTF file in the fonts/ folder of each iOS and Android app. Name both by role. WOFF is copied too, for a brand that still needs it.
Fonts are Git LFS files. Install Git LFS before adding one, so that the commit holds a pointer and not the font itself.
Variable fonts
A family with a variable font gets both forms in a web app: the variable file of the role, and one file per weight beside it.
source/default/docs/fonts/
├── text-italic.woff2 -> every weight, italic
├── text-normal.woff2 -> one weight
├── text-strong.woff2
├── text.css -> declares text.woff2 and text-italic.woff2
├── text.woff2 -> every weight, upright
└── ...The stylesheet of the role declares the variable file. The files by weight stay for a page that writes its own @font-face rules. A family without a variable font has the files by weight only, and its stylesheet declares those.
The demo app of this repository, which is built for iOS and Android, holds the files by weight only.
Brand fonts
A brand changes a font by adding a file of the same name to its own folder. The sinefil brand of this repository replaces the font of every role, in both apps:
source/sinefil/docs/fonts/
├── code-license.txt
├── code-normal.woff2
├── code-strong.woff2
├── code.css
├── display-license.txt
├── display-normal.woff2
├── display.css
├── display.woff2
└── ...A brand that replaces the files of a role replaces the stylesheet and the license of that role too, since both describe the family.
A brand file replaces a default file only when the name and the extension are the same. A brand with text-normal.otf beside a default text-normal.ttf gets both files in its output. Every brand of this repository has its app fonts as TTF for that reason.
A brand gets every default file that it does not replace. The code family of the sinefil brand has no variable font, so its web output holds code.woff2 of the default brand, which code.css of the brand does not load. pnpm assets:lint:source warns about such a file: a font of the default brand whose family no stylesheet of the brand declares.
Stylesheets
A .css or .scss file in fonts/ is copied to the web output like a font. The default brand has one stylesheet per role, text.css, display.css and code.css, each with one @font-face rule per variable file of the role:
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url("text.woff2") format("woff2");
}The rule names the family of the brand and the range of weights the file holds, and loads the file from the folder the stylesheet is in. The stylesheet of a family without a variable font has one rule per file, with the numeric weight of the role. A page that links fonts/text.css needs no @font-face rule of its own.
pnpm assets:lint:source reads the file of each rule and fails when the rule says something else than the file is: another family, another weight or a range the weight axis does not hold, or an italic declared as normal. A cut of a family is of the family: DM Sans 18pt is declared as DM Sans.
A rule declares the file of the web, and an app gets the file of the same name in another format. The lint checks the rule against that file too: text.ttf beside a declared text.woff2 has to be what the rule says. It also fails when two files of one name in a folder, such as text-normal.ttf and text-normal.woff2, are not one font in two formats: when they differ in the family, the weight, the style, the axes, the version or the number of glyphs. Write both files from the same font to keep them together.
The lint warns about a format() that is not the format of its file, such as format("woff") after a .woff2 file.
A stylesheet may also load fonts from a service instead of the folder. The build copies it as it is.
Licenses
Every font in source/ has its license beside it, named by role: text-license.txt, display-license.txt and code-license.txt. The build copies the license to the fonts/ folder of every platform, so the license text travels with every copy of the font, as the SIL Open Font License asks.
Check that the license of a font allows embedding in an app and serving on the web before adding it. A brand that replaces a font adds the license of the new font under the same name.
Integration
The role and weight names come from the typography tokens of Chassis Tokens. The font tokens hold the family of each role and the weight of each name, and the typography tokens combine them into text styles.
The two projects meet in the names only. The build of Chassis Assets does not read a token, and a brand keeps the family in its tokens and the files in its fonts/ folder in step by hand.
Best practices
The rules of fonts/ leave a few choices to the designer. These keep a font replaceable by brand and complete on every platform.
- Name fonts by role:
text-strong.woff2works for every brand. A file named after a family has to be renamed, in the source and in every app, when the brand changes its font. - Add every format a platform needs: WOFF2 for a web app, TTF or OTF for an iOS or Android app. The build drops a format a platform does not keep and does not convert one.
- Keep the license beside the font: as
<role>-license.txtin the same folder. The build copies it to every platform. - Replace the stylesheet with the font: a brand that replaces
display-*.woff2adds its owndisplay.css, or the web output declares the family of the default brand for the files of the brand. - Replace every file of a role: a brand adds a file for each default file of the role, under the same name and extension, or its output holds the default font beside its own.
Avoid the habits that undo this:
- Do not put a font outside
fonts/: a WOFF2 file under another folder is copied to iOS and Android as well, since onlyfonts/has the format rules.
Platform output
The build copies each font to the fonts/ folder of the job, in the formats the platform keeps, and renames it for the platform.
| Platform | Folder | Name | Formats |
|---|---|---|---|
| Web | dist/web/<app>/<brand>/fonts/ | kebab-case | WOFF2, WOFF, CSS, SCSS, TXT |
| iOS | dist/ios/<app>/<brand>/fonts/ | snake_case | TTF, OTF, TXT |
| Android | dist/android/<app>/<brand>/fonts/ | snake_case | TTF, OTF, TXT |
The web output of the configured web app, for the default fonts:
dist/web/docs/chassis/fonts/
├── ...
├── text-italic.woff2
├── text-license.txt
├── text-normal-italic.woff2
├── text-normal.woff2
├── text-strong.woff2
├── text.css
└── text.woff2The iOS and the Android output of the configured app hold the same files, under the same names:
dist/ios/demo/chassis/fonts/
├── text_license.txt
├── text_normal.ttf
├── text_normal_italic.ttf
├── text_strong.ttf
└── ...The snake_case name is a valid Android resource name, so a file of the Android output can go to res/font/ as it is. A build with the --res option writes the font files there, and leaves the licenses in fonts/.
See the web, iOS, and Android docs for how an app uses these files.