Chinese characters are written in a fixed stroke order, and learners who skip it end up with slow, messy handwriting and characters that handwriting keyboards fail to recognise. When we added a handwriting practice tool for Vietnamese learners of Chinese, we used hanzi-writer, an open-source JavaScript library that animates and quizzes stroke order. This post covers the parts that took more thought than expected.
What hanzi-writer gives you
hanzi-writer renders a character as SVG from stroke data (outline paths plus a median line for each stroke). It has three modes that map nicely onto how people learn:
- Animate: draw each stroke in order so the learner sees the sequence.
- Outline + trace: show a faint outline and let the learner draw over it.
- Quiz: hide the character and check each stroke the learner draws against the expected median, with configurable leniency.
import HanziWriter from 'hanzi-writer';
const writer = HanziWriter.create('target', '好', {
width: 240,
height: 240,
padding: 8,
showOutline: true,
charDataLoader: loadCharData, // see below
});
writer.quiz({
leniency: 1.2,
onMistake: s => console.log('wrong stroke', s.strokeNum),
onComplete: s => console.log('mistakes:', s.totalMistakes),
});
Self-host the stroke data
By default the library fetches character data from a CDN, one JSON file per character. For a learning site that is a lot of third-party requests, and it breaks when the CDN is slow. We serve the data files from our own static path and pass a charDataLoader:
function loadCharData(char, onLoad, onError) {
fetch(`/hanzi-data/${encodeURIComponent(char)}.json`)
.then(r => (r.ok ? r.json() : Promise.reject(r.status)))
.then(onLoad)
.catch(onError);
}
Set long cache headers on that path; the stroke data for a character never changes.
Printable practice sheets
Learners still want paper. The classic practice grid is the 米字格 (a square with diagonal and centre guide lines). Because hanzi-writer outputs SVG, the same stroke data can be drawn into a printable grid: one row per character, the first cell solid, the next few cells as faint outlines to trace, the rest empty. Generating the sheet as SVG inside an HTML page and using @media print with @page { size: A4; margin: 12mm; } gives clean output without a PDF library.
Lessons
- Quiz leniency matters. The default (1) can feel strict for complete beginners; raising it a little lowers frustration in the first weeks.
- Show the stroke number on mistakes. "Wrong stroke" alone is not actionable.
- Pair characters with their components (radicals). 好 is 女 + 子; learners remember two parts better than six loose strokes.
You can try the result, including the printable 米字格 sheets, on the Chinese handwriting practice page of Tu Vung 123 (the interface is in Vietnamese, but the writing area is self-explanatory).
Top comments (2)
Nice write-up — the self-hosting point is underrated. Two things worth flagging from the data side. First, lineage matters for licensing: hanzi-writer's characters trace back to Make Me a Hanzi, whose outlines are derived from an Arphic Kai font released under the Arphic Public License, which is not MIT — fine for a free learning site, worth checking before you ship it inside a paid app. Second, if you ever add traditional characters, expect divergent stroke orders: the mainland 现代汉语通用字笔顺规范 and Taiwan's 標準字體 disagree on characters learners actually use. 左 opens with the horizontal while 右 opens with the 撇; 乃 and 及 differ on when the 撇 lands; 門 versus 门 flips strokes. hanzi-writer will animate whichever standard its data came from, and a Taiwanese learner marking a mainland animation wrong is a bug report you can never win. On the quiz, a single leniency scalar is generous on long strokes and too strict on 点 and 提, which are exactly the strokes beginners misplace — per-stroke leniency keyed to stroke length would fix most of it. And a median line cannot distinguish a 提 from a 点, so compare start and end angles too if you want it to catch the confident-but-wrong stroke.
For the printable sheets, do you reuse each character with /