Font-Based Handwriting Text Animation for the Web – Tegaki

Category: Javascript , Recommended , Text | May 28, 2026
AuthorKurtGokhan
Last UpdateMay 28, 2026
LicenseMIT
Views0 views
Font-Based Handwriting Text Animation for the Web – Tegaki

Tegaki is a JavaScript text animation library that converts any font into animated handwriting.

It reads glyph data from a font bundle, computes stroke paths and timing, then renders the animation inside a container element.

The result looks like text being written by hand, stroke by stroke.

Features:

  • Converts any font into animated stroke-by-stroke handwriting.
  • 8 multilingual font bundles: Latin, Arabic, Hebrew, Devanagari, and Japanese scripts.
  • 3 time control modes: uncontrolled clock, developer-controlled value, and CSS-driven animation.
  • 5 effects: glow, wobble, pressure width variation, stroke taper, and rainbow gradient strokes.
  • Works with React, Vue, Svelte, SolidJS, Astro, and Web Components.
  • A browser-based interactive generator for custom font bundles.
  • Supports server-side rendering and client-side adoption for SSR-compatible frameworks.
  • A helper function for computing total animation duration from a text string and font bundle.
  • Supports streaming text input for chat-style incremental text reveal.
  • Allows global bundle registration by font family name for reuse across components.

See It In Action:

Use Cases:

  • Hero section headings that draw themselves on page load to grab attention.
  • Animated subtitles in video productions built with Remotion.
  • Interactive e-learning modules where instructional text appears handwritten.
  • Presentation slides that reveal text with a natural handwriting effect using Slidev.

How To Use It:

1. Install Tegaki with package managers.

# Yarn
$ yarn add tegaki
# NPM
$ npm install tegaki
# PNPM
$ pnpm install tegaki
# BUN
$ bun add tegaki
// Framework adapters
import { TegakiRenderer } from 'tegaki';         // React
import { TegakiRenderer } from 'tegaki/svelte';  // Svelte
import { TegakiRenderer } from 'tegaki/vue';     // Vue
import { TegakiRenderer } from 'tegaki/solid';   // SolidJS
import TegakiRenderer from 'tegaki/astro';       // Astro
import { registerTegakiElement } from 'tegaki/wc'; // Web Components

2. Import a font bundle and a framework adapter. This React example renders an auto-playing looping handwriting animation at 48px font size using the Caveat Latin font:

import { TegakiRenderer } from 'tegaki';
import bundle from 'tegaki/fonts/caveat';
function SignatureHero() {
  return (
    <TegakiRenderer
      font={bundle}
      time={{ mode: 'uncontrolled', speed: 1, loop: true }}
      style={{ fontSize: 48 }}
    >
      Your Name Here
    </TegakiRenderer>
  );
}

3. Vanilla JS usage requires TegakiEngine from tegaki/core:

<div id="hero-signature" style="font-size: 56px"></div>
<div id="hero-signature" style="font-size: 56px"></div>
<script type="module">
  import { TegakiEngine } from 'tegaki/core';
  import bundle from 'tegaki/fonts/italianno';
  const container = document.getElementById('hero-signature');
  // Creates and manages all DOM elements inside the container
  const engine = new TegakiEngine(container, {
    text: 'Welcome Back',
    font: bundle,
    time: { mode: 'uncontrolled', speed: 1, loop: false },
  });
</script>

4. An animation must sync with an external timeline, such as a scrubber or video playback. Pass a numeric time value directly and update it on each frame. Use computeTimeline() to get the total duration and calculate progress:

import { TegakiEngine } from 'tegaki/core';
import { computeTimeline } from 'tegaki';
import bundle from 'tegaki/fonts/tangerine';
const container = document.getElementById('sync-text');
const engine = new TegakiEngine(container, {
  text: 'Handcrafted',
  font: bundle,
  time: 0, // Start at the beginning
});
// Get the total animation duration in seconds
const { totalDuration } = computeTimeline('Handcrafted', bundle);
let elapsed = 0;
function tick() {
  elapsed += 0.016; // Advance by one frame (~60fps)
  const progress = Math.min(elapsed, totalDuration);
  engine.update({ time: progress }); // Feed current time to the engine
  requestAnimationFrame(tick);
}
tick();

5. If your page needs maximum animation performance with no JavaScript clock loop. CSS mode delegates all timing to CSS transitions. This works best for simple one-shot animations where precise programmatic control is not required:

<TegakiRenderer
  font={bundle}
  time={{ mode: 'css', speed: 1.5 }}
  style={{ fontSize: 36 }}
>
  Confirmed
</TegakiRenderer>

6. A server-rendered page can generate the element tree on the server and adopt it on the client. Use this when the framework adapter does not cover your custom rendering stack.

