Fonts
This document explains how a site built on Filament CMS loads its typefaces, and how to give a skin its own fonts: from Bunny Fonts, from font files the site hosts itself, or from both at once. It is written for the developer who builds or changes a skin. The skin system itself is described in Design system.
How the CMS loads fonts
A font reaches a page in two separate steps, and a skin needs both:
- Loading. The browser downloads the font files. The CMS can do this for the site through Bunny Fonts, or the site can serve the files itself.
- Using. The skin's stylesheet names the font in a font token. A font that is loaded but not named in a token is downloaded and never shown, and a font that is named but not loaded is replaced by the next font in the token's list.
The font tokens
Every font on a page comes from one of four CSS custom properties, defined in resources/css/cms.css:
| Token | Where it is used | Base value |
|---|---|---|
| --font-cms-display | Headings and large display text | 'Instrument Sans', ui-sans-serif, system-ui, sans-serif |
| --font-cms-body | Interface text, buttons, forms and short copy | 'Instrument Sans', ui-sans-serif, system-ui, sans-serif |
| --font-cms-prose | Long-form rich text, such as article bodies | 'Newsreader', ui-serif, Georgia, serif |
| --font-cms-mono | Code | ui-monospace, 'SF Mono', Menlo, monospace |
A skin changes a font by setting these tokens inside its own selector, for example [data-cms-skin='mcg']. The values after the first name are fallbacks: the browser uses the first font in the list that it has loaded.
What the CMS requests from Bunny Fonts
When theme.fonts in config/cms.php is true, which is the default, the page head (resources/views/components/head.blade.php) contains a <link rel="preconnect" href="https://fonts.bunny.net"> element and one stylesheet link to https://fonts.bunny.net/css?family=β¦&display=swap. Theme::fontFamilies() (src/Support/Theme.php) decides which families that link requests, in this order:
- The families the active skin declares, in
theme.skin_fontsinconfig/cms.phpor in the third argument ofSkinRegistry::register(). - When the skin declares none and it is one of the six skins the package ships, the families listed for it in the table below.
- When neither applies, the base pair: Instrument Sans and Newsreader.
Only one of the three is requested. A skin that declares its own families does not also receive the base pair.
| Skin | Families requested |
|---|---|
| Base (no skin) | instrument-sans:400,500,600,700, newsreader:400,400i,500,600 |
| press | fraunces:400,500,600,700, newsreader:400,400i,500,600 |
| signal | bricolage-grotesque:400,600,700,800, archivo:400,500,600, jetbrains-mono:400,500 |
| marquee | archivo:400,600,700,800,900, public-sans:400,500,600,700, ibm-plex-mono:400,500 |
| saas | inter:400,500,600,700,800, jetbrains-mono:400,500 |
| arcade | press-start-2p:400, pixelify-sans:400,500,600,700, vt323:400 |
| market | montserrat:400,500,600,700 |
display=swap makes the browser show the text in a fallback font while the font file downloads, so text is never hidden while a font loads.
When theme.fonts is false, the CMS adds neither link, and the site is responsible for loading every font it uses.
A registered skin receives the base pair
A skin that the site registers, in theme.skins or with SkinRegistry::register(), is not one of the six in the table. Unless it declares its own families, the CMS requests Instrument Sans and Newsreader for it, whatever its stylesheet sets the font tokens to. The page then shows the fallback fonts instead of the skin's, and nothing reports an error. This is the most common reason a new skin's fonts do not appear.
Choosing a method
| The skin's fonts | Method | Section |
|---|---|---|
| All available on Bunny Fonts | List them in theme.skinfonts | Adding Bunny Fonts to a skin |
| All served from the site's own files | Turn the CMS's font loading off and declare the files with @font-face | Self-hosting the fonts |
| Some from Bunny Fonts and some self-hosted | List only the Bunny families in theme.skinfonts, and declare the others with @font-face | Using Bunny Fonts and self-hosted fonts together |
Self-hosting is the choice when a font is not on Bunny Fonts (a licensed brand font, for example), or when the site must not make requests to another host.
Adding Bunny Fonts to a skin
This example gives a skin named mcg Montserrat for headings and Lora for long-form text.
- At https://fonts.bunny.net, search for each family and note two things: its identifier, which is the family name in lower case with hyphens for spaces (
montserrat,lora,source-sans-3), and the weights the skin's stylesheet uses. Request only those weights: each weight is a separate file that every visitor downloads. Addito a weight for its italic, for example400i. - In
config/cms.php, undertheme, list the families underskin_fonts, keyed by the skin's name exactly as it is registered:
'theme' => [
// ...
'skin_fonts' => [
'mcg' => ['montserrat:600,700', 'lora:400,400i,700'],
],
],
A skin registered from a service provider passes the same list as the third argument instead:
app(SkinRegistry::class)->register('mcg', 'MCG Strategic', ['montserrat:600,700', 'lora:400,400i,700']);
- In the skin's stylesheet, for example
resources/css/skins/mcg.css, name the fonts in the tokens. Use the family's display name, with its capitals and spaces, in quotes:
[data-cms-skin='mcg'] {
--font-cms-display: 'Montserrat', ui-sans-serif, system-ui, sans-serif;
--font-cms-prose: 'Lora', ui-serif, Georgia, serif;
}
A token the skin does not set keeps its base value. In this example --font-cms-body is still Instrument Sans, which the CMS no longer requests, because the skin now declares its own families. Set every token the skin's design uses, or add the base family to skin_fonts.
- In a terminal at the site's root, clear the cached configuration and rebuild the CSS:
php artisan config:clear
npm run build
- In the browser, follow Checking that a font loads.
Self-hosting the fonts
This example serves Source Sans 3 from the site's own files.
- In
resources/fonts/in the site, add the font files in WOFF2 format, one file per weight and style, for examplesource-sans-3-400.woff2andsource-sans-3-700.woff2. Check that the font's license allows hosting it on a website. - In the skin's stylesheet, for example
resources/css/skins/mcg.css, declare each file with@font-face, above the skin's selector. The path is relative to the stylesheet, and Vite copies each file intopublic/buildwhen the CSS is built:
@font-face {
font-family: 'Source Sans 3';
src: url('../fonts/source-sans-3-400.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Source Sans 3';
src: url('../fonts/source-sans-3-700.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
}
- In the same stylesheet, name the family in the tokens, spelled exactly as in
font-familyabove:
[data-cms-skin='mcg'] {
--font-cms-display: 'Source Sans 3', ui-sans-serif, system-ui, sans-serif;
--font-cms-body: 'Source Sans 3', ui-sans-serif, system-ui, sans-serif;
}
- In
config/cms.php, undertheme, set'fonts' => false. Without this, the CMS still requests Instrument Sans and Newsreader from Bunny Fonts on every page, and visitors download two families nothing displays.falseturns the CMS's font loading off for every skin, so a site that switches between several skins must self-host the fonts of all of them. - In a terminal at the site's root, run
php artisan config:clear, thennpm run build. - In the browser, follow Checking that a font loads.
Using Bunny Fonts and self-hosted fonts together
A skin can take some fonts from Bunny Fonts and serve others itself. This example keeps a self-hosted Source Sans 3 for body text and adds Montserrat from Bunny Fonts for headings.
- In
config/cms.php, undertheme, keep'fonts' => true, and list only the families that come from Bunny Fonts:
'skin_fonts' => [
'mcg' => ['montserrat:600,700'],
],
Listing at least one family for the skin also stops the CMS from requesting the base pair for it.
- In the skin's stylesheet, declare the self-hosted files with
@font-faceas in Self-hosting the fonts, and name both families in the tokens:
[data-cms-skin='mcg'] {
--font-cms-display: 'Montserrat', ui-sans-serif, system-ui, sans-serif;
--font-cms-body: 'Source Sans 3', ui-sans-serif, system-ui, sans-serif;
}
- In a terminal at the site's root, run
php artisan config:clear, thennpm run build. - In the browser, follow Checking that a font loads. Montserrat loads from
fonts.bunny.net, and Source Sans 3 loads from the site's own/build/assets/path.
Checking that a font loads
- In the browser, open a page that uses the skin, open DevTools with F12, select the Network tab, then the Font filter, and reload the page. Each family the skin uses appears as one or more
.woff2requests, fromfonts.bunny.netor from/build/assets/, each with status 200. - In DevTools, in the Elements tab, select a heading, open the Computed pane, and scroll to Rendered Fonts at the bottom. It names the font the browser actually drew the text with. A fallback name there, such as Arial or Segoe UI, means the font was named but not loaded.
- In an MCP client connected to the site, call
cms_theme_state. Itsfontsentry reports whether the CMS loads fonts (loading), the Bunny Fonts URL it requests (url), the families in that URL (families), where they came from (families_source:skin,packageorbase), and whether the active skin is served the base pair (serving_base_families). It also returns the findingfonts_fall_back_silentlywhen the active skin was registered by the site and receives the base pair.
Problems and their causes
| What is seen | Cause | Fix |
|---|---|---|
| The skin's headings show Instrument Sans | The skin was registered by the site and declares no families, so the CMS requested the base pair | Add the skin's families to theme.skinfonts |
| The text shows a fallback font, and Network has no request for the font | The family is named in a token but loaded by nothing | Add it to theme.skinfonts, or declare it with @font-face |
| skinfonts was changed and nothing changed on the site | The configuration is cached | Run php artisan config:clear |
| Network shows a Bunny Fonts request, and the text still shows a fallback | The name in the token differs from the family's display name, for example 'Source Sans 3' against 'Source Sans Pro' | Copy the display name from fonts.bunny.net, or from the font-family in @font-face |
| Bold text looks smeared or wider than in the design | The weight is not requested, so the browser draws a synthetic bold from a lighter weight | Add the weight, for example montserrat:400,700 |
| A self-hosted font returns 404 | The url() path in @font-face does not lead from the stylesheet to the file, or the CSS was not rebuilt | Correct the relative path and run npm run build |
| No font loads, and the browser console reports a Content Security Policy error | The site sends a Content-Security-Policy header that does not allow fonts.bunny.net | Allow https://fonts.bunny.net in style-src and font-src |