Font-Based Handwriting Text Animation for the Web – Tegaki

Category: Javascript , Recommended , Text | October 12, 2026
AuthorKurtGokhan
Last UpdateOctober 12, 2026
LicenseMIT
Views94 views
Font-Based Handwriting Text Animation for the Web – Tegaki

Tegaki is a JavaScript handwriting animation library that turns font glyphs into stroke-by-stroke text animations.

It ships a vanilla JavaScript engine plus adapters for React, Vue, Nuxt, Svelte, SolidJS, Astro, and Web Components.

The renderer builds handwriting from font data and follows the chosen typeface directly. Hand-authored SVG strokes are unnecessary.

Features:

  • Stroke-by-stroke handwriting generated from font data.
  • 13 built-in font bundles for Latin, Hebrew, Arabic, Devanagari, Japanese, Korean, Bengali, and Simplified Chinese text.
  • Vanilla JavaScript plus React, Vue, Nuxt, Svelte, SolidJS, Astro, and Web Component integrations.
  • Uncontrolled, controlled, and CSS-driven time modes.
  • Streaming text playback with configurable catch-up behavior.
  • Pressure control and ordered plugins for glow, taper, gradients, variation, line boil, and annotations.
  • Deterministic seeded rendering for repeatable output.
  • Animated and static SVG export from the renderer or command line.
  • Optional HarfBuzz shaping for ligatures, Arabic joining, Indic scripts, and complex text layout.
  • Custom bundles from Google Fonts or uploaded TTF/OTF files in Tegaki Studio.

See It In Action:

Use Cases:

  • Signature-style logos and hero headings that need to match an existing typeface.
  • Remotion titles whose handwriting follows video time frame by frame.
  • Streaming chat responses that keep writing as new chunks arrive.
  • Self-drawing SVG text for READMEs, slides, and static pages.

How To Use It:

1. Install Tegaki

# npm
npm install tegaki
# yarn
yarn add tegaki
# pnpm
pnpm add tegaki
# bun
bun add tegaki

2. Use the Web Component from a CDN

Your static page can load the Web Component and a font bundle through ESM imports.

<tegaki-renderer
  font="Caveat"
  text="Hello, world!"
  style="font-size: 56px"
></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>

3. Render Handwriting with Vanilla JavaScript

Import TegakiEngine from tegaki/core and pass a font bundle to the constructor.

<div id="handwriting" style="font-size: 56px"></div>
<script type="module">
  import { TegakiEngine } from 'tegaki/core';
  import italianno from 'tegaki/fonts/italianno';
  const engine = new TegakiEngine(
    document.getElementById('handwriting'),
    {
      text: 'Welcome Back',
      font: italianno,
      time: { mode: 'uncontrolled', speed: 1, loop: false },
    }
  );
</script>

4. Render Handwriting in React

Import the React renderer from tegaki and pass a font bundle.

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

5. Framework Entry Points

EnvironmentEntry PointRequirement
Reactimport { TegakiRenderer } from 'tegaki';React 18+
Next.jsUse the React adapter from tegaki in a Client Component.React 18+
Svelteimport { TegakiRenderer } from 'tegaki/svelte';Svelte 5+
Vueimport { TegakiRenderer } from 'tegaki/vue';Vue 3+
NuxtRegister tegaki/nuxt in modules.Nuxt 3 or 4
SolidJSimport { TegakiRenderer } from 'tegaki/solid';SolidJS 1+
Astroimport TegakiRenderer from 'tegaki/astro';Astro 4+
Web Componentsimport { registerTegakiElement } from 'tegaki/wc';No framework dependency.
Vanilla JavaScriptimport { TegakiEngine } from 'tegaki/core';No framework dependency.

6. Configure Nuxt

The Nuxt module auto-imports <TegakiRenderer> and wires the Vue adapter into SSR and client rendering.

export default defineNuxtConfig({
  modules: ['tegaki/nuxt'],
});

7. Style the Handwriting with CSS

