How to Build Accessible Components with Shadcn UI and Radix
Learn how Shadcn UI and Radix primitives deliver WCAG-compliant accessibility out of the box. Covers keyboard navigation, screen reader support, ARIA labels, focus management, color contrast, and how SERP Blocks inherits accessibility from Radix under the hood.
SERP Blocks Team
Product
Accessibility is not an afterthought. For the roughly one billion people worldwide living with some form of disability, accessible web interfaces are the difference between being able to use your product and being locked out entirely. Beyond ethics, accessibility is a legal requirement in many jurisdictions — the ADA, EAA, and Section 508 all mandate that digital products be usable by everyone.
The good news: if you are building with Shadcn UI, you already have a significant head start. Shadcn UI is built on top of Radix UI primitives, which are some of the most rigorously accessible unstyled components available in the React ecosystem. Every component in the SERP Blocks library — all 1,200+ blocks across 50+ categories — inherits this accessibility foundation.
This post explains how Radix handles accessibility under the hood, what Shadcn UI adds on top, and what you still need to do yourself to ship truly WCAG-compliant interfaces.
Why Accessibility Matters for Component Libraries
When developers grab a pre-built component, they trust that it works correctly. "Works correctly" should include accessibility. A dropdown menu that cannot be navigated with a keyboard is broken — not just for screen reader users, but for power users who prefer keyboard shortcuts, users with motor impairments who rely on switch devices, and anyone temporarily unable to use a mouse.
The Web Content Accessibility Guidelines (WCAG) 2.2 define four principles. Content must be:
Perceivable — users can see or hear it (or have it read to them)
Operable — users can interact with it via keyboard, voice, or other input methods
Understandable — content and navigation are predictable and clear
Robust — content works across assistive technologies and browsers
Meeting WCAG 2.1 AA is the standard most organizations target. Level AAA is aspirational. Getting to AA with hand-rolled components is tedious and error-prone. Getting there with Radix primitives is almost automatic.
How Radix Handles Accessibility Under the Hood
Radix UI is an open-source library of unstyled, accessible React primitives. When Shadcn UI wraps a Radix primitive — say, Dialog, DropdownMenu, or Accordion — it gets the entire accessibility implementation for free. Here is what Radix provides at the primitive level.
Keyboard Navigation
Every interactive Radix component supports full keyboard navigation following the WAI-ARIA Authoring Practices Guide. This means:
Arrow keys navigate between items in menus, listboxes, and tab lists
Enter and Space activate buttons, links, and menu items
Escape closes overlays like dialogs, popovers, and dropdown menus
Home and End jump to the first and last items in a list
Tab moves focus between focusable elements in the correct order
This keyboard behavior is not something developers need to implement. Radix handles it internally. When you use the Shadcn DropdownMenu component, arrow key navigation between menu items works immediately.
Focus Management
Focus management is one of the hardest accessibility problems to solve correctly. Radix handles several critical patterns:
Focus trapping — when a dialog opens, focus is trapped inside it. Tab cycles through the dialog's focusable elements and does not escape to the page behind it.
Focus restoration — when a dialog or popover closes, focus returns to the element that triggered it. This prevents users from losing their place on the page.
Auto-focus — when a dropdown menu opens, focus moves to the first item automatically. No manual
useEffectwithref.current.focus()required.
These behaviors are baked into Radix primitives like Dialog, AlertDialog, Popover, DropdownMenu, ContextMenu, and HoverCard. Every Shadcn component that wraps these primitives inherits them.
ARIA Attributes
Radix automatically applies the correct ARIA roles, states, and properties. For example:
Accordionitems userole="region"witharia-labelledbypointing to their triggerDialogusesrole="dialog"witharia-modal="true"andaria-labelledbyfor the titleDropdownMenuusesrole="menu"on the content androle="menuitem"on each itemTabsusesrole="tablist",role="tab", androle="tabpanel"with properaria-selectedandaria-controlsattributesCheckboxusesaria-checkedwith"true","false", and"indeterminate"states
These are not attributes you need to add yourself. They are managed internally by Radix and update automatically as component state changes.
Screen Reader Announcements
Radix uses live regions and proper role assignments so screen readers announce state changes. When a toast appears, when a dialog opens, when an accordion panel expands — screen readers communicate these events to users without any extra work from the developer.
What Shadcn UI Adds on Top of Radix
Shadcn UI wraps Radix primitives with Tailwind CSS styling and provides a consistent API across components. From an accessibility perspective, Shadcn adds:
Visible focus rings — Shadcn's default styles include
focus-visible:ringutilities that draw clear focus indicators around interactive elements. These meet WCAG 2.4.7 (Focus Visible).Consistent color tokens — the theming system uses CSS custom properties for foreground and background colors, making it straightforward to maintain sufficient contrast ratios across themes.
Dark mode support — every Shadcn component works in both light and dark mode by default, ensuring that contrast ratios hold regardless of the user's preference.
Semantic HTML defaults — Shadcn components use appropriate HTML elements. Buttons are
<button>, not<div onClick>. Links are<a>. This ensures assistive technologies identify elements correctly.
What You Still Need to Handle Yourself
Radix and Shadcn give you a strong foundation, but they cannot know everything about your specific application. Here is what you are responsible for.
Labeling
Radix can set aria-labelledby on a dialog, but you need to actually provide the label content. Every form input needs a visible label or an aria-label. Every image needs meaningful alt text (or alt="" if purely decorative). Every icon button needs an accessible name.
// Good — the button has an accessible name
<Button aria-label="Close navigation menu">
<XIcon className="h-4 w-4" />
</Button>
// Bad — screen readers announce "button" with no context
<Button>
<XIcon className="h-4 w-4" />
</Button>Color Contrast
Shadcn's default theme generally meets WCAG AA contrast ratios (4.5:1 for normal text, 3:1 for large text). But if you customize your theme colors — especially for branded elements — you need to verify contrast yourself.
Use tools like the WebAIM Contrast Checker or the axe browser extension to validate. Pay special attention to:
Text on colored backgrounds (buttons, badges, alerts)
Placeholder text in form inputs
Disabled state colors
Link text that relies on color alone to distinguish it from surrounding text
Content Structure
Heading hierarchy matters. Screen reader users navigate by headings — jumping from h1 to h2 to h3. If your page skips heading levels or uses headings purely for styling, navigation breaks.
When building page sections with SERP Blocks, ensure the heading level in each block fits the overall page hierarchy. A hero section's title should typically be h1. Section headings below it should be h2. Sub-sections within those should be h3.
Dynamic Content
If your application adds or removes content dynamically — loading states, error messages, real-time updates — you may need aria-live regions to announce changes. Radix handles this for its own components (toasts, dialogs), but custom dynamic content needs manual attention.
Accessibility Across SERP Blocks Categories
The SERP Blocks library includes 1,200+ blocks across 50+ categories. Because every interactive component is built on Shadcn UI (and therefore Radix primitives), the accessibility baseline is consistent across all of them. Here is how that plays out in practice.
Navigation and Layout
The 17 Navbar blocks and 21 Footer blocks use semantic <nav> elements, proper landmark roles, and keyboard-navigable menus. Mobile menu toggles use the Radix Sheet primitive with focus trapping.
Forms and Input
The 33 Contact blocks, 17 Login blocks, 11 Sign Up blocks, and 10 Forgot Password blocks all use properly associated <label> elements, Radix form primitives, and visible error states. The 15 Payment Form blocks include ARIA descriptions for sensitive fields.
Interactive Patterns
The 21 FAQ blocks use the Radix Accordion primitive — keyboard navigable, with proper aria-expanded states. The 13 Modal blocks use Dialog with focus trapping. The 10 Cookie consent blocks use proper dialog patterns so users can dismiss them via keyboard.
Data Display
The 17 Table blocks use semantic <table>, <thead>, <tbody>, and <th scope> markup. The 10 Stat blocks use proper heading hierarchy. The 10 Timeline blocks use ordered lists for chronological content.
E-Commerce
The 25 Product blocks, 20 Product List blocks, 11 Shopping Cart blocks, and 10 Order Confirmation blocks maintain proper form labeling on quantity selectors, accessible price announcements, and keyboard-operable add-to-cart interactions.
How to Audit Your Components for Accessibility
Having accessible primitives is a great start, but you should still test. Here is a practical audit workflow.
Automated Testing
Run automated tools first to catch the low-hanging fruit:
axe DevTools — a browser extension that scans the page for WCAG violations. Catches missing alt text, contrast issues, missing form labels, and improper ARIA usage.
Lighthouse Accessibility audit — built into Chrome DevTools. Gives a score and a list of specific issues.
eslint-plugin-jsx-a11y — catches accessibility issues at build time. Add it to your ESLint config and it flags missing alt text, invalid ARIA attributes, and non-interactive element event handlers.
Automated tools catch roughly 30-40% of accessibility issues. They are necessary but not sufficient.
Manual Keyboard Testing
Unplug your mouse (or just do not touch it) and navigate your entire page using only the keyboard:
Can you reach every interactive element with Tab?
Can you activate buttons and links with Enter and Space?
Can you navigate dropdown menus with arrow keys?
Can you close dialogs and popovers with Escape?
Does focus return to the trigger element after closing an overlay?
Is there a visible focus indicator on every focused element?
If any of these fail, you have a keyboard accessibility bug.
Screen Reader Testing
Test with at least one screen reader:
VoiceOver on macOS (free, built-in) — activate with Cmd+F5
NVDA on Windows (free, open-source)
TalkBack on Android (free, built-in)
Listen for: Are headings announced with their level? Are buttons announced with their name? Are form inputs announced with their label? Are state changes (expanded, collapsed, checked, selected) announced when they happen?
Zoom and Reflow Testing
Zoom your browser to 200% and verify that:
No content is clipped or hidden
No horizontal scrollbar appears (content reflows)
Text remains readable
Interactive elements remain usable
WCAG 1.4.10 requires content to reflow at 400% zoom (or 320px viewport width). Test at both 200% and 400%.
Practical Tips for Maintaining Accessibility
Use Shadcn components as intended. Do not replace a
Buttonwith a styleddiv. Do not skip theDialogTitleinside aDialog. The Radix primitives enforce correct patterns — work with them, not around them.Add accessibility checks to your CI pipeline. Tools like
axe-corecan run in your test suite. Fail the build if new violations appear.Test with real users. Automated tools and manual testing catch technical issues. Usability issues — confusing navigation flows, unclear labels, overwhelming content — only surface through actual user testing.
Keep up with WCAG updates. WCAG 2.2 added new success criteria around focus appearance, dragging movements, and consistent help placement. The guidelines evolve, and your components should evolve with them.
Document accessibility behavior. When you build a custom component that extends a Radix primitive, document what keyboard interactions it supports and what ARIA attributes it uses. Future you (and your team) will thank you.
Building on a Solid Foundation
The combination of Radix primitives, Shadcn UI styling, and thoughtful development practices makes it realistic to ship accessible interfaces without heroic effort. The primitives handle the hard parts — focus trapping, keyboard navigation, ARIA state management, screen reader announcements. Shadcn adds visible focus indicators and consistent theming. You handle labeling, contrast verification, content structure, and testing.
Every block in the SERP Blocks library — from the 81 Hero blocks to the 132 Feature sections to the 10 Chat interfaces — is built on this foundation. When you drop a block into your project, the accessibility baseline comes with it. Your job is to verify it holds in your specific context, add the labels and alt text that only you can provide, and test the result.
Accessible components are not a feature. They are the baseline. Radix and Shadcn make that baseline achievable.