
Radix Primitives: Behaviour Without the Look
Every custom dropdown I have audited had the same six accessibility bugs. Radix ships the keyboard handling and focus management unstyled, so you keep your design and lose the bugs.
I audit a lot of frontends. Custom dropdowns, dialogs and tabs fail in the same places every time:
- Focus is not trapped in the open dialog, so tabbing walks into the page behind it.
Escapedoes nothing.- Focus is not returned to the trigger on close, so keyboard users land at the top of the document.
- Arrow keys do not move between menu items.
aria-expandedis missing, so a screen reader announces a button with no state.- The overlay is not marked
aria-hidden, so the background is still readable.
None of this is exotic. It is just a lot of detail, and it gets cut when the design review is tomorrow.
What Radix actually gives you
Unstyled components that implement the WAI-ARIA authoring practices and nothing else. No colours, no spacing, no opinions about your design system.
import * as Dialog from '@radix-ui/react-dialog'
<Dialog.Root>
<Dialog.Trigger className="btn">Open</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay className="overlay" />
<Dialog.Content className="panel">
<Dialog.Title>Confirm</Dialog.Title>
<Dialog.Description>This cannot be undone.</Dialog.Description>
<Dialog.Close className="btn-ghost">Cancel</Dialog.Close>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
Every one of those six bugs is handled. The classNames are entirely yours — this is why a brutalist site and a soft SaaS dashboard can both sit on the same primitives without either looking like the other.
The composition detail worth copying
Each part is a real element you can style and target, rather than a monolith configured by props:
.panel[data-state='open'] { animation: pop 160ms ease-out }
.panel[data-state='closed'] { animation: fade 120ms ease-in }
State lives in data attributes, so animation is CSS rather than a JavaScript API you have to learn. That also means you can respect prefers-reduced-motion in the same stylesheet as everything else.
Where it costs you
You are writing the styles. For a small internal tool where a pre-styled kit would have done, that is real work you did not need to do. Radix pays off when the design is specific — which, on client work, it always is.
Bundle-wise you import per component, so a site using dialog and dropdown does not ship the tabs implementation.
Why this is the layer to standardise on
Most modern component libraries are Radix with a coat of paint. Building on the primitives directly means you own the paint and inherit the correctness, and you are never one design change away from fighting somebody else's stylesheet.
For bilingual work there is a further win: the primitives are direction-aware, so an RTL layout gets the correct arrow-key behaviour without a special case.
Resources
- Repo: radix-ui/primitives
- Docs: www.radix-ui.com/primitives
- Video walkthroughs: YouTube: radix ui primitives tutorial
- Related: Accessible drag and drop with dnd-kit
Need this built properly?
I build secure, fast, bilingual platforms for clients across Egypt, Saudi Arabia, the UAE and Kuwait.