import { TegakiEngine } from 'tegaki/core';
import caveatBundle from 'tegaki/fonts/caveat';
// Server side.
const html = TegakiEngine.renderElements(
  {
    text: 'Server Rendered Note',
    font: caveatBundle,
  },
  (tag, props, ...children) => {
    // Return an HTML string from your own renderer.
  }
);
// Client side.
const engine = new TegakiEngine(target, {
  adopt: true,
  text: 'Server Rendered Note',
  font: caveatBundle,
});

7. A chatbot UI can animate response text as chunks arrive from a streaming API. Use uncontrolled time with catchUp so the animation can keep pace when buffered text grows.

const messageWriter = new TegakiEngine(target, {
  font: bundle,
  time: {
    mode: 'uncontrolled',
    speed: 4,
    catchUp: 0.5,
  },
});
let assistantReply = '';
function appendStreamChunk(chunk) {
  assistantReply += chunk;
  // Update the text whenever a new chunk arrives.
  messageWriter.update({
    text: assistantReply,
  });
}

8. Global font bundle registration.

import { TegakiEngine } from 'tegaki/core';
import caveat from 'tegaki/fonts/caveat';
import parisienne from 'tegaki/fonts/parisienne';
// Register once, globally
TegakiEngine.registerBundle(caveat);
TegakiEngine.registerBundle(parisienne);
// Anywhere else in the app, reference by name
const engine = new TegakiEngine(container, {
  text: 'Thank you',
  font: 'Parisienne', // Resolved from the registry
  time: { mode: 'uncontrolled', speed: 1, loop: false },
});

All available options (props):

  • font (TegakiBundle, required): The font bundle object containing glyph data, metrics, and timing.
  • text (string): The text to animate. In framework components, you may also pass it as children.
  • time (number | object): Animation timing control. Pass a number for direct time injection, or an object with a mode key set to 'uncontrolled', 'controlled', or 'css'. Uncontrolled mode accepts speed (number, default 1), loop (boolean, default false), playing (boolean, default true), and catchUp (boolean, for streaming). Controlled mode accepts a value (number). CSS mode accepts speed (number).
  • effects (TegakiEffectConfigs): Visual effects object. All keys are optional.
    • glow (object): Glow behind strokes. Accepts radius, color, offsetX, and offsetY.
    • wobble (object): Hand tremor on strokes. Accepts amplitude, frequency, and mode ('sine' or 'noise').
    • pressureWidth (object | true): Stroke width variation simulating pen pressure. Accepts strength.
    • taper (object): Tapered stroke ends. Accepts startLength and endLength.
    • strokeGradient (object): Gradient colors along strokes. Accepts colors (string or string array), saturation, and lightness.
  • style (object | string): Style applied to the container element. Use fontSize to scale the animation.
  • shaper (boolean, default true): Controls whether this renderer instance uses the globally registered HarfBuzz text shaper. Set to false to opt a single renderer out of shaping.
  • onComplete (function): Callback fired when the animation reaches the end. Fires only in uncontrolled mode.

API Methods:

// TegakiEngine constructor — creates the engine and DOM inside the container
const engine = new TegakiEngine(containerElement, options);
// Update any subset of options at runtime. Unchanged options persist.
engine.update({ text: 'New text', time: { mode: 'uncontrolled', speed: 2 } });
// Register a font bundle globally so components can reference it by family name string
TegakiEngine.registerBundle(bundle);
// Register a HarfBuzz text shaper globally for correct Unicode text shaping.
// Pass null to unregister and clear the shaper cache.
TegakiEngine.registerShaper(harfbuzzShaper);
// Produce a pre-rendered element tree on the server for SSR adoption on the client.
// Pass a createElement-style factory function to receive the element tree.
TegakiEngine.renderElements({ text: 'Hello', font: bundle }, createElement);
// Adopt a pre-rendered DOM tree on the client after SSR rendering.
const engine = new TegakiEngine(container, { adopt: true, text: 'Hello', font: bundle });

Built-in Font Bundles:

The built-in bundles include Caveat, Italianno, Tangerine, and Parisienne for Latin text. Suez One supports Hebrew and Latin, Amiri supports Arabic and Latin, Tillana supports Devanagari and Latin, and Klee One supports Japanese kana, selected kanji, and Latin text.

import caveat from 'tegaki/fonts/caveat';
import italianno from 'tegaki/fonts/italianno';
import tangerine from 'tegaki/fonts/tangerine';
import parisienne from 'tegaki/fonts/parisienne';
import suezOne from 'tegaki/fonts/suez-one';
import amiri from 'tegaki/fonts/amiri';
import tillana from 'tegaki/fonts/tillana';
import kleeOne from 'tegaki/fonts/klee-one';

Custom font bundle

The built-in bundles only cover a fixed character subset per font. For a project font or specific characters outside the bundled subset, use the interactive generator at gkurt.com/tegaki/generator/ to create a custom bundle, then import it locally:

Alternatives

You Might Be Interested In:


Leave a Reply