Most font preview tools show you “The quick brown fox” in different sizes. That’s not how anyone actually uses fonts.
I built a font editor that lets users see fonts in real contexts: articles, landing pages, pricing tables, and dashboards. With 40+ OpenType feature toggles and shareable URLs.
The problem
Users visiting FontAlternatives fall into two groups:
- Browsers: Scanning font lists, comparing visual appearance
- Buyers: Evaluating a specific font for their project
Group 1 is served by good thumbnails. Group 2 needs more.
When I’m evaluating a font for a project, I want to see:
- How it looks at my actual font sizes (not 48px hero text)
- How it handles long paragraphs (not three-word samples)
- How tabular figures align in a pricing table
- How small caps work in navigation
No font preview tool gave me this. So I built it.
The PRO mode concept
The font editor has two modes:
Basic mode (default): Simple text input with font preview. Works on all devices.
PRO mode: Full typography toolkit with templates, axis controls, and OpenType features. Desktop only (disabled below 640px).
Why disable on mobile? OpenType controls need precision. Trying to adjust 8 variable font axes on a 375px screen creates a frustrating experience. Better to offer a simpler tool than a broken one.
Variable font axes
Variable fonts pack multiple weights, widths, and styles into a single file. The font editor exposes all available axes:
interface VariableFontAxis {
tag: string; // 'wght', 'wdth', 'slnt', etc.
name: string; // 'Weight', 'Width', 'Slant'
min: number;
max: number;
default: number;
}
The UI renders a slider for each axis, pulling min/max/default from the font’s metadata. When users adjust an axis, the CSS font-variation-settings updates:
.preview-text {
font-variation-settings:
"wght" 450,
"wdth" 87.5,
"slnt" -8;
}
For standard axes (wght, wdth, ital, slnt, opsz), I use the CSS properties directly (font-weight, font-stretch, etc.) since they have better browser support.
OpenType features: the three-state toggle
OpenType features are binary (on/off), but CSS font-feature-settings has three states:
- Default: Let the browser decide (inherit from user agent)
- On: Explicitly enable the feature
- Off: Explicitly disable the feature
Most UIs use two-state toggles. I use three-state:
type FeatureState = 'default' | 'on' | 'off';
interface OpenTypeToggle {
tag: string; // 'smcp', 'onum', 'liga', etc.
name: string; // 'Small Caps', 'Oldstyle Figures'
state: FeatureState;
}
Why three states? Some fonts ship with features enabled by default (like standard ligatures). A two-state toggle would force users to explicitly turn them off, then back on. Three states let the browser do its thing unless you specifically want to override.
The UI uses a segmented control:
[Default] [On] [Off]
When generating CSS, I only include features that aren’t in ‘default’ state:
function generateFeatureSettings(features: OpenTypeToggle[]): string {
const active = features.filter(f => f.state !== 'default');
if (active.length === 0) return 'normal';
return active
.map(f => `"${f.tag}" ${f.state === 'on' ? 1 : 0}`)
.join(', ');
}
The 40+ features
I group OpenType features by category:
Ligatures
liga- Standard Ligatures (fi, fl, ff)dlig- Discretionary Ligaturesclig- Contextual Ligatureshlig- Historical Ligatures
Figures
lnum- Lining Figures (1234)onum- Oldstyle Figures (with descenders)pnum- Proportional Figurestnum- Tabular Figures (monospaced)
Capitals
smcp- Small Capsc2sc- Caps to Small Capspcap- Petite Capstitl- Titling Capitals
Stylistic
salt- Stylistic Alternatesss01throughss20- Stylistic Setsswsh- Swashcswh- Contextual Swash
Spacing
kern- Kerningcpsp- Capital Spacingcase- Case-Sensitive Forms
The editor detects which features a font actually supports (via the font.featureSettings API) and only shows relevant toggles.
Design templates
This is where PRO mode gets practical. Instead of lorem ipsum, I provide real HTML structures:
Article template: Heading hierarchy, body text, blockquotes, code blocks Landing page template: Hero section, feature grid, CTA buttons Pricing table template: Plan cards with prices, feature lists Dashboard template: Stats cards, data tables, navigation
Each template is semantic HTML with utility classes:
<!-- Pricing table template -->
<div class="pricing-card">
<h3 class="plan-name">Professional</h3>
<div class="price">
<span class="currency">$</span>
<span class="amount">49</span>
<span class="period">/month</span>
</div>
<ul class="features">
<li>10,000 API calls</li>
<li>Priority support</li>
<li>Custom domains</li>
</ul>
<button class="cta">Get Started</button>
</div>
Users can test tabular figures on the price, small caps on the plan name, and different weights throughout.
URL state synchronization
Every editor adjustment syncs to the URL. This enables:
- Shareable previews: Send a link showing exact settings
- Bookmarking: Save configurations for later
- Back button: Undo changes naturally
The URL structure (using Inter as an example):
/fonts/inter/?
text=Custom+text&
size=18&
weight=450&
features=smcp,tnum&
template=pricing
I use Preact signals for state management with URL sync:
import { signal, effect } from '@preact/signals';
const editorState = signal({
text: 'Preview text',
size: 16,
weight: 400,
features: new Map<string, FeatureState>(),
template: null,
});
// Sync state to URL
effect(() => {
const params = new URLSearchParams();
const state = editorState.value;
if (state.text !== defaultText) {
params.set('text', state.text);
}
// ... other params
const url = `${location.pathname}?${params}`;
history.replaceState(null, '', url);
});
// Parse URL on load
function initFromURL() {
const params = new URLSearchParams(location.search);
// ... restore state
}
The CSS output panel
Users don’t just preview; they need to use the font. The editor generates copy-paste CSS:
/* Generated CSS */
font-family: 'Inter', sans-serif;
font-size: 18px;
font-weight: 450;
font-feature-settings: "smcp" 1, "tnum" 1;
For variable fonts, it includes both the font-variation-settings for broad support and individual properties for modern browsers:
/* Variable font settings */
font-weight: 450;
font-stretch: 87.5%;
font-variation-settings: "wght" 450, "wdth" 87.5;
Performance considerations
The font editor is heavy. It includes:
- Multiple Preact components
- OpenType feature detection
- Template HTML
- CSS generation logic
I lazy-load the entire PRO mode:
const ProEditor = lazy(() => import('./ProEditor'));
function FontEditor({ font }) {
const [proMode, setProMode] = useState(false);
return (
<div>
<BasicEditor font={font} />
{proMode && (
<Suspense fallback={<ProEditorSkeleton />}>
<ProEditor font={font} />
</Suspense>
)}
<button onClick={() => setProMode(!proMode)}>
{proMode ? 'Simple mode' : 'PRO mode'}
</button>
</div>
);
}
The basic editor loads instantly. PRO mode loads on demand, typically under 50KB gzipped.
Tradeoffs
What I gained:
- Users can evaluate fonts in realistic contexts
- Shareable URLs reduce support questions (“what settings are you using?”)
- OpenType features become discoverable (most users don’t know they exist)
What I lost:
- Complexity: 2,000+ lines of editor code
- Mobile support: PRO mode disabled on small screens
- Maintenance: Font APIs change, features break
Design decisions I’d reconsider:
- Three-state toggles confuse some users. Two-state with “reset to default” button might be clearer.
- Template HTML is brittle. A more structured template system would be easier to maintain.
Impact
Since launching PRO mode:
- Average time on font pages increased from 45s to 2m 30s
- “Copy CSS” button clicked on 34% of PRO mode sessions
- Feature toggle usage: tabular figures (41%), small caps (28%), oldstyle figures (22%)
The tabular figures stat surprised me. Turns out lots of users are building pricing pages and dashboards. The template approach was the right call.