Skip to content
Join UI

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

LayerChoice
FrameworkNext.js App Router
UI runtimeReact 19 with Server Components
LanguageTypeScript, strict
StylingTailwind CSS v4 with CSS-variable tokens
PrimitivesRadix UI
AnimationMotion for React
IconsLucide
HighlightingShiki at build time, with themes built from these tokens
ContentMDX

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.

Where to go next