Tegaki reads font-size, line-height, color, and letter-spacing from the renderer container.

#handwriting {
  font-size: 64px;
  line-height: normal;
  color: #242424;
  letter-spacing: 0.03em;
}

8. Style the Ink with Plugins

import {
  glowPlugin,
  strokeGradientPlugin,
  taperPlugin,
  variationPlugin,
} from 'tegaki/core';
const plugins = [
  taperPlugin(),
  variationPlugin(),
  strokeGradientPlugin(),
  glowPlugin({ radius: 0.1, color: '#00ccff' }),
];
const engine = new TegakiEngine(container, {
  text: 'Handwritten',
  font: caveat,
  pressure: 0.8,
  plugins,
});

9. Control the Animation from JavaScript

Pass seconds directly to time, use a percentage string for progress, or pass a controlled-time object when another timeline owns playback.

const engine = new TegakiEngine(container, {
  text: 'Scrub me',
  font: caveat,
  time: 0,
});
// Seek to a time in seconds.
engine.update({ time: 1.25 });
// Or seek by progress.
engine.update({ time: '50%' });
// Equivalent controlled object.
engine.update({
  time: {
    mode: 'controlled',
    value: 0.75,
    unit: 'progress',
  },
});

10. Read the Timeline Duration

Use computeTimeline() when external playback needs the natural duration before a renderer starts.

import { computeTimeline } from 'tegaki';
import caveat from 'tegaki/fonts/caveat';
const { totalDuration } = computeTimeline('Handcrafted', caveat);

11. Drive the Animation with CSS

Set time to 'css' and animate --tegaki-progress from 0 to 1. This works with normal CSS animation timelines, including scroll-driven timelines in browsers that implement them.

<tegaki-renderer
  class="scroll-writing"
  font="Caveat"
  text="Scroll to write"
  time="css"
></tegaki-renderer>
@keyframes tegaki-progress {
  from { --tegaki-progress: 0; }
  to { --tegaki-progress: 1; }
}
.scroll-writing {
  animation-name: tegaki-progress;
  animation-duration: 1s;
  animation-fill-mode: both;
  animation-timeline: view();
  animation-range: cover 10% cover 70%;
}

12. Animate Streaming Text

Update text as chunks arrive. catchUp increases playback speed when buffered text grows.

const writer = new TegakiEngine(container, {
  font: caveat,
  time: {
    mode: 'uncontrolled',
    speed: 4,
    catchUp: 0.5,
  },
});
let response = '';
function appendChunk(chunk) {
  response += chunk;
  writer.update({ text: response });
}

13. Register a Font Bundle by Name

Register a bundle once when later renderers need to reference it by its family name.

import { TegakiEngine } from 'tegaki/core';
import caveat from 'tegaki/fonts/caveat';
TegakiEngine.registerBundle(caveat);
const engine = new TegakiEngine(container, {
  text: 'Thank you',
  font: 'Caveat',
});

14. Pre-render Tegaki Markup for SSR

TegakiEngine.renderElements() creates the renderer tree for a server renderer. The browser can adopt that pre-rendered content with adopt: true.

const { rootProps, content } = TegakiEngine.renderElements(
  { text: 'Server Rendered Note', font: caveat },
  createElement
);
// Render rootProps on the container and content inside it
// with your server renderer.
// In the browser:
const engine = new TegakiEngine(container, {
  adopt: true,
  text: 'Server Rendered Note',
  font: caveat,
});

15. Generate an Animated SVG from the Command Line

The CLI writes SVG only and uses bundled HarfBuzz shaping. Tegaki Studio exports PNG, GIF, and WebM when raster or video output is required.

npx tegaki "Tegaki is awesome"
npx tegaki "Hello World" --font tangerine --mode once -o hello.svg
npx tegaki "Glow" --plugins '[["glow", {"radius": 0.12, "color": "#f0a"}]]'

Renderer Options:

