Skip to content

Web Components

Tegaki has a <tegaki-renderer> custom element that works in any browser, without a framework.

npm install tegaki

Register the custom element once, before using it in HTML:

import { registerTegakiElement, TegakiEngine } from 'tegaki/wc';
import caveat from 'tegaki/fonts/caveat';

// Register the font bundle so the element can find it by name
TegakiEngine.registerBundle(caveat);

// Register the <tegaki-renderer> custom element
registerTegakiElement();

You can use Tegaki directly from a CDN without any build tools or package manager. Add a <script> tag to any HTML page.

esm.sh serves npm packages as ES modules, with their dependencies bundled:

<tegaki-renderer font="Caveat" text="Hello from a CDN!" loop style="font-size: 48px">
</tegaki-renderer>

<script type="module">
  import { registerTegakiElement, TegakiEngine } from 'https://esm.sh/tegaki/wc';
  import caveat from 'https://esm.sh/tegaki/fonts/caveat';

  TegakiEngine.registerBundle(caveat);
  registerTegakiElement();
</script>

To pin a specific version, add it to the URL:

import { registerTegakiElement, TegakiEngine } from 'https://esm.sh/[email protected]/wc';
import caveat from 'https://esm.sh/[email protected]/fonts/caveat';

jsDelivr can serve package files directly via its ESM endpoint:

<script type="module">
  import { registerTegakiElement, TegakiEngine } from 'https://esm.run/tegaki/wc';
  import caveat from 'https://esm.run/tegaki/fonts/caveat';

  TegakiEngine.registerBundle(caveat);
  registerTegakiElement();
</script>

For cleaner imports, use an import map to map bare specifiers to CDN URLs:

<script type="importmap">
{
  "imports": {
    "tegaki/wc": "https://esm.sh/tegaki/wc",
    "tegaki/fonts/caveat": "https://esm.sh/tegaki/fonts/caveat"
  }
}
</script>

<tegaki-renderer font="Caveat" text="Hello World" loop style="font-size: 48px">
</tegaki-renderer>

<script type="module">
  import { registerTegakiElement, TegakiEngine } from 'tegaki/wc';
  import caveat from 'tegaki/fonts/caveat';

  TegakiEngine.registerBundle(caveat);
  registerTegakiElement();
</script>

This lets you write the same import paths you’d use with a bundler, while loading from the CDN at runtime.

Loading font data manually with createBundle

Section titled “Loading font data manually with createBundle”

If the pre-built font bundles don’t work in your environment, or you want to load font data from a custom URL, use createBundle to assemble a bundle from its parts:

<script type="module">
  import { registerTegakiElement, TegakiEngine, createBundle } from 'https://esm.sh/tegaki/wc';

  const caveat = createBundle({
    family: 'Caveat',
    fontUrl: 'https://cdn.jsdelivr.net/npm/tegaki/fonts/caveat/caveat.ttf',
    glyphData: await fetch('https://cdn.jsdelivr.net/npm/tegaki/fonts/caveat/glyphData.json')
      .then(r => r.json()),
    unitsPerEm: 1000,
    ascender: 960,
    descender: -300,
  });

  TegakiEngine.registerBundle(caveat);
  registerTegakiElement();
</script>

This is also useful for loading custom font bundles generated with the studio.

A complete HTML file you can save and open in a browser:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Tegaki CDN Example</title>
</head>
<body>
  <tegaki-renderer font="Caveat" text="Handwriting from a CDN!" loop style="font-size: 64px">
  </tegaki-renderer>

  <script type="module">
    import { registerTegakiElement, TegakiEngine } from 'https://esm.sh/tegaki/wc';
    import caveat from 'https://esm.sh/tegaki/fonts/caveat';

    TegakiEngine.registerBundle(caveat);
    registerTegakiElement();
  </script>
</body>
</html>
<tegaki-renderer font="Caveat" text="Hello World" style="font-size: 48px">
</tegaki-renderer>

The element uses Shadow DOM internally. Style the host element with font-size and color as usual; the renderer inherits them.

You can set the text via the text attribute or as the element’s text content:

<!-- Via attribute -->
<tegaki-renderer font="Caveat" text="Hello World"></tegaki-renderer>

<!-- Via text content (attribute takes priority if both are set) -->
<tegaki-renderer font="Caveat">Hello World</tegaki-renderer>

Set the time attribute to a number to control the animation manually:

<tegaki-renderer id="tegaki" font="Caveat" text="Scrub me!" time="0">
</tegaki-renderer>

<input type="range" min="0" max="10" step="0.01" value="0"
  oninput="document.getElementById('tegaki').setAttribute('time', this.value)" />
Drag the slider to scrub through the animation

Control uncontrolled playback via attributes:

