Kanji by Insomnius
Muhammad Arief Rahman
-
April 26, 2026
6 min read
vite
react
typescript
tailwind
bun
tanstack-router
indexeddb
pwa
accessibility
japanese
education
privacy
Overview
Kanji by Insomnius is a focused study companion for Japanese kanji, vocabulary, and kana. It runs entirely in the browser — no account, no server, no analytics. Every answer, streak, and session lives in IndexedDB on the learner's device, with a downloadable backup as the only path off-device.
The 2026 rebuild migrated the project from Next.js to a Vite + TanStack Router stack, redesigned the interface around an "Editorial Sumi" aesthetic (sumi black, vermilion accents, cream paper), and added a local-first progress system, stroke-order practice, gamification, and PWA support — all while keeping the privacy-first promise intact.
Screenshots
Desktop


Mobile


Practice modes
- Kanji & vocabulary quizzes — type meanings, pick from multiple choice, with hotkey-driven flow.
- Kana drills — hiragana and katakana practice with
1–4plusEnterkeyboard shortcuts. - Stroke-order practice — watch each kanji drawn one stroke at a time, then trace it yourself. Hints appear after two misses. The dedicated
/drawroute lets learners practice any JLPT level.
Reference & exploration
- Searchable kanji, vocab, and kana lists — over 2,000 kanji from JLPT N5 to N1, the full vocabulary set, and complete hiragana / katakana tables.
- Master-detail layout on desktop, inline expansion on mobile — the grid never reflows on click, so the learner never loses spatial context.
- Deep-link routes for every kanji, word, and kana character (
/kanji/$char,/vocab/$word,/kana/$char), making any item shareable as its own URL with a per-character SEOhead()andDefinedTermJSON-LD block.
Progress, streaks, and gamification
- Daily streak + goal — set a daily target (default 10, range 1–200). The streak only counts correct answers, framing practice as "learn 10 things right" instead of button-mashing.
- At-risk warning — when the streak is alive but today is empty, the session strip swaps to vermilion until the learner answers one question. Loss-aversion nudge that disappears the moment they engage.
- Activity calendar — GitHub-style heatmap of every day practiced over the last 52 weeks, embedded on the profile page. Hand-rolled with CSS Grid and
Intl.DateTimeFormat, no calendar library. - Goal-hit transition — a subtle 1.2s pulse when the daily goal is reached. Editorial restraint applied to gamification: typography, color, and a quiet ring shadow. No confetti, no overlay.
Local-first data
- IndexedDB-backed progress with a thin Promise wrapper around the native API — no Dexie, no idb library.
- BroadcastChannel for instant tab-to-tab sync — change your name in one tab, the other updates immediately.
- localStorage mirror of display name and streak so first paint reads are synchronous. No "loading…" flash for personal data.
- Local timezone day boundaries — midnight in Jakarta resets the streak window for a Jakarta learner, not UTC.
Backup, import, and share
- Gzipped JSON backups via the native
CompressionStreamAPI, with a plain-JSON fallback. Filename pattern:kanji-by-insomnius-backup-YYYY-MM-DD.kbi.gz. - Schema-versioned imports — auto-detects gzip via magic bytes, decompresses with
DecompressionStream, runs forward migrations, and restores via chunked IndexedDB writes (500 rows per transaction with event-loop yields between chunks for UI responsiveness). - Documented migration policy — old backups always import, even after the schema evolves. Migration paths are spec'd in
docs/BACKUP_SCHEMA.mdand treated as a contract. - Progress card sharing to Twitter, Facebook, and Threads via the Web Share API, plus an Instagram-ready square or story image generated on a
canvaselement. No html2canvas dependency.
Accessibility — WCAG 2.1 AA as the floor
- Japanese text wrapped with
lang="ja"so screen readers pronounce it correctly, not as garbled romaji. - Full keyboard navigation. Focus rings are vermilion (brand-aligned), never blue, never invisible.
- Live-region announcements for quiz feedback (
role="status",aria-live="assertive") withsr-only"Correct: " / "Incorrect: " prefixes. - Reduced-motion respected on every transition and animation via
motion-reduce:transition-noneandprefers-reduced-motionmedia queries. - Touch targets sized to at least 44 by 44 pixels throughout, including segmented-control items.
- Toggle buttons use
aria-pressed, disclosure widgets usearia-expandedplusaria-controls, modals and dropdowns rely on Radix primitives so focus traps and Escape handling are correct by default.
PWA & offline
vite-plugin-pwawith a Workbox-generated service worker,autoUpdateregistration.- All built JS, CSS, fonts, and images precached — except the heavy 1.2 MB
kanji-datachunk, which is runtime-cached so the first visit isn't blocked by precaching it. - Hanzi-writer stroke data (loaded from jsdelivr CDN) is also runtime-cached so stroke order works offline after first use.
- Sitemap is regenerated at build time from the kanji, vocab, and kana data files — every per-character URL ends up in
dist/sitemap.xmlautomatically. The Vite content hash in chunk filenames is the cache invalidation strategy; no manual cache busting on top.
Privacy
There is no account to create, no server holding learner data, and no analytics on individual learning behavior. Names, streaks, sessions, answers, and goal preferences live in IndexedDB on the device. Moving between devices means downloading a backup file and restoring it — that is the only data path. The promise of "no tracking, no ads, no upsells" is enforced at the codebase level: any addition of GA, Plausible, Posthog, or Sentry is treated as a brand-and-trust regression and discussed before merging.
Tech stack
- Vite + React 19 + TanStack Router for routing, build tooling, and code-splitting (file-based routes auto-generate
routeTree.gen.ts). - TypeScript (strict) —
tsc --noEmitis a deploy gate; type errors block deployment, including unused locals and parameters. - Tailwind 3 with custom design tokens (sumi, vermilion, cream, gold).
- Bun as the package manager, test runner, and build orchestrator.
bun.lockis the authoritative lockfile. - Native browser APIs — IndexedDB, BroadcastChannel, CompressionStream, Web Share, Canvas,
crypto.randomUUID,useSyncExternalStore. No Dexie, no html2canvas, no SRS library. - Radix UI via shadcn-style wrappers for accessible dialog, dropdown, toggle-group, tabs, and radio primitives.
- hanzi-writer for kanji stroke-order rendering. Stroke data sourced from the Make Me a Hanzi project (combining KanjiVG with Arphic font data).
- Docker plus nginx for production deploys; manual chunking in
vite.config.tsseparatesreact-vendor,router-vendor,radix-vendor, and the data files into long-lived caches.
Design system: Editorial Sumi
The aesthetic is refined editorial typography with traditional Japanese accents. The bold choice is restraint — kanji characters are the visual heroes; the chrome frames them.
- Two-voice typography. Newsreader (variable, opsz + wght axes, lining and tabular figures) for display headings and titles; system sans for body, buttons, and microcopy; Noto Serif JP (subsetted to
漢字only) for the wordmark glyph. - Color palette. Sumi black for text, cream paper for surfaces, vermilion for accents (underlines, focus rings, the seal-stamp glyph), gold for "earned" states (streak pills, goal completion). Never blue. No gradients.
- Master-detail interaction lock-in. Documented after iterating through modal, dedicated route, inline expansion, and back. The grid never reflows on click. References live alongside the components in the codebase so future contributors don't relitigate the decision.
- Editorial segmented control. Stacked Fraunces letterform with italic descriptor and a 2px vermilion underline on the active item. Used for JLPT level selection and quiz difficulty.
My role
End-to-end design and implementation:
- Migrated the application from Next.js to Vite plus TanStack Router, dropping legacy framework weight and unlocking native ESM, Workbox-driven PWA, and faster cold starts.
- Built the local-first progress system from scratch — IndexedDB schema, store with subscribe-notify, hooks via
useSyncExternalStore, sessions with idle timeout, streaks with timezone-aware day boundaries, and daily goals with a flexible additive-field schema policy. - Designed the Editorial Sumi visual language: typography hierarchy, color tokens, segmented-control pattern, master-detail layout standard.
- Implemented WCAG 2.1 AA accessibility throughout — keyboard navigation, focus management, live regions, contrast tuning, motion preferences, screen-reader walkthroughs.
- Specified the backup schema with a forward-migration policy so future data evolution doesn't break old user backups.
- Set up the PWA, service-worker caching strategy, build chunking, and runtime-cached stroke data so the heavy data files don't block first paint.
Data sources
Kanji stroke order data comes from the Make Me a Hanzi project (which combines KanjiVG with Arphic font data) via the hanzi-writer library. JLPT level groupings are community-curated — the JLPT organization has not published official kanji lists since the 2010 test reform.