07 / Adoption

Where to start

Radix Primitives covers behaviour and accessibility for the overlay and form components, and ships genuinely unstyled. Take the anatomy conventions from it, keep the token contract yours.

Radix Primitives

Dialog, popover, menu, select, tabs, tooltip. Focus traps and keyboard behaviour you should not rewrite.

React · MIT

Zag.js

The same behaviours as framework-agnostic state machines. Worth adding the day a non-React surface appears.

Any framework · MIT

Open Props

A ready primitive tier. Import the scales, then map them onto your own semantic names — do not expose it directly.

Plain CSS · MIT

Style Dictionary

One JSON source compiled to CSS, JS, and Figma variables. Add it when the token list stops fitting in your head.

Build step · Apache 2.0

What actually needs Radix

Five of the built components need a behaviour library; the rest are native elements plus the token contract. The split is not arbitrary — everything on the list is either floating (needs positioning, collision and a portal) or overlay (needs a focus scope and an animated exit). Nothing in the form or data layers needs it, which is why those were the right things to build first.

ComponentDependencyWhat it buys that native does not
Dialog, Sheet@radix-ui/react-dialogNative dialog cannot animate its own close, and it does not nest — a dialog opening a second dialog breaks the focus scope.
Menu…react-dropdown-menuCollision detection, so it flips instead of clipping at a viewport edge. Plus typeahead and submenus.
Popover…react-popoverPortalling. A popover parented to its trigger is clipped by any ancestor with overflow: hidden — which every card here has.
Tooltip…react-tooltipHover intent and delay grouping, so dragging a cursor across a toolbar does not fire every tooltip in turn.
Select…react-selectThe one field that needs it: a native select cannot style its own options or render anything but text inside them.
ButtonA button element and the token contract. Nothing to add.
Input, TextareaNative keeps IME composition, autofill and spellcheck — all of which a custom editor has to rebuild.
Checkbox, Radio, Switchappearance: none keeps :checked, :indeterminate and radio-group arrow keys while replacing the appearance entirely.
Table, ListSemantic table elements. Sorting is state, not behaviour.
Builder shellThe Sidebar primitive and a container query. Drag-and-drop is the one place to reach for Motion rather than Radix.

Radix supplies behaviour only — it ships no appearance at all, including no focus ring, which is why Focus is in the contract rather than inherited. Where a Radix primitive is wrapped, the wrapper's job is to add the data-part attributes and nothing else, so the skin surface is identical whether the behaviour underneath is Radix, Zag or the platform.

Suggested sequence

  1. 01

    Freeze the token contract. Names only — values come later and change freely.

  2. 02

    Ship the seven layout primitives as plain CSS. They have no dependencies and unblock every other team immediately.

  3. 03

    Build button, input, and select against the contract. Prove the swap by writing the high-contrast skin second, not last — it finds the hardcoded values.

  4. 04

    Wrap the Radix overlays. Dialog, popover, menu, tooltip.

  5. 05

    Then the builder surface — canvas, section panel, inspector — composed entirely from the above. If it needs something new, that something belongs in the system first.