Docs

Preview widgets

One script and one tag show a parameter set live on any page, drawn in your visitor's browser by Typomat's engine. It's display-only, needs no key, and is free on every plan.

Quickstart

HTML
<script src="https://engine.typomat.app/typomat/widget.js" async></script>

<typomat-preview
  text="Acme Studio"
  parameters='{"weight":120,"contrast":0.4,"serif":0.6}'>
</typomat-preview>

That's a line of “Acme Studio” in those numbers, with “Powered by Typomat” under it. The widget is as wide as its container and sets its own height. It draws all 95 parameters and all 271 characters with the same drawing code as the API, so what it shows is what the font would look like. See it working on the widgets page.

Values go one way: from your page into the widget. It never hands back parameters, typed text or a font, only a status.

React, Next.js and Vue

"use client";
import Script from "next/script";
import { useState } from "react";

export function HeadingFont() {
  const [weight, setWeight] = useState(120);
  const parameters = { weight, contrast: 0.4, serif: 0.6 };

  return (
    <>
      <Script src="https://engine.typomat.app/typomat/widget.js" />
      <typomat-preview text="Acme Studio" parameters={JSON.stringify(parameters)} />
      <input type="range" min={12} max={210} value={weight} onChange={(e) => setWeight(Number(e.target.value))} />
    </>
  );
}

Load the script once, in your root layout or on the pages with a widget. In React and Vue, pass parameters as a JSON string. React 19 sets it as the element's parameters property once the script has loaded, and as the attribute before that and in server rendering, and the widget reads both. An object would be lost in server rendering. The same goes for theme. Vue needs to be told the element isn't one of its components.

Every change redraws at once: there's no need to debounce a slider. In TypeScript, declare the element for JSX:

TypeScript
// typomat-preview.d.ts
import "react";

type TypomatPreviewAttributes = Partial<Record<
  "variant" | "show" | "show-request" | "text" | "size" | "sizes"
  | "groups" | "paragraph" | "lines" | "align" | "editable" | "placeholder"
  | "controls" | "parameters" | "name" | "label" | "theme" | "mode"
  | "background" | "ink" | "accent" | "muted" | "font" | "radius"
  | "padding",
  string | number | boolean
>> & { [param: `param-${string}`]: string | number };

declare module "react" {
  namespace JSX {
    interface IntrinsicElements {
      "typomat-preview": React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement> & TypomatPreviewAttributes;
    }
  }
}

Choosing what shows

variant picks a preset: mini, the default, is one line, and full is a specimen. show lists the pieces yourself, top to bottom, and wins over variant. Live examples of each are on the widgets page.

HTML
<!-- One line: the default, variant="mini" -->
<typomat-preview text="Acme Studio"></typomat-preview>

<!-- A specimen: the line, the characters, a paragraph, the parameters and the request -->
<typomat-preview variant="full" show-request name="Acme Display"></typomat-preview>

<!-- Your own pieces, in your order -->
<typomat-preview show="line waterfall" sizes="12 18 28 44 68" text="Harbour Lights"></typomat-preview>
<typomat-preview show="grid" groups="figures"></typomat-preview>
<typomat-preview show="paragraph parameters" lines="6" align="center"></typomat-preview>
PieceShowsShaped by
lineOne line of text, as large as size and the width allow.text, size, align, editable, placeholder, controls
waterfalltext at several sizes, a row each. A row too wide for the widget is drawn as large as fits, never cut.text, sizes, align
gridEvery character, a group at a time, with tabs to switch.groups
paragraphA wrapped paragraph, a fixed number of lines tall.paragraph, lines, align, editable
parametersThe parameters as the engine reads them: known names only, clamped to their ranges.Nothing
requestThe JSON body POST /api/v1/fonts would get for them, with a Copy button.name

The grid's groups, for groups: letters (59), figures (54: figures, punctuation and symbols), accented (68) and extended (90). Together they're all 271.

When the widget is at least 640 pixels wide and shows parameters or request beside other pieces, those two sit in a column on the right. Narrower, everything stacks in your order. Every widget ends with one line: notes on anything it couldn't show, and the mark.

Heights don't move. Each piece's height comes from the widget's width and your attributes, never from the text, the parameters or what a visitor types, so nothing jumps while a slider drags. The grid is as tall as its largest group, so switching tabs moves nothing either.

Letting visitors type

HTML
<typomat-preview show="line paragraph" editable="line paragraph" placeholder="Type your name"></typomat-preview>
  • editable on its own makes the line a text field. editable="paragraph" or "line paragraph" does the paragraph too. They're real fields, so keyboards, phones, screen readers, selection, undo and paste work as they do anywhere.
  • Without a placeholder, the line starts with your text, all selected on first focus so typing replaces it. With one, it starts empty and shows the placeholder, muted, until the first keystroke.
  • Typed text is plain: line breaks and tabs become spaces, pasting takes plain text only, dropping text in is off, and it stops at 120 characters in the line and 600 in the paragraph.
  • What a visitor types stays in the widget. It goes to the engine in their own browser to be drawn, and nowhere else: not to your page, not to a server, and not into the status event, which doesn't even report its missing characters. It isn't stored, so a reload starts over.
  • Your page still wins. A new text, placeholder or paragraph from your page replaces what was typed.