OptionType / DefaultDescription
textstring, default ''Text to render. The React adapter can also read string or number children.
fontTegakiBundle | stringFont bundle object or a family name registered with registerBundle().
timeTimeControlPropControls playback through uncontrolled, controlled, or CSS time.
reducedMotion'never' | 'user' | 'always', default 'never'Controls reduced-motion behavior for uncontrolled playback.
pressurenumber, default 1Sets how closely stroke width follows pressure data stored in the bundle. Use values from 0 to 1.
timingTimelineConfigConfigures glyph gaps, word gaps, line gaps, easing, dot deferral, and staggered scheduling.
qualityTegakiQualityConfigures pixel ratio, stroke subdivision, text clipping, and smoothing.
pluginsreadonly TegakiPluginSpec[]Ordered plugin objects or registered plugin names that reshape, retime, paint, or decorate the handwriting.
seednumber | 'random', default 0Controls deterministic random variation used by plugins.
showOverlayboolean, default falseShows Tegaki’s debug text overlay.
onComplete() => voidRuns when uncontrolled playback reaches the end.
onChangeTimeline(timeline) => voidRuns after Tegaki recomputes the active timeline.
direction'ltr' | 'rtl'Sets text direction explicitly. Omit it to use automatic paragraph direction.
fallbackFontstringCSS font-family list for characters that have no stroke data in the active bundle.
shaperboolean, default trueLets the renderer use a globally registered text shaper. Set it to false for one renderer instance.

Time Control Options

OptionDefaultDescription
time: number–Controlled time in seconds.
time: '50%'–Controlled progress shorthand.
time: 'css'–CSS-time shorthand that reads --tegaki-progress.
controlled.valueRequiredCurrent controlled time or progress value.
controlled.unit'seconds''seconds' or 'progress'.
uncontrolled.initialTime0Initial time in seconds.
uncontrolled.playingtrueInitial play or pause state.
uncontrolled.loopfalseRestarts playback after completion.
uncontrolled.delay0Delay in seconds before playback begins.
uncontrolled.loopGap0Pause in seconds between loop iterations.
uncontrolled.speed1Playback speed multiplier.
uncontrolled.catchUp0Increases playback speed when buffered text has advanced ahead of the pen.
uncontrolled.duration–Sets one iteration to a fixed number of seconds. It is mutually exclusive with speed and catchUp.
uncontrolled.easing–Maps linear playback progress to displayed progress.
uncontrolled.onTimeChange–Runs on each frame with the current eased time.

Timeline Options

OptionDefaultDescription
glyphGap0.1Pause between glyphs in seconds.
wordGap0.15Pause after a space in seconds.
lineGap0.3Pause after a line break in seconds.
unknownDuration0.2Duration assigned to characters that have no glyph stroke data.
strokeEasingEase-out exponentialEasing function applied to each stroke’s draw progress.
glyphEasingLinearEasing function applied to each glyph’s local time.
deferDotstrueDefers priority-tagged marks and script-specific finishing strokes until body strokes in the word are complete.
stagger.advanceRequired when stagger is usedDelay between glyph starts in seconds or as a percentage string.
stagger.duration'auto'Uses bundled glyph timing or scales each glyph to a fixed number of seconds.

Quality Options

OptionDefaultDescription
pixelRatio1Internal supersampling factor applied on top of the device pixel ratio.
segmentSize2 px when subdivision is activeMaximum drawn segment length in CSS pixels for variable-width or reshaped strokes.
clipTextfalseClips handwriting strokes to the filled text shape. A positive number also scales stroke widths before clipping.
smoothingfalseSmooths stroke polylines with a centripetal Catmull-Rom spline.

Nuxt Module Options

OptionDefaultDescription
autoImporttrueRegisters <TegakiRenderer> as a global Nuxt component.
prefix''Prepends a string to the auto-imported component name.

Built-in Plugins:

Built-in plugin factories come from tegaki/core. Plugin order affects the final render. Register plugin factories when configuration must use plain names in HTML attributes, Astro props, or CLI data.

glowPlugin / glow

