Introduction
What Join UI is and how it is built.
Join UI is a catalog of React components for Next.js. It is not a package you install and import from — it is a registry that writes real source files into your repository, which you then own outright.
If you have used shadcn/ui, the model will be familiar. What Join UI adds is a two-layer design system — a soft, entirely neutral interface wrapped around components that carry hue through tokens — a focus on motion that stays cheap, accessibility that is designed in rather than retrofitted, and a per-component prompt built for coding agents.
What you get
Why open code
A component library that ships as a dependency has to guess. It guesses which
props you will need, which parts you will want to restyle, and which behaviours
you will want to change. Every guess it gets wrong becomes a wrapper, a
!important, or a fork.
Registry components invert that. The CLI resolves dependencies and writes files; after that there is no abstraction between you and the markup. When the design changes, you edit a file in your own repository instead of filing an issue.
The tradeoff is real and worth stating: you do not get updates for free. A component you installed six months ago stays exactly as it was until you reinstall it. For UI — the layer that diverges from upstream fastest — that is usually the trade you want.
Design principles
Colour is a token, never a literal
The palette splits in two. The chrome — navigation, type, rules, buttons, the
documentation itself — is neutral and has no brand hue in it at all. The ramp is
not greyscale either: it carries a trace of warmth in both themes, so this page
resolves to #13110f rather than to black and its ink to #f5f3f1 rather than
to white. Neither ramp reaches a pure value at either end, which caps the
strongest pair at about 17:1 rather than 21:1 — the deliberate cost of surfaces
that recede instead of glowing.
This site renders the dark theme and only the dark theme. The token system still ships both, because registry components get installed into other people's projects and plenty of those are light.
Emphasis is carried by ink, by material and by space rather than by an accent. The primary action is the one filled high-contrast shape in a view; a card, an active sidebar row and a selected tab are all the same move — a surface lifted one step off the page by a hairline and the faintest shadow. An accent was tried here and removed: pick one and it ends up on the link, the active row, the inline code span, the chip, the step number and the focus ring, at which point it has stopped meaning anything.
Components are allowed hue of their own, because they are the product rather
than the frame. They take it from four semantic families — --info,
--positive, --caution and --critical — rather than from a literal colour
or Tailwind's built-in palette, which is what keeps a component retintable in
one place. It is held at low chroma, because next to a chrome with no hue in it
a saturated chip reads as a different website rather than as emphasis.
No component leans on red-versus-green: every state it draws in colour is also drawn as a glyph and spelled out in words. See theming.
Motion is a cost, not a decoration
Every animation here is on transform and opacity only, so it runs on the
compositor. Pointer listeners are throttled to one frame and attach only while
the element is near the viewport. Long-running animations pause when they scroll
off-screen. And every effect has a static fallback under
prefers-reduced-motion: reduce — not a degraded component, a fully functional
one that simply does not move.
Accessibility is part of the component
Interactive components implement the WAI-ARIA pattern they look like: real
button elements, roving tabindex on tab lists, aria-expanded on
disclosures, focus traps on dialogs, and polite live regions for status changes.
Nothing here is hover-only — anything reachable with a pointer is reachable with
a keyboard.
Tokens, not dark: variants
Registry components theme entirely through CSS custom properties. None of them
use a dark: utility. That sounds like a small stylistic rule, and it is the
one that makes a component portable: dropped into a light project, or into a
scoped subtree that redeclares the variables, it themes correctly with no
dark: branch to get wrong. See dark mode.
One source of truth
Every surface of this site — the catalog card, the component page, the sidebar,
search, the install command, the /r/*.json registry item and the AI prompt —
is projected from a single typed metadata record per component. There is no
second copy to drift out of sync, and the build refuses to run when a component
declares a file that does not exist or points related at a slug that is not
real.
The stack
| Layer | Choice |
|---|---|
| Framework | Next.js App Router |
| UI runtime | React 19 with Server Components |
| Language | TypeScript, strict |
| Styling | Tailwind CSS v4 with CSS-variable tokens |
| Primitives | Radix UI |
| Animation | Motion for React |
| Icons | Lucide |
| Highlighting | Shiki at build time, with themes built from these tokens |
| Content | MDX |
Syntax highlighting is worth a note. Off-the-shelf Shiki themes are tuned for an
editor at full saturation, which on a documentation page reads as noise sitting
inside otherwise quiet type. The site ships its own pair — joinui-light and
joinui-dark — holding the same six grammar roles at roughly half that chroma,
over the same neutral base as everything else.