Fonts
The <x-wirekit::fonts /> component loads locally bundled Google Fonts and sets CSS custom properties for typography. Add it to your layout's <head> to activate font presets.
Usage
<head>
<x-wirekit::fonts />
</head>
Configuration
Configure which fonts to load in config/wirekit.php:
'fonts' => [
'sans' => 'inter', // or null for system font stack
'serif' => null,
'mono' => 'jetbrains-mono',
],
Setting a category to null uses the system font stack with zero extra requests.
display decides how the browser behaves while a font is still arriving, and it applies to every bundled face:
// 1. The default. Text paints immediately in the fallback and swaps when the
// real font lands — nothing is invisible while waiting.
'fonts' => [
'display' => 'swap',
],
optional is the other end: the browser gives the font a very short window and, if it misses, keeps the fallback for that page view — no swap at all, and therefore no shift, at the price of sometimes not showing your font. block, fallback and auto behave as the CSS property describes.
Changing it does not require re-publishing: the value is applied when the CSS is served. If you published your fonts to public/, re-run php artisan wirekit:publish-fonts --force so the copies on disk carry the new value too — wirekit:doctor warns when they disagree.
Every bundled family also ships a metric-matched fallback face, which is what actually stops the page from moving when the font arrives: a local system font is registered under the family's own name with the web font's measured metrics, so the text painted before the swap occupies the same box as the text painted after it. Nothing to configure — it is in the family's CSS. For a font you host yourself, see below.
The published config defaults sans to 'inter', which loads a webfont. A
fresh wirekit:install bundles and serves Inter locally (GDPR-friendly, no
Google CDN) so the default look is consistent out of the box. If you are
optimizing for zero font files — e.g. you self-host your own typeface, or
you want the pure system stack — set 'sans' => null (and leave serif /
mono at null). Only non-null categories load font files.
Publishing
Before fonts can be loaded, publish the font files to your public directory. Publish only the families you activated — each font has its own tag:
# 1. Publish just the families named in config/wirekit.php.
# The tag is wirekit-font- followed by the font key from the tables below.
php artisan vendor:publish --tag=wirekit-font-ibm-plex-sans
php artisan vendor:publish --tag=wirekit-font-ibm-plex-mono
The whole bundled library is about 5.8 MB. A typical app activates one sans and
one mono family, which is roughly 430 KB — so publishing per family keeps the
rest out of public/ entirely. That matters more than it first looks: the
publish is usually wired into a deploy step so the files survive a fresh
composer install, which means the difference is re-copied on every deploy.
To publish every bundled family at once:
# 2. All families — convenient, but writes the full 5.8 MB.
php artisan vendor:publish --tag=wirekit-fonts
A font that is configured but never published falls back to system fonts. The page still renders, so this is easy to miss — watch for the WireKit warning in your application log, and for the explanatory HTML comment in the page source. Both appear in every environment.
Available Fonts
Sans-Serif
| Key | Font Family | Weights | Specimen |
|---|---|---|---|
roboto |
Roboto | 400, 500, 700 | View → |
open-sans |
Open Sans | 400, 500, 600, 700 | View → |
lato |
Lato | 400, 700 | View → |
inter |
Inter | 400, 500, 600, 700 | View → |
montserrat |
Montserrat | 400, 500, 600, 700 | View → |
ibm-plex-sans |
IBM Plex Sans | 400, 500, 600, 700 | View → |
noto-sans |
Noto Sans | 400, 500, 600, 700 | View → |
nunito-sans |
Nunito Sans | 400, 500, 600, 700 | View → |
dm-sans |
DM Sans | 400, 500, 600, 700 | View → |
vt323 |
VT323 | 400 | View → |
Serif
| Key | Font Family | Weights | Specimen |
|---|---|---|---|
playfair-display |
Playfair Display | 400, 500, 600, 700 | View → |
lora |
Lora | 400, 500, 600, 700 | View → |
merriweather |
Merriweather | 400, 700 | View → |
ibm-plex-serif |
IBM Plex Serif | 400, 500, 700 | View → |
noto-serif |
Noto Serif | 400, 500, 600, 700 | View → |
Monospace
| Key | Font Family | Weights | Specimen |
|---|---|---|---|
ibm-plex-mono |
IBM Plex Mono | 400, 500, 700 | View → |
roboto-mono |
Roboto Mono | 400, 500, 700 | View → |
source-code-pro |
Source Code Pro | 400, 500, 700 | View → |
jetbrains-mono |
JetBrains Mono | 400, 500, 700 | View → |
space-mono |
Space Mono | 400, 700 | View → |
google-sans-code |
Google Sans Code | 400, 500, 700 | View → |
Typography Preview
The three font-family CSS variables render like this:
The quick brown fox jumps over the lazy dog. 0123456789
Bold: The quick brown fox jumps over the lazy dog.
The quick brown fox jumps over the lazy dog. 0123456789
Bold: The quick brown fox jumps over the lazy dog.
The quick brown fox jumps over the lazy dog. 0123456789
Bold: The quick brown fox jumps over the lazy dog.
Once you configure a font (e.g. ibm-plex), the CSS variables point to the configured font family. Use them in your layout:
{{-- Sans — var(--font-wk-sans) --}}
<p style="font-family: var(--font-wk-sans); font-size: 1.125rem;">
The quick brown fox jumps over the lazy dog. 0123456789
</p>
{{-- Serif — var(--font-wk-serif) --}}
<p style="font-family: var(--font-wk-serif); font-size: 1.125rem;">
The quick brown fox jumps over the lazy dog. 0123456789
</p>
{{-- Mono — var(--font-wk-mono) --}}
<p style="font-family: var(--font-wk-mono); font-size: 1.125rem;">
The quick brown fox jumps over the lazy dog. 0123456789
</p>
CSS Variables
The component sets these CSS custom properties on :root:
| Variable | Default (no font configured) |
|---|---|
--font-wk-sans |
ui-sans-serif, system-ui, sans-serif |
--font-wk-serif |
ui-serif, Georgia, serif |
--font-wk-mono |
ui-monospace, monospace |
When a font is configured, the variable includes the font family with fallbacks:
--font-wk-sans: 'Inter', ui-sans-serif, system-ui, sans-serif;
Tailwind v4 Integration
To use WireKit fonts globally with Tailwind's font-sans, font-serif, and font-mono utilities:
@theme {
--font-sans: var(--font-wk-sans);
--font-serif: var(--font-wk-serif);
--font-mono: var(--font-wk-mono);
}
Behavior
- No font configured: CSS variables use system font stacks. No
<link>tags rendered. Zero requests. - Font configured + published:
<link>tag loads the font CSS. CSS variable set to the font family with fallbacks. - Font configured + not published (local): HTML comment warning in page source with the publish command.
- Font configured + not published (production): Silently falls back to system font stack. No crash.
- Invalid font key (local):
InvalidArgumentExceptionwith list of available presets. - Invalid font key (production): Ignored. System font stack used.
Matching a font you host yourself
Every bundled family ships with a second @font-face that registers a local system font under its own name and overrides its metrics to match. Text painted before the swap then occupies the same box as text painted after it, and the layout does not move.
A font of your own gets none of that — and that is the setup null above is for, so the gap sits exactly where the documented path leads. Measured in one application: a 400px prose column at line-height: normal was 160px tall in the web font and 144px in the fallback painted before it. A 16px jump, on every page, worst where the text is the point.
Declare the measured numbers in wirekit.fonts.fallbacks and WireKit emits the same kind of face:
// 1. config/wirekit.php — one entry per family you host yourself.
// The four numbers are placeholders on purpose: they are properties of YOUR
// font file, and no other family's values will do. Measuring them is the
// section below.
'fonts' => [
'fallbacks' => [
'Your Family' => [
'local' => ['Arial', 'Helvetica Neue', 'Liberation Sans'],
'sizeAdjust' => '___%',
'ascentOverride' => '___%',
'descentOverride' => '___%',
'lineGapOverride' => '___%',
],
],
],
/* 2. Reference the name it creates, right after your own family. */
font-family: 'Your Family', 'Your Family Fallback', sans-serif;
This example used to name a real family and give it another one's numbers — it read Instrument Sans and carried Inter's measurements, rounded. Someone whose font was Instrument Sans read that as already measured, copied it, and closed about a quarter of the gap it appeared to close. A placeholder cannot be pasted; a plausible number can.
Declare nothing and nothing is emitted. That is deliberate: a font nobody declared is a font nobody measured.
Measuring the four values
Do not estimate them. A guessed size-adjust moves the layout in the other direction and looks deliberate while doing it, which is worse than leaving the whole thing alone. Two parts of the method are easy to get wrong, and both were on the first attempt:
OS/2.xAvgCharWidthis not a usable width. Every font computes it over a different glyph set — Arial reports 0.4414em and Inter 0.6401em, which would imply Inter is 45% wider when the two are within 8% of each other. Average the same fixed characters on both faces instead, weighted by how often they occur in your prose. The check that the method is right is monospace: measured against a monospace reference, a monospace family has to land within about 2% of 100%, because "same advance width" is what monospace means.- Which ascent and descent count is not a free choice. A font that sets the
USE_TYPO_METRICSbit inOS/2.fsSelectionis asking to be read throughsTypoAscender/sTypoDescender; one that does not is read throughhhea. Reading the wrong pair builds a box the browser does not use.
size-adjust is the width ratio of your face to the local one; the three overrides are that face's ascent, descent and line gap divided by its units-per-em, then divided again by the size adjust.
Content Security Policy
This component renders one inline <style> block — the three font custom properties. It has to be inline: the values come from your wirekit.fonts configuration, so they are not knowable when the stylesheet is built.
If your policy has a nonce on style-src, this block needs it too. That is not a preference. From CSP Level 2 on, a nonce anywhere in a directive makes the browser ignore 'unsafe-inline' in that same directive, so the moment you add a nonce for any other block, an un-nonced one stops being allowed. The failure is silent: no console error, no log line — the page renders and your typography falls back to the system font.
Usually there is nothing to do. The component resolves the nonce itself, from a csp-nonce container binding or from Vite::cspNonce() — the same value Livewire reads — so an application that already has one is covered:
{{-- 1. Nothing to pass: the nonce is discovered. --}}
<x-wirekit::fonts />
Pass one explicitly when your application mints a value per response and publishes it nowhere:
{{-- 2. An explicit nonce always outranks a discovered one. --}}
<x-wirekit::fonts :nonce="$nonce" />
With no nonce anywhere, no nonce attribute is emitted — an invented value would be rejected exactly like a missing one, while hiding its absence from anyone reading the markup.
GDPR
All fonts are bundled locally in the package. No requests are made to Google Fonts or any external CDN. Font files are served from your own public/vendor/wirekit/fonts/ directory after publishing.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
nonce |
string|null |
null |
A CSP nonce for the inline <style> this component emits. Leave it unset and the component resolves one itself — from a csp-nonce container binding, or from Vite::cspNonce(), the same value Livewire reads. Pass one explicitly when your application mints a value per response and publishes it nowhere. See Content Security Policy for why an un-nonced block fails silently once any nonce exists in the same directive. |
Accessibility
- Font selection does not affect screen-reader output — semantic HTML and text content remain fully accessible regardless of which display font is loaded
- Contrast: verify the chosen font + background combination meets WCAG 1.4.3 Contrast (Minimum) — 4.5:1 for body text, 3:1 for large text (18pt+). Thin/light weights at small sizes can fail contrast even with "black" text
- Legibility at small sizes: condensed or decorative faces (Playfair Display, Space Mono) can hurt readability for dyslexic users at body-copy sizes — prefer Inter, Open Sans, or IBM Plex Sans for primary body text
font-display: swapis used by all bundled fonts — text is shown immediately using a fallback face and swapped once the web font loads. This avoids FOIT ("flash of invisible text") and keeps content accessible during slow connectionsfont-displayis a config key, not a workaround — setwirekit.fonts.displayand every bundled face is served with it. Do not write your ownfont-displayrules inapp.css: they land on the same faces and will fight the metric-matched fallbacks below, which are the thing that actually stops the shift- All fonts are served over the same origin as your app (no CORS, no third-party cookies), which keeps Content-Security-Policy simple and blocks no font loading when users disable third-party requests
Keyboard Interaction
This component is purely presentational and does not respond to keyboard input.
Design Tokens
The Fonts feature drives the typography variables every other component reads. Configuring a preset via config/wirekit.php updates these variables at the :root level so every WireKit component picks up the new face automatically.
| Token | Used for |
|---|---|
--font-wk-sans |
Sans-serif stack — body text on every component |
--font-wk-serif |
Serif stack — typeset content (Prose, hero headlines) |
--font-wk-mono |
Monospace stack — Code, Code Block, Kbd, hex value displays |
--font-wk-body-weight |
Default body weight |
--font-wk-heading-weight |
Default heading weight |
--font-wk-letter-spacing |
Default letter spacing |
--font-wk-line-height |
Default line height |
Each preset listed in Available Fonts ships with font-display: swap and is served same-origin (no third-party cookies, no CORS).