OptionDefaultDescription
radius0.1Glow radius. Bare numbers use em units. A string like '8px' uses a fixed unit.
color''Glow color. An empty value uses the text color.
offsetX0Horizontal glow offset.
offsetY0Vertical glow offset.

taperPlugin / taper

OptionDefaultDescription
startLength0.15Share of a stroke that thins toward its start.
endLength0.15Share of a stroke that thins toward its end.

strokeGradientPlugin / strokeGradient

OptionDefaultDescription
colors[]Gradient colors. An empty array creates the rainbow behavior.
saturation80Rainbow saturation in percent.
lightness55Rainbow lightness in percent.

globalGradientPlugin / globalGradient

OptionDefaultDescription
colors['#ff0000', '#0000ff']Color stops across the complete text.
angle0Gradient direction in degrees.

variationPlugin / variation

Presets: Subtle, Loose, and Signature.

OptionDefaultDescription
amount1Scales all variation settings.
size0.05Glyph size variation.
slant4Slant variation in degrees.
rotation2Rotation variation in degrees.
drift0.02Position drift as a length value.
warp0.015Glyph bend as a length value.
width0.12Stroke-width variation.

boilPlugin / boil

Presets: Cartoon, Nervous, and Sketchy.

OptionDefaultDescription
drawings3Number of alternate drawings in the cycle.
fps12Drawing changes per second.
amount0.012Line displacement as a length value.
detail0.3Controls slow bends versus finer jitter.
width0.08Stroke-width variation.
idlefalseKeeps the boil cycle active after writing finishes or during a pause.

annotatePlugin / annotate

OptionDefaultDescription
mark'underline''underline', 'double', 'highlight', 'strike', 'circle', 'box', or 'cross'.
target'text''text', 'lines', or 'words'.
pick0Selects one item by one-based index. 0 marks every eligible item.
when'after''after' draws marks after the text. 'each' draws a mark after its associated text unit.
color'#e5484d'Annotation color.
width0.045Annotation stroke width as a length value.
padding0.12Distance between the annotation and handwriting.
roughness0.5Controls how loosely the annotation is drawn.
duration0.5Seconds used to draw each mark.
delay0.25Seconds before each mark begins.

Custom Plugin Hooks:

HookPurpose
geometry(path, ctx)Reshapes stroke points and widths when layout changes.
timing(ctx)Changes stroke start times and durations.
paint(stroke, next)Changes how each stroke is painted.
ink(ctx)Post-processes the finished ink for glow, shadows, or filters.
underlay(ctx)Paints beneath the handwriting.
overlay(ctx)Paints above the handwriting.
onFrame(frame, prev)Runs after every rendered frame for side effects or synchronization.
bounds(ctx)Expands drawing bounds for plugin output beyond the ink.
outline(contour, ctx)Reshapes glyph outlines used by text clipping.
svg(ctx)Supplies plugin-specific SVG markup or styling during SVG export.
attach(ctx)Sets up plugin resources and can return a cleanup function.
stepsDefines a drawing cycle with count, fps, optional idle, and optional paintOnly.

TegakiEngine API:

Constructor

SignatureDescription
new TegakiEngine(container, options?)Creates an engine in an HTML container. Constructor options use TegakiEngineOptions plus adopt?: boolean for pre-rendered content.

Static Methods

MethodDescription
TegakiEngine.registerBundle(bundle)Registers a font bundle by family name.
TegakiEngine.getBundle(name)Returns a registered font bundle.
TegakiEngine.registerPlugin(...factories)Registers plugin factories for name-based plugin configuration.
TegakiEngine.getPlugin(name)Returns a registered plugin factory.
TegakiEngine.registerShaper(factory)Registers a global text shaper. Pass null to unregister it.
TegakiEngine.preload(bundle)Preloads font faces and the registered shaper for a bundle.
TegakiEngine.renderElements(options, createElement)Returns { rootProps, content } for server rendering and later client adoption.

Instance Properties

