The root entry is a client boundary
Importing from @gunkin/uiify is convenient and marks a client boundary. In a Server Component, import the component’s own entry instead; it has no "use client" directive.
// convenient: a client boundary
import { Accordion } from "@gunkin/uiify";
// server-compatible: no client JavaScript
import { Accordion } from "@gunkin/uiify/components/accordion";Controlled variants under /client
A controlled component always needs a client boundary, because something must hold the value and re-render. Controlled variants therefore live under a /client subpath, not behind a prop.
import { Modal } from "@gunkin/uiify/components/modal"; // uncontrolled, 0 kB
import { ModalClientContent } from "@gunkin/uiify/components/modal/client"; // controlledThe hydration cost then appears in an import you wrote, not in a later bundle report. The uncontrolled component is the one that works before hydration.
Composing across the boundary
A Client Component can render Server Components passed as children, which keeps an interactive shell thin while its content stays on the server.
// server component
<ClientShell>
<ExpensiveServerRenderedThing />
</ClientShell>Avoid React context
Tier 0 bans React context: Server Components cannot use context, so parts that communicate through a provider cannot render on the server. Parts are wired with HTML attributes and deterministic ids instead.
Follow the same rule when you build on @gunkin/uiify/elements: the browser resolves commandfor, popovertarget, for and name without JavaScript. See Architecture
Before hydration
Tier 0 components pass the no-JavaScript suite in full: they are operable, not only visible. Tier 1 keeps native state working and loses only what its island adds. Tier 2 renders meaningful markup; keyboard navigation, filtering and ARIA sync wait for hydration.
Each component page shows its tier and measured budget; Proof explains how both are produced.