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.
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>"Playground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<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
| Prop | Type | Default | Description |
|---|---|---|---|
text | string | — | The text to draw, rendered in the built-in single-line font (uppercased automatically); use either this or paths |
paths | string[] | — | Custom SVG paths (an array of d strings); takes the place of text when provided |
viewBox | string | "0 0 100 100" | The viewBox for paths mode; in text mode it is computed automatically |
label | string | — | Text description exposed to assistive technology in paths mode |
duration | number | 0.7 | Duration of a single stroke (seconds) |
stagger | number | 0.18 | Interval between the starts of adjacent strokes (seconds) |
delay | number | 0 | Delay 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 |
totalDuration | number | — | Total length of the whole drawing (seconds), scaling duration / stagger proportionally; 2.8 is a good start. When omitted, each stroke uses duration |
play | boolean | false | Whether to draw when trigger is manual; true draws, false retracts |
onComplete | () => void | — | Called when the last stroke finishes drawing; not fired with loop |
strokeWidth | number | 5 | Stroke width (in viewBox units) |
fill | boolean | false | Fade in a fill once drawing finishes, suited to closed paths; ignored when loop is on |
loop | boolean | false | Whether to redraw in a loop |
trigger | "view" | "hover" | "mount" | "manual" | "view" | When the animation is triggered; manual is controlled by play |
once | boolean | true | With trigger set to view, plays only the first time the element enters the viewport |
className | string | — | The outer container; in text mode size is controlled by font-size, in paths mode by width |
svgClassName | string | — | Applied directly to the <svg>, so the default size classes can be overridden |
How it works
- Size: in
textmode the<svg>is1emtall, so a font-size class liketext-5xlis enough to scale it; inpathsmode the width fills the container, so the outer width controls the size. - Color: the stroke uses
currentColor, so a text color class such astext-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
loopon, 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
pathLengthis 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:
totalDurationonly scales time; stroke order and offset ratios stay the same, anddelayis 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
textmode the container carriesrole="img"and anaria-label, and keeps ansr-onlycopy of the text for the screen reader. - In
pathsmode, provide a text description throughlabel; when neithertextnorlabelis provided, the whole thing is hidden from assistive technology witharia-hidden. - When the user has "reduce motion" enabled at the system level, the finished drawing is shown statically with no animation.