‮Skip to content‬
uiify

‮Architecture‬

‮uiify has five layers, each published on its own and never importing the one above. One rule shapes all of them: a component must work before hydration.‬

‮The hydration gap‬

‮Streaming SSR leaves a gap between first paint and the end of hydration, and under Suspense every late boundary has its own. In that gap, a‬ useState ‮accordion is visible but inert, while a‬ <details> ‮accordion works from first paint.‬

‮Two documents render the same accordion without any‬ <script> ‮tag:‬ ‮one built on‬ `<details>` ‮and‬ ‮one built on‬ `useState`‮. Open both: the first opens, the second cannot. View the source to confirm.‬

‮The goal is zero unnecessary renders, zero provider-wide fan-out and zero synchronous layout work from library JavaScript — not zero renders, since state must render somewhere. JavaScript is zero by default and thin only where accessibility requires it.‬

‮Tiers‬

‮Each component declares a tier in a manifest that the build reads. The manifest drives the size budget, the RSC-purity lint, the Playwright suite and the tier badge on each component page.‬

‮Tier‬‮Budget‬‮Without JavaScript‬‮What it means‬
‮Tier 0‬
‮RSC — zero client JavaScript‬
‮0 kB‬‮Passes the no-JavaScript suite in full.‬‮State lives in a native element, or there is no state. Every React hook is banned, including useMemo and useRef.‬
‮Tier 1‬
‮RSC shell + thin client island‬
‮≤ 1 kB‬‮Degraded behavior, documented per component.‬‮The native element still owns the state. JavaScript only syncs ARIA or adds what the platform lacks, such as hover intent or auto-dismiss.‬
‮Tier 2‬
‮Client component — pre-hydration SSR‬
‮per-component‬‮Renders meaningful markup before hydration.‬‮A single client entry. The server emits native-first markup where it can; keyboard navigation, filtering and ARIA sync wait for hydration.‬

‮Tiers are also a migration tool: gates switch on per component, so the architecture tightens one component at a time.‬

‮The layer stack‬

@gunkin/uiify/hooks       SSR-safe React hooks — the substrate
@gunkin/uiify/core        headless behaviour: focus scope, dismiss layers, anchoring
@gunkin/uiify/elements    native-mechanism wrappers — zero client JavaScript
@gunkin/uiify/components  unstyled components, RSC by default
@gunkin/uiify/styles      optional CSS design layer

‮Lower layers never import higher ones.‬ @gunkin/uiify/elements ‮is the strictest: it may import only‬ @gunkin/uiify/core/render ‮and React types.‬

‮Why elements are a public layer‬

‮Most apps never import‬ @gunkin/uiify/elements ‮directly. It is published for two reasons.‬

‮First, it makes the design verifiable. When a component page says‬ _‮built on‬ elements/dialog_‮, that element page shows the exact markup and‬ data-* ‮contract underneath.‬

‮Second, composition: if the component anatomy does not fit your design, build your own on the same wiring instead of reimplementing the mechanism.‬

‮A module joins the layer only if it meets all five rules: one native mechanism, no JavaScript behavior, two or more consumers in‬ @gunkin/uiify/components‮, state a CSS selector can read, and less duplicated markup rather than more indirection.‬

‮Modules are named after platform features —‬ details, dialog, popover, invoker ‮— not UI concepts. A component never shares a name with an element, which is why‬ Modal ‮is built on‬ dialog. ‮Browse Elements‬

‮Why React context is banned in Tier 0‬

‮Moving state into the DOM achieves nothing if the parts still talk through React context. Server Components cannot use context, so a‬ Root ‮and‬ Item ‮that share a provider cannot render on the server, however native their markup.‬

‮The wiring must survive serialization, so parts connect through HTML:‬ <details name> ‮for grouping,‬ commandfor ‮and‬ popovertarget ‮for references the browser resolves, and ids derived from props. A‬ useId ‮value cannot reach sibling parts without context.‬

<Accordion id="faq">
  <Accordion.Item value="shipping">   {/* id="faq-shipping" */}

‮The cost: a root needs an‬ id‮. Where composition is unnecessary, a component takes data and renders the tree itself.‬ ‮See Server components‬