Text Animations

Handwriting Text

Handwriting animation drawn stroke by stroke with SVG pathLength, with a built-in single-line font and support for custom signature paths.

This is a WebberUI Pro component

The install command below needs a licence to run; without one it returns 401.

How to install Pro components →See the plans →

Bring this into your project

Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.

npx webberui@latest add "https://webberui.com/r/handwriting-text.en.json?t=<install token>"

New project or installation trouble? Start here →

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

0.7
0.18
5
0
Active edited usageDefault props
<HandwritingText/>

The complete example includes the preview layout and data. Install the component first, then paste this file into your project.

Installation

npx webberui@latest add "https://webberui.com/r/handwriting-text.en.json?t=<install token>"

Or, once registries are configured in components.json, install it as @webberui/handwriting-text.

Usage

Pass text and it is drawn stroke by stroke in the built-in single-line font (the text is uppercased automatically):

import { HandwritingText } from "@/components/ui/handwriting-text";

<HandwritingText text="Webber" className="text-5xl font-medium" />

To draw a signature or a script flourish, pass paths instead (an array of SVG path d strings) together with a viewBox:

<HandwritingText
  paths={["M10 66 C 24 20 40 22 42 60 …", "M6 90 C 90 76 200 76 262 88"]}
  viewBox="0 0 272 100"
  label="Handwritten signature"
/>

Rhythm and manual triggering

totalDuration sets the length of the whole drawing; the component scales duration and stagger proportionally, so the relative pacing between strokes stays the same. easing accepts a cubic-bezier() string, a 4-number tuple, or a named motion easing. With trigger="manual", play controls when drawing starts, and an incrementing key gives you a replay:

const [play, setPlay] = React.useState(false);
const [replayKey, setReplayKey] = React.useState(0);

<button type="button" onClick={() => setPlay(true)}>
  <HandwritingText
    key={replayKey}
    text="Hello"
    trigger="manual"
    play={play}
    totalDuration={2.8}
    easing="cubic-bezier(0.2,0.2,0.8,0.9)"
    delay={0.2}
    onComplete={() => console.log("done")}
  />
</button>
<button type="button" onClick={() => setReplayKey((k) => k + 1)}>Replay</button>

Props

PropTypeDefaultDescription
textstring—The text to draw, rendered in the built-in single-line font (uppercased automatically); use either this or paths
pathsstring[]—Custom SVG paths (an array of d strings); takes the place of text when provided
viewBoxstring"0 0 100 100"The viewBox for paths mode; in text mode it is computed automatically
labelstring—Text description exposed to assistive technology in paths mode
durationnumber0.7Duration of a single stroke (seconds)
staggernumber0.18Interval between the starts of adjacent strokes (seconds)
delaynumber0Delay before the whole drawing starts (seconds)
easing[number, number, number, number] | "cubic-bezier(…)" | "linear" | "easeIn" | "easeOut" | "easeInOut"[0.22, 1, 0.36, 1]Stroke easing; the suggested handwriting rhythm is "cubic-bezier(0.2,0.2,0.8,0.9)", and an unparsable string falls back to the default
totalDurationnumber—Total length of the whole drawing (seconds), scaling duration / stagger proportionally; 2.8 is a good start. When omitted, each stroke uses duration
playbooleanfalseWhether to draw when trigger is manual; true draws, false retracts
onComplete() => void—Called when the last stroke finishes drawing; not fired with loop
strokeWidthnumber5Stroke width (in viewBox units)
fillbooleanfalseFade in a fill once drawing finishes, suited to closed paths; ignored when loop is on
loopbooleanfalseWhether to redraw in a loop
trigger"view" | "hover" | "mount" | "manual""view"When the animation is triggered; manual is controlled by play
oncebooleantrueWith trigger set to view, plays only the first time the element enters the viewport
classNamestring—The outer container; in text mode size is controlled by font-size, in paths mode by width
svgClassNamestring—Applied directly to the <svg>, so the default size classes can be overridden

How it works

  • Size: in text mode the <svg> is 1em tall, so a font-size class like text-5xl is enough to scale it; in paths mode the width fills the container, so the outer width controls the size.
  • Color: the stroke uses currentColor, so a text color class such as text-neutral-* colors it directly, and light and dark themes can each set their own.
  • Single-line font: A–Z, 0–9, and common punctuation are built in, with all letters uppercase; each glyph is broken into several strokes drawn in writing order. Glyphs that are not included are treated as blank spacing.
  • Looping: with loop on, all strokes share the same cycle, so a redraw keeps the original offset ordering and does not flicker out of sequence.
  • Triggering: with trigger="hover" the container is focusable (tabIndex=0), so both a mouse enter and keyboard focus redraw it.
  • pathLength normalization: every path's pathLength is normalized to 1, so dash lengths are computed on 0–1 regardless of the path's real length, letting strokes of different lengths share one set of timing params.
  • Total duration: totalDuration only scales time; stroke order and offset ratios stay the same, and delay is not counted in the total.

Accessibility

  • trigger="manual" binds no keys of its own; for "press Space to start", wrap the component in a native <button> and the browser fires click on Space and Enter.
  • In text mode the container carries role="img" and an aria-label, and keeps an sr-only copy of the text for the screen reader.
  • In paths mode, provide a text description through label; when neither text nor label is provided, the whole thing is hidden from assistive technology with aria-hidden.
  • When the user has "reduce motion" enabled at the system level, the finished drawing is shown statically with no animation.