Viewer controls

HTML
<typomat-preview controls="size theme"></typomat-preview>
  • size adds a slider that shrinks the line, from its full size down to a quarter of it. The line's box keeps its height.
  • theme adds a button that switches the widget between light and dark. The other mode uses its own defaults, and your dark colours when it's dark.
  • Both belong to the visitor: they change nothing on your page, aren't sent to it, and reset on reload.

Theming and white-label

HTML
<typomat-preview
  mode="auto"
  background="#F6F3EE" ink="#1B1B1B" accent="#E22400" muted="#77716A"
  font="Georgia, serif" radius="14" padding="20"
  theme='{"dark":{"background":"#111318","ink":"#F2F2F2","accent":"#7CC4FF","muted":"#8A8F98"}}'>
</typomat-preview>

Each option is an attribute of its own, or a key of the theme attribute's JSON or the theme property. The attributes are laid over the JSON, and the property over both. The reference lists every option and default.

Colours

background, ink, accent and muted take #rgb, #rgba, #rrggbb or #rrggbbaa, rgb(), rgba(), hsl() or hsla() with plain numbers, or transparent. Anything else, colour names, var() and calc() included, is ignored, and the default is used. With background="transparent" and padding="0" the widget sits on your page as part of it.

Light, dark and auto

  • light, the default: the light defaults, under your colours.
  • dark: the dark defaults, under your colours, then under dark.
  • auto: follows the visitor's system setting, and changes with it. Light, it's as light. Dark, it uses the dark defaults under dark alone, and leaves your other colours out: colours picked for a light page rarely work on a dark one. So with auto, give dark as well.

Label fonts

font sets the labels, tabs and notes. The letters themselves are always drawn by the engine. The widget runs in its own frame, so it only sees fonts installed on the visitor's device and generic families such as serif or system-ui: your page's web fonts aren't visible there. Give up to six names of letters, digits, spaces, dashes and underscores, with or without quotes. If one name has anything else in it, the whole list is ignored and Typomat's own UI font is used.

The mark

Every widget shows “Powered by Typomat” at the bottom right, linking to engine.typomat.app in a new tab. It's set in Typomat's own display font whatever font says, in your ink colour at 70% opacity. No attribute or theme option hides it. White-label customers get their widget without it, as part of their bespoke setup: White-label.

Parameters

There are three ways to set them, laid over each other in this order, so your script has the last word:

HTML
<typomat-preview parameters='{"weight":120,"contrast":0.4}' param-serif="0.6"></typomat-preview>

<script>
  document.querySelector("typomat-preview").parameters = { weight: 150 };
  // Drawn: weight 150, contrast 0.4, serif 0.6
</script>
  1. The parameters attribute: a JSON object.
  2. A param- attribute per parameter. Names ignore case and dashes, as HTML lowercases attribute names: param-x-height and param-xHeight are both xHeight.
  3. The parameters property: an object, or a JSON string. Setting the parameters attribute again drops it.

The names, ranges and defaults are the engine's own: see Parameters. Every one is drawn, the 23 style parameters included.

  • A value outside its range is clamped to it, as the API does.
  • A value can be a number or a string of one, such as "120": attributes are strings.
  • A name that isn't a parameter is left out, reported in unknown, and named in the widget's note: “Not Typomat parameters, left out: boldness.”
  • A value that isn't a number, such as "heavy" or "0.5px", is left out, reported in invalid, and noted too.

And the Font API

The widget's parameters are the parameters of POST /api/v1/fonts: the same names, the same ranges, the same letters. Keep one object and give it to both. The request piece shows the body the API would get, with name, for your users to read or copy:

request
{
  "name": "Acme Display",
  "parameters": {
    "weight": 120,
    "contrast": 0.4
  }
}

The widget is more forgiving than the API. Where it leaves out an unknown name or a value that isn't a number, the API answers 400, so watch the status to catch them before you send. The request piece only ever holds what the API accepts, by the engine's own names, clamped. Showing a widget never calls the API and never counts against your plan.

Events and status

JavaScript
preview.addEventListener("typomat-status", (event) => {
  const { unknown, invalid, missing, ignored } = event.detail;
  if (unknown.length || invalid.length) console.warn("Left out:", unknown, invalid);
});
FieldLists
unknownParameter names that aren't Typomat's. Left out of the drawing and the request.
invalidParameters whose value isn't a number. Left out. If the parameters attribute is the text [object Object], this starts with a hint: pass JSON.stringify(...) or set the property.
missingCharacters of your own text and paragraph that Typomat has no drawing for. Never anything a visitor typed.
ignoredWords in show, groups, editable, controls or align that mean nothing to the widget.