PropertyDescription
currentTimeCurrent rendered timeline time in seconds.
durationTotal active timeline duration.
seedResolved numeric seed used by the renderer.
timelineCurrent computed timeline.
strokesRead-only stroke list with start times and durations.
isPlayingCurrent uncontrolled playback state.
isCompleteReports completion for the active timeline.
elementRenderer root element.
canvasBacking canvas containing the latest rendered frame.

Instance Methods

MethodDescription
update(options)Updates any subset of engine options.
computeTimeline(text)Computes a timeline with the engine’s loaded font, timing configuration, and resolved shaper.
frameAt(time?)Returns stroke state, progress, geometry, and pen-head data at a timeline time.
toSVG(options?)Serializes the current render to an SVG string.
exportSVG(options?)Exports SVG asynchronously and embeds font data required by SVG text.
play()Resumes uncontrolled playback.
pause()Pauses uncontrolled playback.
seek(time)Moves uncontrolled playback to seconds or a percentage string.
restart()Returns uncontrolled playback to the beginning and applies its configured delay.
destroy()Stops observers, loops, plugin attachments, and engine-owned DOM content.

SVG Export Options

OptionDefaultDescription
animatedtrueExports self-drawing SVG animation. Set false for finished artwork.
loopfalseUses a repeating CSS-keyframe SVG animation.
speedEngine speed or 1Playback speed used by the exported SVG.
loopHold1.5Seconds the completed text is held before a looping SVG repeats.
croptrueCrops the SVG viewBox to the rendered ink.
fontFaces–toSVG() can embed supplied font-face data. exportSVG() builds the required font data automatically.

Additional Public Core APIs:

APIDescription
BUNDLE_VERSIONCurrent public font-bundle format version.
textToSvg(text, bundle, options?)Creates animated or static SVG markup in headless code.
drawGlyph(ctx, glyph, pos, time, options?)Draws one glyph on a canvas context at a specific local time.
computeTimeline(text, font, timing?, shaper?)Computes text timing outside an engine instance.
StrokePathStroke polyline class with length, pointAt(), slice(), bounds(), and map().
paintStroke()Default stroke painter used at the end of a plugin paint chain.
offsetPath()Creates a parallel stroke path.
inkEdge()Builds an offset distance that follows the ink edge.
clearance()Measures distance from a point to the nearest ink edge.
unionBoxes()Returns one box that contains a list of boxes.
expandBox()Expands a box by a pixel amount on each side.
groupStrokes()Groups strokes by complete text, lines, or words.
seededRandom()Creates deterministic random values from a seed and key.
createBundle()Creates a font bundle from font and glyph data supplied by application code.
createPlugin()Defines a typed plugin factory with parameters, defaults, ranges, and presets.
lengthToPx()Converts a plugin length value to CSS pixels for a given font size.
parseLength()Parses a Tegaki plugin length value.
registerBundle() / getBundle()Function exports for the global bundle registry.
registerPlugin() / getPlugin()Function exports for the global plugin registry.
parsePluginSpecs()Parses name-based plugin configuration.
paintsDrawnOnly()Marks a plugin factory whose paint hook only restyles strokes that the pen has reached.

Built-in Font Bundles:

Most built-in bundles can load their source font as fallback text when a character has no stroke data. LXGW WenKai ships the generated subset only; configure fallbackFont for characters outside that subset.

FontImportWriting System
Caveattegaki/fonts/caveatLatin
Italiannotegaki/fonts/italiannoLatin
Tangerinetegaki/fonts/tangerineLatin
Parisiennetegaki/fonts/parisienneLatin
Suez Onetegaki/fonts/suez-oneHebrew and Latin
Amiritegaki/fonts/amiriArabic and Latin
Tillanategaki/fonts/tillanaDevanagari and Latin
Klee Onetegaki/fonts/klee-oneJapanese kana, Kyōiku grade 1-2 kanji, and Latin
Nanum Pen Scripttegaki/fonts/nanum-pen-scriptKorean Hangul, jamo, and Latin
Atmategaki/fonts/atmaBengali and Latin
LXGW WenKaitegaki/fonts/lxgw-wenkaiSimplified Chinese, 1000 common hanzi, punctuation, and Latin
Hershey Scripttegaki/fonts/hershey-scriptLatin single-line stroke font
EMS Alluretegaki/fonts/ems-allureLatin single-line stroke font

