Fonts
The <x-wirekit:: 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/:
'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.
Why Inter and not something with more character: it is open-licensed, so it can be
bundled and self-hosted rather than fetched from a CDN, and its metrics are published
and stable, which is what makes the metric-matched fallback above possible at all. It
is also the safest default to leave — and the least distinctive one to keep. If your
product has a voice, one of the other twenty families will carry it better, and
switching is one config line. 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-sans), 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 — one per configured category, and
nothing for the others. An unconfigured category is left to the stylesheet, which already
declares it:
| Variable | Value with no font configured | Declared by |
|---|---|---|
--font-wk-sans |
Inter, ui-sans-serif, system-ui, -apple-system, sans-serif |
dist/wirekit.css |
--font-wk-serif |
ui-serif, Georgia, serif |
this component |
--font-wk-mono |
ui-monospace, 'Fira Code', 'Cascadia Code', monospace |
dist/wirekit.css |
With no font configured, the component declares only the serif stack, which dist/
does not. The sans and mono stacks are the stylesheet's own, so they read the same whether the
component comes before or after @wirekitStyles.
When a font is configured, the variable names three things in order — the web font, its metric-matched local face, and the generic stack:
--font-wk-sans: 'Inter', 'Inter Fallback', ui-sans-serif, system-ui, sans-serif;
The middle entry is the one worth not dropping. 'Inter Fallback' is a local @font-face
built with size-adjust and ascent/descent overrides so it occupies the same space as Inter —
it is what keeps the page from reflowing when the web font arrives, and a hand-written copy of
this line that omits it re-introduces the shift the preset exists to prevent.
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);
}
Browser support for the fallback faces
Each bundled family ships a metric-matched local face — a @font-face that registers a font
you already have under a WireKit name, carrying four descriptors that make it occupy the same
box as the web font: size-adjust, ascent-override, descent-override and
line-gap-override. They are what stops the page reflowing when the web font finishes
downloading.
None of the four can be feature-detected: @supports tests properties, and these are
@font-face descriptors. Chromium-based browsers and Firefox have had all four since version
92. Safari is the exception: size-adjust arrived in Safari 17, and no Safari release has
the three overrides yet. So Safari keeps the local face's own line metrics, which text set with
line-height: normal still shows. The width is matched from 17 on, and the width is what decides
where a paragraph with a numeric line height wraps, so from Safari 17 on such a paragraph wraps
close to the web font's lines and comes out nearer its height than the same local face without
the descriptors. Before 17 the local face keeps its natural size as well, which is the shift the
fallback exists to remove: the text is complete and readable from the first paint and moves
once when the web font arrives. Nothing else depends on the descriptors.
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):
Invalidwith list of available presets.Argument Exception - 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 by default, and that is the setup null above is for. Without a matched fallback, a column of prose painted in the fallback before the swap can be a line or more shorter or taller than the same text in your web font, and the page jumps when the font arrives, on every page, worst where the text is the point.
Declare the measured numbers in wirekit. 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;
The placeholders are deliberate. A real family's name beside plausible numbers invites copying, and another family's metrics pasted for your own close only part of the gap they appear 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_bit inTYPO_ METRICS OS/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. Nothing on the page says so: it renders, and your typography falls back to the system font. The only report is the browser's own, a CSP violation in the console and a securitypolicyviolation event, and no test of yours turns red from it.
Usually there is nothing to do. The component resolves the nonce itself, from a csp-nonce container binding or from Vite:: — 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/ directory after publishing. Until then a package route serves the family from WireKit itself, so an unpublished font still loads; the asset routes section covers its caching and middleware.
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 is refused once any nonce exists in the same directive, with nothing on the page to show it. |
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 the DEFAULT for every bundled font, and it is a config key rather than a fact —fonts.displaytakesoptional,block,fallbackandautotoo, and the paragraph above says what each trades. On the default, text is shown immediately in a fallback face and swapped once the web font loads, which avoids FOIT ("flash of invisible text") and keeps content readable on a slow connectionfont-displayis a config key, not a workaround — setwirekit.and every bundled face is served with it. Do not write your ownfonts. display font-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/ 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 is served same-origin (no third-party cookies, no CORS) and ships with font-display: swap unless fonts.display says otherwise — the key is configurable, and optional is the setting to reach for when a swap-induced reflow matters more than seeing the face.