This episode covers how to build accessible components using Headless UI, Radix, and Tailwind UI, as well as structuring component documentation and development with Storybook integrated with Tailwind.

Interactive components like dialogs, menus, and tabs hide many accessibility traps: focus traps, keyboard navigation, and ARIA. Writing them from scratch over and over is wasteful and bug-prone. Episode 19 covers the headless UI approach: logic-only components that are then styled with Tailwind.
This pattern separates two concerns: behavior, provided by the headless library, and appearance, entirely in your hands via utility classes. The result is accessible components with fully custom styling — and Tailwind UI and Storybook round out the workflow.
Headless UI from Tailwind Labs provides unstyled components: menu, listbox, dialog, disclosure, and more. Installing it:
npm install @headlessui/reactThen styling is done purely with Tailwind utilities:
import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react";
export function Dropdown() {
return (
<Menu>
<MenuButton className="rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600">
Opsi
</MenuButton>
<MenuItems className="absolute mt-1 w-40 rounded border border-gray-200 bg-white p-1 shadow-lg">
<MenuItem>
<button className="block w-full rounded px-3 py-1 text-left hover:bg-gray-100">
Edit
</button>
</MenuItem>
<MenuItem>
<button className="block w-full rounded px-3 py-1 text-left hover:bg-gray-100">
Hapus
</button>
</MenuItem>
</MenuItems>
</Menu>
);
}MenuButton and MenuItem handle keyboard navigation, focus, and ARIA automatically — you're only responsible for appearance through classes like hover:bg-gray-100.
Radix UI is a collection of more granular, unstyled primitives. The usage pattern is the same: wrap with Tailwind classes. Choosing between Headless UI and Radix is usually a matter of taste and requirements — both produce accessible components with no built-in styling.
Tailwind UI is a premium, paid component collection from Tailwind Labs. Its components are written fully in utility classes — with no other dependency. You copy the markup and adjust:
<div class="flex items-center gap-4 rounded-lg bg-white p-6 shadow">
<img src="avatar.png" alt="Avatar" class="h-12 w-12 rounded-full" />
<div>
<p class="text-sm font-medium text-gray-900">Nama Pengguna</p>
<p class="text-sm text-gray-500">Deskripsi singkat profil.</p>
</div>
</div>Tailwind UI's strengths: consistent, already responsive, and directly customizable. Its weakness: paid, and it doesn't cover components with complex logic — for those you still need Headless UI or Radix.
Storybook is where you develop and document components in isolation. Initialize:
npx storybook@latest initFor Tailwind to work in Storybook, import the global CSS via a decorator:
import "../src/styles.css";
import type { Preview } from "@storybook/react";
const preview: Preview = {
decorators: [
(Story) => (
<div className="p-4">
<Story />
</div>
),
],
};
export default preview;After that, each component gets its own stories:
import { Button } from "./Button";
export const Primary = {
args: { variant: "primary", children: "Simpan" },
};
export const Ghost = {
args: { variant: "ghost", children: "Batal" },
};These stories aren't just documentation — episode 20 will use them as the foundation for visual regression testing.
Tip
The best combination for a component library: Headless UI or Radix for logic, Tailwind utilities for appearance, Storybook for development, and CVA from episode 15 for the variants API. These four elements form the foundation of accessible, consistent, and documented components.
Episode 19 built the component library foundation: accessible components with Headless UI and Radix, ready-made styles from Tailwind UI, and isolated documentation and development with Storybook.
Key takeaways:
Next, in episode 20, we'll cover testing, observability & monitoring — visual regression testing with Playwright and Chromatic, performance monitoring with Lighthouse and Web Vitals, and preventing CSS regressions in CI.