Client/server boundaries
Directive preservation
Each client entry ships with'use client' at the top of its bundle:
So
cn(), If, Spinner, and the other directive-free entries can be used from Server Components:
'use client' only when it uses hooks or event handlers — for example, a form built with useForm():
Trans (i18n with next-intl)
Trans is a client component that reads next-intl’s context, so it must render under a NextIntlClientProvider. The Ekanos host mounts one at its root, and @ekanos/harness’s RootLayout mounts one for you:
Fallback chain
A key (ns:key shape) resolves to the host’s message when it exists. Otherwise Trans falls back in this order:
defaults(interpolated withvalues, withcomponents)children- The key itself
Trans throws. Anywhere else (a Storybook, a bare unit test), wrap the tree in a NextIntlClientProvider; empty messages are enough, because a missing key renders defaults:
Interpolation
defaults strings use {{variable}} interpolation:
Components
Acomponents map renders tags in the string as elements:
Spinner
Spinner draws an inline SVG (a faint full ring under a quarter-turn arc), so unlike Icon it needs no icon font — only the Tailwind utilities it emits (animate-spin, size-*, text-*):
<span role="status" aria-label="Loading">; other props land on that span. It honours prefers-reduced-motion via motion-reduce:animate-none, freezing the arc into a static busy glyph. It is hook-free, so it renders from a Server Component or a Client Component.
Base UI (not Radix)
Popup-shaped components (Select, DropdownMenu, Dialog, Tooltip) are built on @base-ui/react, not Radix. The main difference you’ll notice:
No asChild prop
Instead, pass a render prop to the trigger:
render, SelectTrigger renders a <button> by default.
Polymorphic components
Some components (triggers, items) accept arender prop to change the element:
Form with react-hook-form
Forms usereact-hook-form and @hookform/resolvers for validation:
FormMessage displays the field’s validation error. zod here is your own dependency; @ekanos/ui does not ship it.
Type imports
Some entries export named prop types (ButtonProps from button, BadgeProps from badge, the PromptInput*Props types from ai-prompt-input). For the rest, use React.ComponentProps<typeof Component>:
Peer dependencies
You provide these (react-hook-form and @hookform/resolvers for form usage, next-intl for Trans):
@base-ui/react— the primitives (Base UI, not Radix)ai— used for types only (ChatStatus,FileUIPartinPromptInput’s props); noaicode is imported at runtimeclass-variance-authority— variant managementclsx+tailwind-merge— class mergingcmdk— command palette/comboboxnanoid— unique IDs
dist/.
'use client' in Next.js App Router
Client entries keep their directive in the published bundle (the build re-applies it per entry), so Next.js treats them as client components:
Per-component entries
Each component is its own subpath with its own bundle, and the package declares"sideEffects": false, so importing @ekanos/ui/button does not pull in @ekanos/ui/select. There is no root barrel export. Entries that render icons (for example select and dropdown-menu) share the one icon bundle, so a single IconStyleOverrideProvider reaches every icon.
Base UI patch divergence (minor)
The Ekanos monorepo patches@base-ui/react to widen a trigger drag-boundary offset in menu, select, and combobox triggers. The published package resolves the unpatched package from npm, so partner-side menus keep upstream’s slightly tighter press-and-drag selection boundary. Cosmetic only.
CSS-in-JS vs. Tailwind
All components emit static Tailwind class strings, not CSS-in-JS: no styles are generated or injected at render time. The tradeoff is that you must load eitherstyles.css (pre-compiled) or theme.css in your Tailwind build so the utilities exist. See Styling.