<!-- Half speed, looping -->
<tegaki-renderer font="Caveat" text="Slow loop" speed="0.5" loop>
</tegaki-renderer>

<!-- Paused initially -->
<tegaki-renderer font="Caveat" text="Paused" playing="false">
</tegaki-renderer>

Glows, gradients, tapered ends and the rest are plugins. Register the factories you want once, next to the font bundle, and the element can name them in its plugins attribute:

import { glowPlugin, registerTegakiElement, strokeGradientPlugin, TegakiEngine, taperPlugin } from 'tegaki/wc';
import caveat from 'tegaki/fonts/caveat';

TegakiEngine.registerBundle(caveat);
TegakiEngine.registerPlugin(glowPlugin, strokeGradientPlugin, taperPlugin);
registerTegakiElement();

List names split by spaces or commas for the defaults, or write a JSON array to give options, with [name, options] for each plugin that takes them:

<tegaki-renderer font="Caveat" text="Tapered" plugins="taper"></tegaki-renderer>

<tegaki-renderer
  font="Caveat"
  text="Glowing rainbow"
  loop
  plugins='["strokeGradient", ["glow", {"radius": 0.1, "color": "#00ccff"}]]'
></tegaki-renderer>

The built-in names are glow, taper, strokeGradient, globalGradient, variation, boil and annotate, and none is registered until you register it. A plugin registered after the element is on the page is picked up then, so the order of your scripts doesn’t matter. A name that’s still unregistered once the page has loaded is warned about in the console. See Style plugins for their options.

Plugin objects, your own included, go on the plugins property. It takes names too, and when it’s set it takes precedence over the attribute:

const el = document.querySelector('tegaki-renderer');

el.plugins = [strokeGradientPlugin(), glowPlugin({ radius: 0.1, color: '#00ccff' })];

The pressure attribute (0 to 1, default 1) sets how much the ink’s width follows the pen’s pressure. The old effects property is gone: see Migrating from effects.

The element exposes playback methods and read-only properties:

const el = document.querySelector('tegaki-renderer');

el.play();
el.pause();
el.seek(1.5);    // jump to 1.5 seconds
el.restart();

el.currentTime;  // current playback time (seconds)
el.duration;     // total animation duration (seconds)
el.isPlaying;    // whether animation is running
el.isComplete;   // whether animation has finished

For advanced use cases, the underlying TegakiEngine instance is available:

const el = document.querySelector('tegaki-renderer');
const engine = el.engine;

engine.update({ timing: { charDuration: 0.5 } });

Use time="css" to drive the animation from CSS custom properties:

<tegaki-renderer font="Caveat" text="CSS driven" time="css">
</tegaki-renderer>

See the Rendering Animations guide for details.

Custom element names must contain a hyphen per the HTML spec. The default is tegaki-renderer, but you can choose any valid name:

registerTegakiElement('my-handwriting');
<my-handwriting font="Caveat" text="Custom tag!"></my-handwriting>
Attribute Type Description
text string Text to render. Falls back to textContent if not set.
font string Registered bundle name (see TegakiEngine.registerBundle).
time number | "css" Controlled time in seconds, or "css" for CSS mode. Omit for uncontrolled.
speed number Playback speed multiplier (default 1).
playing "true" | "false" Whether animation is playing (default true).
loop boolean attribute Loop animation when it finishes.
reduced-motion "never" | "user" | "always" Whether to honour reduced motion by showing the text finished (default "never"; see reducedMotion).
pixel-ratio number Supersampling factor on top of devicePixelRatio (shortcut for quality.pixelRatio).
segment-size number Max drawn segment length in CSS pixels (shortcut for quality.segmentSize).
pressure number How much the ink’s width follows the pen’s pressure, 0 to 1 (default 1; see pressure).
plugins string Registered plugins by name: "taper glow", or a JSON array with options, '["taper", ["glow", {"radius": 0.15}]]' (see Plugins). The plugins property takes precedence.
seed number | "random" What plugins’ random choices come from: where a gradient starts, how variation changes each glyph (default 0; see seed). The element’s engine.seed is the number it drew with.
show-overlay boolean attribute Show debug text overlay.
fallback-font string CSS font-family list for characters the bundle has no stroke data for (see fallbackFont).

These properties are set via JavaScript and support object values that can’t be expressed as attributes:

Property Type Description
font TegakiBundle | string Font bundle object or registered name.
plugins TegakiPluginSpec[] Plugin objects or registered names. Takes precedence over the plugins attribute.
timing TimelineConfig Timeline timing configuration.
quality TegakiQuality Render-quality knobs (pixelRatio, segmentSize).
onComplete () => void Callback when animation completes.
engine TegakiEngine (read-only) The underlying engine instance.

For the full options reference, see the TegakiRenderer API docs.