Font Editor
Edit the CSS font shorthand β the most order-sensitive of the common shorthands β with a compile-time STRICT-ORDER PARSE. Order-free prefix tokens (style, variant, weight, stretch β each at most once), then a mandatory size, an optional / line-height, then a mandatory comma-separated family list. Or a system-font keyword as the whole value. 16px β never (no family); italic oblique 16px serif β never (two styles). The strict tier resolves any order or structure violation to never.
Build a font shorthand
Controlled value + onChange. Set the optional prefix (style/variant/weight/stretch), the mandatory size and family, and the optional line-height β the editor emits a valid, canonically-ordered font string.
italic 600 18px/1.4 'Inter', sans-serifSee the shorthand on real text
Every control β style, variant, weight, stretch, size, line-height, and the family list (web-safe + generic) β composes the typed font string. The preview below renders sample text with exactly that shorthand applied.
font: italic small-caps 500 18px/1.6 Georgia, serif;Three usage tiers
From useState-and-go to a full ordered-grammar parse at compile time.
Pass any string
useState<string>. No compile-time validation; the runtime parser handles whatever you type β including var() family tokens.
16px var(--brand-stack), serifconst [value, setValue] = useState<string>("16px var(--brand-stack), serif")
Suggestion-shaped hints
State typed as FontString β the system-font keywords plus any space-containing shorthand. It is also the onChange return type. Precise validation lives in the strict tier.
bold 16px sans-serifconst [value, setValue] = useState<FontString>("bold 16px sans-serif")
The ordered grammar at compile time
cssFont() parses the prefix, size, optional line-height and family in order and resolves a missing size, missing family, or duplicate prefix kind to never β a type error before you run the code.
italic bold 16px/1.5 'Inter', sans-serifcssFont("italic bold 16px/1.5 serif") // β // @ts-expect-error no font-family cssFont("16px") // @ts-expect-error two style tokens cssFont("italic oblique 16px serif")
Note: var() / calc() are undecidable at compile time β use the casual or IntelliSense tier for those.
API
Public surface β component props, runtime helpers, and the type exports.
Β§ FontEditor / FontEditorPanel
<FontEditor
value: FontString | (string & {})
onChange: (next: FontString) => void
className?: string
aria-label?: string
/>FontEditor is popover-wrapped; FontEditorPanel renders the same editor inline. Both are controlled.
| Prop | Type | Description |
|---|---|---|
| value | FontString | (string & {}) | Current CSS font value. Required. A shorthand or a system-font keyword. |
| onChange | (next: FontString) => void | Fires when the edited font changes. Emits the canonically-ordered string (or the system keyword). |
Β§ Sub-components
<FontPreview value sampleText? editable? />
The live text preview β renders sample text with the built `font` shorthand applied inline. Editable sample text by default; pass sampleText for a fixed string.
<FamilyEditor value onChange />
The comma-separated family-list editor: edit/remove tokens and add from a web-safe + generic family list. Emits string[].
<PropertyField label>{control}</PropertyField>A labelled control row used by the panel; exported for composition.
Β§ Runtime helpers
cssFont<S extends string>(value: S & FontLiteral<S>): S
Call-site validator. Mirrors cssTransform() / color() / easing().
parseFont(src: string): FontParts | null
String β typed parts, or null on missing size / missing family / duplicate prefix kind / junk. A system keyword β { kind: 'system' }. Tolerates var() family tokens.
formatFont(parts: FontParts): string
Canonical re-serialization: style variant weight stretch size[/lh] family. System keyword β itself.
fontFamilies(src: string): string[]
Runtime mirror of FamiliesOf β the comma-separated family tokens in order.
defaultParts(): FontParts
Seed a fresh editor: { kind: 'shorthand', size: '16px', family: ['sans-serif'] }.
classifyStyle / classifyWeight / classifySize / classifyFamilyToken / β¦
Per-slot token classifiers (runtime mirror of the type predicates) plus the option-list constants that drive the UI.
Β§ Types
- FontLiteral<S>
- Strict validator β S if the ordered grammar holds (prefix β€1 each β size β /lh? β family), else never. System keywords pass.
- FontString
- Suggestion union: system keywords + any space-containing shorthand. Also the onChange return type.
- FontStringMap / FontStringKey
- Representative output-string shapes and the key union.
- SystemFontKeyword / FontGenericFamily
- The system-font keywords and the generic font-family keywords.
- IsSystemFont<S>
- true when S is a system-font keyword.
- FamiliesOf<S>
- Tuple of the comma-separated family tokens.
- SizeOf<S> / LineHeightOf<S>
- The size token, and the line-height token (never when absent).
- FontParts
- Discriminated-union state (system | shorthand), exported for advanced use.
Β§ Strict-tier scope
- Full ordered parse: an order-free prefix (
style/variant/weight/stretch, each at most once) β mandatory<size>β optional/ <line-height>(attached16px/1.5and every spaced form) β mandatory comma-separated<font-family>list. System keywords (captionβ¦status-bar) pass as a whole value. - Rejects (β
never): missing size, missing family, duplicate prefix kind, line-height without a size, empty / garbage. - Weak-validated (documented): bare
<custom-ident>family names are accepted as ident-safe (letters/digits/-/_/spaces; quoted strings accept any content). The<number>weight range 1β1000 is not bound-checked. - A leading
normalconsumes the first free prefix kind β sound for acceptance (CSS treats prefixnormalas a no-op).oblique <angle>is deferred (only bareobliquevalidates strictly). var()/calc()resolve toneverat the strict tier (undecidable). The runtime parser accepts them β use the casual / IntelliSense tier.
Drop it in
One command via the shadcn CLI.
$ pnpm dlx shadcn@latest add https://turtiesocks.github.io/ridiculous/r/font-editor.json