One brick definition → seven identical runtimes.
Spec-driven codegen for UI8Kit ui/ primitives. Define props, variants, and a render tree once; emit idiomatic Templ, React, Svelte, Vue, SolidJS, Latte, and Twig with the same DOM, design tokens, and ARIA contract — verified by parity tests, not by review.
| Fact | Value |
|---|---|
| Inventory | 33 bricks · 63 parts · 7 runtimes |
| PHP templates | 59 / 63 parts (4 skipped by structural predicate) |
| License | MIT |
| Runtime | Output | Composition |
|---|---|---|
| Go Templ | ui/<brick>/<brick>.templ + _gen.go | SSR, { children... }, Attrs |
| React 19 | ui/<brick>/<brick>.tsx | forwardRef, asChild / Slot |
| Svelte 5 | ui/<brick>/<Part>.svelte | runes, snippets, <svelte:element> |
| Vue 3 | ui/<brick>/<Part>.vue | <script setup>, slots, <component :is> |
| SolidJS | ui/<brick>/<brick>.solid.tsx | splitProps, Dynamic, *Classes() |
| Latte | ui/<brick>/<Part>.latte | typed {parameters}, n:attr |
| Twig | ui/<brick>/<Part>.html.twig | ui8kit_attr_str, include with |
Plus a static HTML export via bun run build:html → examples/html/.
All runtimes share the same colocated *.variants.json (CVA-style recipes). Attribute logic lives in a small typed expression IR and is printed idiomatically per target.
PHP note: Latte/Twig skip
breadcrumb/Breadcrumb,select/Select, andicon/Icon(loops over typed items or branched roots). Class helpers inphp/UI8Kit/Classes.phpare still emitted for every part. Details: docs/php-complex-parts.md.
Hand-writing .templ + .tsx (and then Svelte, Vue, Solid, Latte, Twig) multiplies drift. This engine inverts the workflow:
- Brick definitions (
bricks/<name>/<name>.def.ts) — single source of truth - Canonical renderer (
src/domain/render.ts) — executable DOM spec - Emitters — one printer per runtime
- Parity tests — SSR every generated component and assert normalized DOM equals the canonical output
Identity across runtimes is a property of the test suite, not a convention.
Prerequisites:Bun. Go 1.25+ and PHP 8.1+ / Composer are optional (parity suites skip when missing).
bun install
bun run check # validate all brick definitions
bun run generate # emit all seven runtimes → generated/
bun test# domain units + DOM parity
bun run verify # check + typecheck + tests (CI equivalent)Eight welcome screens (generated primitives + shadcn tokens) live in examples/:
bun run generate &&cd examples && bun install && bun run build:css
bun run dev:templ # :8080
bun run dev:react # :5173
bun run dev:svelte # :5174
bun run dev:vue # :5175
bun run dev:latte # :5176
bun run dev:twig # :5177
bun run dev:solid # :5178
bun run build:html # → examples/html/bun src/infrastructure/cli.ts generate \
--out generated \
--go-module github.com/ui8kit/ui \
--runtimes templ,react,svelte,vue,solid,latte,twigAlso available: bun run generate:latte-bundle for a Latte-only package — see docs/latte-bundle.md.
generated/
go.mod composer.json
cmd/parity/ # Go + PHP render harnesses
php/UI8Kit/ # Rt.php, Classes.php, TwigExtension.php
utils/ # shared runtime support (Go + TS)
ui/
index.ts | index.svelte.ts | index.vue.ts | index.solid.ts
button/
button.variants.json # shared CVA recipe
button.shared.ts # TS types + classes helpers
button.templ | button.tsx | button.solid.tsx | Button.svelte | Button.vue
Button.latte | Button.html.twig
Consume: Templ → templ generate && go build · TS → Vite (@vitejs/plugin-vue, @sveltejs/vite-plugin-svelte, vite-plugin-solid) · PHP → composer install in generated/ with a loader rooted at generated/ui.
Peer deps: react / svelte / vue / solid-js + clsx + tailwind-merge; PHP: latte/latte ^3 or twig/twig ^3.
bricks/ # 33 definitions + variants.json + fixtures
src/
domain/ # pure model — expr IR, render, validate (zero I/O)
emitters/ # one printer per runtime
application/ # generate pipeline
infrastructure/ # CLI
runtime/ # support files copied into generated/utils
tests/ # domain units + cross-runtime parity
examples/ # eight welcome previews
docs/ # full documentation
| Principle | How it shows up |
|---|---|
| Single source of truth | Brick defs are data, not templates |
| Closed IR | 13 expression kinds · 5 node kinds — exhaustive per emitter |
| Open emitters | New runtime = new printer, model unchanged |
| Fail fast | Validation before any emit |
| Parity as gate | Canonical renderer is the spec; tests enforce it |
Deep dive: docs/architecture.md.
Decided once, applied everywhere (docs/contract.md):
| Concern | Rule |
|---|---|
| Prop naming | Canonical Go PascalCase → React / Svelte / Vue native names |
| Variants | Shared *.variants.json; TS literal unions; Go embedded Variants |
| Class merge | Recipe → variants → base → fragments → state → caller (cn / Cn) |
| Children | Templ / React / Svelte snippets / Vue slots — or none |
| Root tags | Same TagGroup allow-lists in every runtime |
| Booleans vs ARIA | Native bools present-or-absent; ARIA always "true" / "false" |
| Rest / Attrs | Spread last — caller wins |
| Behavior | Presentation-only; client hooks via @ui8kit/aria at app level |
// bricks/button/button.def.ts (abridged)exportdefaultbrick({id: "ui.button",dir: "button",recipes: {button: {file: "button.variants.json",recipe: buttonVariants}},parts: [{name: "Button",recipeId: "button",asChild: true,// React-only; other runtimes use ButtonClassesclasses: {recipe: {variant: "Variant",size: "Size"},state: [{test: prop("Disabled"),classes: "pointer-events-none opacity-50"}],},props: [pVariant(),pSize(),pClass(),pStr("Type"),pStr("Href"),pBool("Disabled"), ...controlPassthroughProps(),pAttrs(),pChildren()],// Href set → <a>; otherwise <button>. asChild / ButtonClasses remain for exotic roots.render: when(isSet(prop("Href")),[el("a",…)],[el("button",…)]),}],});- Create
bricks/<name>/<name>.def.ts+<name>.variants.json(+ optional.data.jsonfixtures) - Register in
bricks/index.ts bun run check→bun test
Authoring guide: docs/bricks.md.
In:ui/ primitives — 33 bricks, 63 parts (Tiers A–E), including multi-part composites (card, table, form, select, breadcrumb).
Out (later phase): behavior-driven components/ (Sheet, Tabs, Popover, Combobox, Menu, Toast) that need @ui8kit/aria. The IR and emitters are ready once that contract is specified.
Deliberate deviations from upstream hand-written pairs (empty attrs omitted, rest spread last, etc.): docs/contract.md.
| Doc | Topic |
|---|---|
| Overview | Goals, runtimes, scope |
| Getting started | Install, generate, consume |
| Architecture | Domain, IR, emitters, pipeline |
| Bricks | Authoring & validation |
| Runtimes | Per-runtime notes |
| Contract | Cross-runtime rules |
| Testing & CI | Parity harnesses |
| Examples | Local previews |
| Latte bundle | Latte-only package |
MIT © UI8Kit