The widget sends a status once it has your first settings, and again whenever it changes. missing fills in once the engine has drawn your text. The event is dispatched on the element and doesn't bubble, so listen on the element itself. It carries nothing else.

There's no ready event to wait for. Attributes and properties set before the script loads, or while the widget is still loading, are sent as soon as it's ready, and changes made in the same moment go together.

The widget sets its own height: 64 pixels while it loads (520 with variant="full"), then exactly as tall as it is, with no scrollbar. After that it changes only when its width does. To keep your page from shifting as it loads, give the element a min-height.

Security and privacy

Its own frame
The widget runs in a frame from engine.typomat.app, in a sandbox: it can run its own scripts and open the mark's link in a new tab, but it can't read or change your page, navigate it, submit forms or start downloads. The frame's page may only load files from Typomat's own hosts.
Locked messages
The loader sends your settings only to engine.typomat.app, and only listens to its own frame. The frame only listens to the page that embeds it, and once your page has sent its settings, answers only your page's origin. A message over 32,000 characters is dropped.
Checked values
Everything sent in is checked before it's used: parameters against the engine's catalog, text made plain and cut to length, colours and fonts matched against fixed forms and set one by one. Nothing you or a visitor sends becomes HTML, CSS or code.
No tracking
The widget sets and reads no cookies, uses no browser storage, and has no analytics or third-party scripts. Its files come from engine.typomat.app, which sees the requests for them as for any embedded file. Nothing about the font goes in the frame's address.
Display-only
The only messages the widget sends your page are its height and the status. It never sends parameters, typed text or a font.
Nothing to take
The widget's engine is a preview edition: it draws every letter, and it was built without the code that writes fonts. There's no download, export or font file in it. Someone who saves its files gets outlines of letters, which a screenshot gives too, and copied to another site it only draws in part.

Content Security Policy

If your page sends a Content-Security-Policy, add the script and the frame to your own directives. That's all it needs:

Content-Security-Policy
script-src https://engine.typomat.app;
frame-src https://engine.typomat.app;

Without frame-src (or a child-src or default-src that allows it), the browser refuses the frame and the widget stays empty. Nothing else is needed, not even under a strict style-src: the loader adds no style text to your page, and the widget's engine, worker and fonts load inside its own frame, under engine.typomat.app's policy.

Performance

  • The loader is about 10 KB, 3 KB compressed, and is cached for five minutes, so fixes reach your page quickly.
  • The widget's engine is about 7.5 MB, or 1.9 MB as it travels compressed, and the browser caches it for a day. The first widget a visitor sees downloads it; the rest load it from the cache.
  • Widgets load lazily: one far down the page starts loading when the visitor scrolls near it.
  • Each widget is its own frame, with its own engine in memory, drawing on its own. Ten widgets on a page are ten engines. One widget with several pieces costs less than several widgets with one each.
  • Each picture is more drawing: the line or waterfall, the grid and the paragraph each ask the engine for their characters, while parameters and request are text and cost nothing. Show what you need.
  • The engine draws the newest numbers first, and the line before the rest, so a dragged slider stays live. Most styles draw in milliseconds. The heaviest take longer, script about 0.3 s for the 90 extended characters, and the grid catches up after the line.

Troubleshooting

Nothing shows
The script didn't load or the frame was refused. The browser's console names the reason: a policy error names script-src or frame-src (see above). Until the script runs, the element shows what you put inside it, and hides it once the widget is in place, so it's a good spot for a fallback.
“The preview couldn't load.”
The engine couldn't start in the visitor's browser: they're offline, or the browser has no WebAssembly or workers. A reload fixes the first.
“Shown in part”
The widget isn't running from engine.typomat.app. Load the script from https://engine.typomat.app/typomat/widget.js, not a copy: the engine only draws in full from there.
“Not a Typomat character”
The engine has 271 characters: Latin with accents, figures, punctuation and symbols. Others are left blank, named in the note, and listed in missing when they're in your own text.
“Not Typomat parameters” or “Not a number”
A name that isn't a parameter (check it against Parameters), or a value that isn't a number. They're listed in unknown and invalid.
The label font doesn't change
The font isn't installed on the visitor's device (your page's web fonts aren't visible in the frame), or a name has a character other than letters, digits, spaces, dashes and underscores, which drops the whole list.
A colour doesn't apply
Use a hex, rgb() or hsl() value: names like red are ignored. With mode="auto" on a dark system, only the dark colours apply.
The height looks wrong
Don't set a height on the element: the widget sets its own, and only changes it when its width changes. Use min-height to keep room while it loads.
React changes don't show
Pass parameters and theme as JSON strings, with JSON.stringify. An object is dropped in server rendering, and becomes the text “[object Object]” if it's set before the script loads. For parameters, the widget says so in its note, and the status's invalid starts with the hint “pass JSON.stringify(...) or set the property”.
A property was undone
Setting the parameters, theme or show attribute drops the property of the same name, so the attribute shows. Use one or the other.

Every attribute, property and theme option is in the widget reference.