Blog
typography

Variable Font Editor: 40+ OpenType Feature Toggles (Build Guide)

How I built a variable font editor with CSS font-feature-settings, 40+ OpenType toggles, and Preact for real-time typography exploration.

Mladen Ruzicic
Mladen Ruzicic
7 min

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:

  1. Browsers: Scanning font lists, comparing visual appearance
  2. 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:

  1. Default: Let the browser decide (inherit from user agent)
  2. On: Explicitly enable the feature
  3. 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 Ligatures
  • clig - Contextual Ligatures
  • hlig - Historical Ligatures

Figures

  • lnum - Lining Figures (1234)
  • onum - Oldstyle Figures (with descenders)
  • pnum - Proportional Figures
  • tnum - Tabular Figures (monospaced)

Capitals

  • smcp - Small Caps
  • c2sc - Caps to Small Caps
  • pcap - Petite Caps
  • titl - Titling Capitals

Stylistic

  • salt - Stylistic Alternates
  • ss01 through ss20 - Stylistic Sets
  • swsh - Swash
  • cswh - Contextual Swash

Spacing

  • kern - Kerning
  • cpsp - Capital Spacing
  • case - 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:

  1. Shareable previews: Send a link showing exact settings
  2. Bookmarking: Save configurations for later
  3. 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.

Explore on FontAlternatives

#typography#opentype#variable-fonts#preact#ux

More from the blog