Each runtime is generated from the same brick definition. This page describes output shape, composition model, and consumption notes.
Output: ui/<brick>/<brick>.templ + ui/<brick>/<brick>_gen.go
| Feature | Implementation |
|---|---|
| Children | { children... } |
| Attrs escape hatch | templ.Attributes spread { p.Attrs... } (last) |
| Variants | //go:embed <brick>.variants.json → ButtonVariants |
| Class helper | func ButtonClasses(p ButtonProps) string |
| Dynamic tags | Local switch over TagGroup allow-list |
| Boolean attrs | disabled?={ p.Disabled } (present-or-absent) |
Build:
cd generated
templ generate && go buildModule path: set with --go-module at generate time (default github.com/ui8kit/ui).
Output: ui/<brick>/<brick>.tsx (all parts in one file)
| Feature | Implementation |
|---|---|
| Children | children prop |
asChild |
Slot from ui/slot/slot.tsx (parts with asChild: true) |
| Rest props | ...rest spread last |
| Prop naming | camelCase (variant, className, htmlFor, aria-label) |
| Class merge | cn() — Tailwind conflict-aware |
| Shared types | <brick>.shared.ts — literal unions + buttonClasses() |
Peer deps: react, react-dom, clsx, tailwind-merge.
Note: <textarea> uses defaultValue instead of children (React constraint); SSR DOM matches other runtimes.
Output: ui/<brick>/<Part>.svelte (one SFC per part)
| Feature | Implementation |
|---|---|
| Children | Snippets — {@render children?.()} |
| Prop naming | HTML-native (class, for, aria-label) |
| Dynamic tags | <svelte:element this={resolvedTag}> |
| Class merge | cn() |
Peer deps: svelte, clsx, tailwind-merge.
Bun test preload compiles .svelte with generate: "server" for parity tests (tests/preload-bun.ts).
Output: ui/<brick>/<Part>.vue (one SFC per part)
| Feature | Implementation |
|---|---|
| Children | <slot /> |
| Prop naming | HTML-native (class, for, …) |
| Dynamic tags | <component :is="resolvedTag"> |
| Attrs | inheritAttrs: false; class consumed into cn merge |
| Class merge | cn() |
Peer deps: vue, clsx, tailwind-merge.
Output: ui/<brick>/<brick>.solid.tsx (all parts in one file; .solid.tsx avoids colliding with React)
| Feature | Implementation |
|---|---|
| Children | children prop (JSX.Element) |
| Prop naming | camelCase variants; HTML-native class (+ className alias) |
| Dynamic tags | <Dynamic component={…}> from solid-js/web |
| Rest props | splitProps → {...rest} spread last |
| Class merge | shared *Classes() helpers (no asChild) |
| Shared types | <brick>.shared.ts — same as React/Svelte/Vue |
Composition: no Radix-style asChild. Style another element via buttonClasses() / *Classes() — same substitute as Svelte/Vue.
Peer deps: solid-js, clsx, tailwind-merge.
Bun/Vitest transform *.solid.tsx with babel-preset-solid / vite-plugin-solid (generate: "ssr") for parity tests.
Output: ui/<brick>/<Part>.latte
| Feature | Implementation |
|---|---|
| Parameters | Typed {parameters} block |
| Attrs | Single n:attr map; caller $attrs spread last |
| Children | $children HTML string, printed with |noescape |
| Expressions | UI8Kit\Rt helpers + generated preamble for derived values |
| Class merge | Rt::cn() — plain concatenation (like Go) |
Coverage: 60/63 parts (see PHP complex parts).
Composition pattern:
{capture $inner}
{include 'button/Button.latte', variant: 'default', children: 'Click'}
{/capture}
{include 'stack/Stack.latte', class: 'gap-4', children: (string) $inner}Peer dep: latte/latte ^3
Output: ui/<brick>/<Part>.html.twig
| Feature | Implementation |
|---|---|
| Attrs | {{ ui8kit_attr_str(computed, attrs) }} via TwigExtension |
| Children | children param, printed |raw |
| Dynamic tags | {% set uiTag = … %} interpolation |
| Class merge | Rt::cn() via extension |
Coverage: 60/63 parts (same predicate as Latte).
Composition pattern:
{% set inner %}
{% include 'button/Button.html.twig' with {variant: 'default', children: 'Click'} only %}
{% endset %}
{% include 'stack/Stack.html.twig' with {class: 'gap-4', children: inner} only %}Peer deps: twig/twig ^3; register UI8Kit\TwigExtension.
php/UI8Kit/Classes.php exposes static methods for every part with a classes contract:
UI8Kit\Classes::buttonClasses(['variant' => 'ghost', 'size' => 'sm', 'class' => 'mt-2']);
UI8Kit\Classes::iconClasses([...]); // even though Icon.latte is not generated
UI8Kit\Classes::selectClasses([...]); // even though Select.html.twig is not generatedUse these when building markup manually for parts the template emitters skip.
Not a generated runtime — built from the React welcome view:
bun run build:html # examples/react/script/generate-static.tsxWrites examples/html/index.html, examples/html/about/index.html with relative asset links.
bun src/infrastructure/cli.ts generate --runtimes react,svelteValid values: templ, react, svelte, vue, solid, latte, twig (comma-separated). Default: all seven.
| Runtime | Parity test | Skipped when |
|---|---|---|
| React, Svelte, Vue, Solid | tests/parity.test.ts |
never (always run) |
| Go Templ | tests/parity.templ.test.ts |
go not on PATH |
| Latte | tests/parity.latte.test.ts |
PHP or Composer unavailable |
| Twig | tests/parity.twig.test.ts |
PHP or Composer unavailable |
PHP parity excludes the three structurally complex parts symmetrically with the emitters.