Custom Font Bundles:

Tegaki Studio accepts Google Fonts and uploaded .ttf or .otf files. Download the generated bundle and import its bundle.ts file in place of a built-in font import.

import customFont from './output/my-font/bundle';

Text Shaping:

Complex scripts and OpenType features can use the optional HarfBuzz shaper. Install harfbuzzjs before importing tegaki/shaper-harfbuzz.

npm install harfbuzzjs
import { TegakiEngine } from 'tegaki/core';
import harfbuzzShaper from 'tegaki/shaper-harfbuzz';
TegakiEngine.registerShaper(harfbuzzShaper);

Migrating from the Old effects API:

The stable 1.x API removes effects. Existing effect configuration must move to plugins and pressure.

Old APICurrent API
effects: { glow: { radius: 8 } }plugins: [glowPlugin({ radius: '8px' })]
effects: { wobble: { ... } }Use variationPlugin(), boilPlugin(), or a custom wobble plugin.
effects: { taper: true }plugins: [taperPlugin()]
effects: { strokeGradient: { colors: 'rainbow' } }plugins: [strokeGradientPlugin()]
effects: { globalGradient: { colors, angle } }plugins: [globalGradientPlugin({ colors, angle })]
effects: { pressureWidth: false }pressure: 0
effects: { pressureWidth: { strength: 0.5 } }pressure: 0.5
--effects in the CLI--plugins

Alternatives & Related Resources:

FAQs:

Q: Why does Tegaki lay out text in Vite but fail to draw the handwriting?
A: Vite 7 and earlier can pre-bundle Tegaki in a way that breaks the relative font URL. Exclude tegaki from dependency optimization and restart the dev server.

export default defineConfig({
  optimizeDeps: {
    exclude: ['tegaki'],
  },
});

Q: What does Astro need for server-rendered font bundles?
A: Use the Astro integration in astro.config.mjs. It lets Vite emit bundled fonts as assets and resolves their public URLs during server rendering.

import { defineConfig } from 'astro/config';
import tegaki from 'tegaki/astro/integration';
export default defineConfig({
  integrations: [tegaki()],
});

Q: Why does a registered plugin name fail to resolve?
A: Name-based plugins are not registered automatically in framework renderers. Register each factory with TegakiEngine.registerPlugin() before relying on names like 'glow' or 'taper'.

Changelog:

v1.1.0 (10/12/2026)

  • Bundles with several font weights

v1.0.3 (10/07/2026)

  • Nib stamps keep their size below full pressure

v1.0.2 (10/02/2026)

  • Idle plugin loops pause off screen

v1.0.1 (10/01/2026)

  • Unified Vue, Nuxt, Svelte, SolidJS, and Astro adapters on tegaki/core to fix plugin and bundle registry compatibility.

v1.0.0 (09/30/2026)

  • Established the stable semver renderer, plugin, and bundle APIs.
  • Replaced the old effects configuration with plugins and the pressure option.
  • Shipped LXGW WenKai for Simplified Chinese plus the Hershey Script and EMS Allure single-line bundles.
  • Introduced deterministic seed, fallbackFont, reducedMotion, plugin registration, and improved SVG export.
  • Regenerated bundled fonts with improved stroke extraction and stroke order.
  • Shipped annotatePlugin and expanded the public plugin API.

v0.22.2 (09/23/2026)

  • Continued rendering when a bundle font file fails to load.

v0.22.1 (07/13/2026)

  • Shipped the tegaki/fonts/atma build output.

v0.22.0 (07/12/2026)

  • Introduced Bengali support through the Atma font bundle.

v0.19.0 (06/29/2026)

  • Introduced Korean Hangul support and the Nanum Pen Script bundle.

You Might Be Interested In:


Leave a Reply