Caio Alfonso · 2026

kanso-ui

A headless, accessible component library for Vue 3 and React 19, built on one framework-agnostic core. Every behaviour (state, keyboard handling, ARIA, focus management) is written once in plain TypeScript with zero dependencies; the two adapters bind reactivity and render, and nothing else. Seven components, conformant to their WAI-ARIA Authoring Practices patterns, published to npm.

TypeScriptVue 3React 19pnpmtsupVitestPlaywrightAstro Starlight
Two identical toggle switches side by side, labelled Vue 3 and React 19, both switched on.

The thesis

The accessibility lives once, the frameworks are skins.

Writing a component library twice, once per framework, duplicates the expensive part, the accessibility, and guarantees the two implementations drift. So kanso-ui puts every behaviour in a framework-agnostic core: a pure state reducer plus a connect() function that returns neutral prop bags. Each adapter supplies a normalizeProps translator and renders. The core imports neither vue nor react, not even as a type, and CI fails the build if that ever stops being true.

The cost is a layer of indirection, and it is worth stating honestly: a single one-off component is more code this way. It pays off at the second framework and at every component after the first, because the hard part is written once.

Accessibility that is measured, not claimed

Automated scanning is a floor, not a ceiling. axe cannot detect a broken arrow-key handler, a focus trap that leaks, or a dialog that never moves focus inside itself. So every interactive component ships an explicit keyboard test asserting its full APG key map, and focus behaviour is verified in a real browser rather than in jsdom, whose focus model is not trustworthy for it.

Colour contrast is a CI gate rather than a design review. A script parses the OKLCH tokens, converts them to sRGB and then to WCAG relative luminance, and measures 22 declared pairs against their required ratio: 4.5:1 for text, 3:1 for anything that is the only indicator of a state. Change a colour token and the build tells you what you broke, and which component it broke it for.

The parts that were hard

Every interesting failure mode in this architecture is silent. The core emits React's camelCase event names because turning onKeyDown into onKeydown for Vue is mechanical while the reverse is not. But lowercase the whole name and you get onkeydown, which fails Vue's event test, gets assigned as a DOM property instead, works perfectly on a fresh mount, and is dropped on hydration. A server-rendered component that is simply inert, with no error anywhere. Only an explicit SSR test catches it.

Ids are the same shape of problem: a library-generated counter produces hydration mismatches, so core accepts ids as input and each adapter passes its framework's own useId() in.

The defect I am fondest of was not found by any test. Consolidating seven duplicated install sections into one page surfaced that all seven told you to import the tokens stylesheet and the component stylesheet, but not the base one, which is where the :focus-visible ring lives. The documented install produced a control with no visible focus indicator: a WCAG 2.4.7 failure, in an accessibility library, in its own instructions. It survived because the docs site imports the bundled stylesheet, so every live preview on the site got the focus ring for free and proved nothing about the code printed above it.

Restraint is the design

Kanso (簡素) means simplicity through the elimination of clutter: not emptiness, but restraint in service of function. Seven components, done properly, rather than forty. Vue and React only, though the architecture would allow a third adapter cleanly. No animation library, no form-library integration, no i18n layer, no in-browser code REPL in the docs. Saying no is what kept it shippable.

Outcome

Seven components (Switch, Tabs, Dialog, Menu, Field, Button and Card), each with a shared core, both adapters, full APG keyboard coverage and a server-render test. Published to npm under @caioalfonso. 894 unit tests and 213 browser tests, zero axe violations across the docs site, and every colour pair measured against its WCAG requirement in CI.