React yang aksesibel dengan Claude Code dan Radix UI
Bangun Dialog, Dropdown, dan Tabs dengan Claude Code dan Radix UI: instalasi, contoh React, styling, jebakan, dan checklist.
Membuat modal, dropdown, dan tabs di React terlihat mudah jika hanya diuji dengan mouse. Masalah sebenarnya muncul saat pengguna memakai keyboard: fokus tidak masuk ke dialog, Escape tidak menutup, screen reader tidak membaca judul, fokus tidak kembali ke tombol pemicu, atau menu keluar dari layar mobile.
Radix UI membantu karena menyediakan primitives tanpa style. Kamu tetap mengatur warna, spacing, tipografi, dan animasi, tetapi perilaku dasar Dialog, Dropdown Menu, dan Tabs tidak perlu dibuat dari nol. Dengan Claude Code, batas ini sangat penting. Agent tidak perlu menciptakan perilaku modal sendiri; ia cukup merakit primitive yang sudah punya pola interaksi.
Panduan ini menjelaskan cara memakai Claude Code dan Radix UI untuk membangun UI React yang aksesibel: command instalasi, prompt siap pakai, contoh TypeScript yang bisa disalin, catatan CSS, 3 use case, pitfall, link resmi, dan monetization CTA. Jika ingin layer komponen yang lebih siap pakai di atas Radix, baca panduan shadcn/ui dengan Claude Code. Untuk review aksesibilitas yang lebih luas, lihat juga panduan aksesibilitas Claude Code.
Kenapa Radix UI cocok untuk Claude Code
Dokumentasi resmi menyebut Radix Primitives sebagai library komponen level rendah yang fokus pada accessibility, customization, dan developer experience. Artinya Radix bukan library yang memaksakan tampilan. Ia menyediakan role, focus management, keyboard interaction, dan struktur komponen.
Radix Dialog mendukung mode modal dan non-modal, menahan fokus di dalam modal, bisa ditutup dengan Escape, dan memakai Title serta Description untuk membantu screen reader. Radix Tabs mengikuti WAI-ARIA tabs pattern dan menangani arrow keys, Home, serta End. Dropdown Menu menyediakan label, separator, radio item, dan submenu.
Claude Code bekerja dengan membaca konteks, mengedit, lalu memverifikasi. Kalau kamu meminta modal dari div biasa, diff akan panjang dan rawan. Jika prompt sudah menentukan Radix UI, Claude Code bisa fokus pada hal khusus project: state React, API, CSS class, analytics event, test, dan checklist.
flowchart LR
A["Berikan kebutuhan UI ke Claude Code"] --> B["Gunakan Radix UI untuk perilaku"]
B --> C["Hubungkan state React dan logic produk"]
C --> D["Terapkan CSS atau design token"]
D --> E["Cek keyboard, screen reader, dan mobile"]
Instalasi
Radix juga mendokumentasikan paket gabungan radix-ui, tetapi banyak project React masih memakai paket individual. Contoh di artikel ini memakai paket individual agar dependency di package.json jelas.
npm install @radix-ui/react-dialog @radix-ui/react-dropdown-menu @radix-ui/react-tabs
Dengan pnpm:
pnpm add @radix-ui/react-dialog @radix-ui/react-dropdown-menu @radix-ui/react-tabs
Jika Claude Code belum terpasang, cek dokumentasi resmi terlebih dahulu. Instalasi npm:
npm install -g @anthropic-ai/claude-code
Untuk tim, instalasi saja tidak cukup. Tentukan permission, cara update, command yang boleh dijalankan, dan siapa yang mereview perubahan UI sebelum dipublikasikan.
Prompt untuk Claude Code
Prompt yang baik menjelaskan kontrak interaksi, bukan hanya tampilan.
claude "Tambahkan confirmation Dialog, user Dropdown Menu, dan settings Tabs ke screen React + TypeScript yang sudah ada.
Syarat:
- gunakan @radix-ui/react-dialog, @radix-ui/react-dropdown-menu, dan @radix-ui/react-tabs
- pertahankan Dialog.Title dan Dialog.Description
- tombol close yang hanya berupa icon harus punya aria-label
- jangan hapus focus style; gunakan :focus-visible
- pakai design token yang sudah ada jika tersedia
- setelah selesai, tulis checks untuk keyboard, mobile, typecheck, dan accessibility"
Setelah Claude Code membuat perubahan, jangan hanya melihat screenshot. Review diff. Pastikan asChild tidak membuat button di dalam button, Dialog.Title tidak hilang karena alasan visual, dan CSS tidak menghapus outline fokus.
Contoh React yang bisa disalin
Contoh ini berisi confirmation dialog, user menu, dan settings tabs dalam satu file. Bisa dipakai di Vite, React SPA, atau Next.js Client Component. Untuk Next.js App Router, tambahkan "use client"; di baris pertama.
import * as React from "react";
import * as Dialog from "@radix-ui/react-dialog";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
import * as Tabs from "@radix-ui/react-tabs";
import "./radix-accessible-demo.css";
type User = { name: string; email: string };
type ConfirmDialogProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
title: string;
description: string;
confirmLabel?: string;
danger?: boolean;
onConfirm: () => Promise<void> | void;
};
export function ConfirmDialog({
open,
onOpenChange,
title,
description,
confirmLabel = "Confirm",
danger = false,
onConfirm,
}: ConfirmDialogProps) {
const [pending, setPending] = React.useState(false);
async function handleConfirm() {
setPending(true);
try {
await onConfirm();
onOpenChange(false);
} finally {
setPending(false);
}
}
return (
<Dialog.Root open={open} onOpenChange={onOpenChange}>
<Dialog.Portal>
<Dialog.Overlay className="radix-overlay" />
<Dialog.Content className="radix-dialog">
<Dialog.Title className="radix-dialog-title">{title}</Dialog.Title>
<Dialog.Description className="radix-dialog-description">
{description}
</Dialog.Description>
<div className="button-row">
<Dialog.Close asChild>
<button type="button" className="button secondary">Cancel</button>
</Dialog.Close>
<button
type="button"
className={`button ${danger ? "danger" : "primary"}`}
disabled={pending}
onClick={handleConfirm}
>
{pending ? "Working..." : confirmLabel}
</button>
</div>
<Dialog.Close asChild>
<button type="button" className="icon-button" aria-label="Close dialog">
x
</button>
</Dialog.Close>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}
export function UserMenu({
user,
onOpenProfile,
onOpenBilling,
onSignOut,
}: {
user: User;
onOpenProfile: () => void;
onOpenBilling: () => void;
onSignOut: () => void;
}) {
const [theme, setTheme] = React.useState<"light" | "dark" | "system">("system");
return (
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild>
<button type="button" className="user-trigger" aria-label={`${user.name} menu`}>
<span className="avatar" aria-hidden="true">{user.name.slice(0, 1)}</span>
<span>{user.name}</span>
</button>
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content className="dropdown-content" align="end" sideOffset={8}>
<DropdownMenu.Label className="dropdown-label">{user.email}</DropdownMenu.Label>
<DropdownMenu.Separator className="dropdown-separator" />
<DropdownMenu.Item className="dropdown-item" onSelect={() => onOpenProfile()}>
Profile
</DropdownMenu.Item>
<DropdownMenu.Item className="dropdown-item" onSelect={() => onOpenBilling()}>
Billing
</DropdownMenu.Item>
<DropdownMenu.Separator className="dropdown-separator" />
<DropdownMenu.RadioGroup
value={theme}
onValueChange={(value) => setTheme(value as "light" | "dark" | "system")}
>
<DropdownMenu.RadioItem className="dropdown-item" value="light">Light</DropdownMenu.RadioItem>
<DropdownMenu.RadioItem className="dropdown-item" value="dark">Dark</DropdownMenu.RadioItem>
<DropdownMenu.RadioItem className="dropdown-item" value="system">System</DropdownMenu.RadioItem>
</DropdownMenu.RadioGroup>
<DropdownMenu.Separator className="dropdown-separator" />
<DropdownMenu.Item className="dropdown-item danger-text" onSelect={() => onSignOut()}>
Sign out
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
);
}
export function SettingsTabs() {
return (
<Tabs.Root defaultValue="profile" className="tabs-root">
<Tabs.List className="tabs-list" aria-label="Account settings">
<Tabs.Trigger className="tabs-trigger" value="profile">Profile</Tabs.Trigger>
<Tabs.Trigger className="tabs-trigger" value="security">Security</Tabs.Trigger>
<Tabs.Trigger className="tabs-trigger" value="notifications">Notifications</Tabs.Trigger>
</Tabs.List>
<Tabs.Content className="tabs-content" value="profile">
<label className="field">
<span>Display name</span>
<input defaultValue="Masa" />
</label>
</Tabs.Content>
<Tabs.Content className="tabs-content" value="security">
<p>Require two-factor authentication before changing billing settings.</p>
<button type="button" className="button secondary">Review security</button>
</Tabs.Content>
<Tabs.Content className="tabs-content" value="notifications">
<label className="check-row">
<input type="checkbox" defaultChecked />
<span>Email me when a project is exported.</span>
</label>
</Tabs.Content>
</Tabs.Root>
);
}
Catatan styling
Radix UI tidak membawa CSS. Kamu bisa memakai Tailwind, CSS Modules, CSS biasa, atau design token. Yang penting: fokus tetap terlihat, Dialog tidak overflow di mobile, dan state aktif terlihat jelas.
.radix-overlay {
position: fixed;
inset: 0;
background: rgba(15, 23, 42, 0.55);
}
.radix-dialog {
position: fixed;
left: 50%;
top: 50%;
width: min(calc(100vw - 32px), 480px);
max-height: calc(100vh - 32px);
overflow: auto;
transform: translate(-50%, -50%);
border-radius: 8px;
background: white;
box-shadow: 0 24px 80px rgba(15, 23, 42, 0.28);
padding: 24px;
}
.button-row {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 24px;
}
.dropdown-content {
min-width: 220px;
border: 1px solid #e2e8f0;
border-radius: 8px;
background: white;
box-shadow: 0 18px 50px rgba(15, 23, 42, 0.18);
padding: 6px;
}
.dropdown-item {
border-radius: 6px;
cursor: pointer;
outline: none;
padding: 8px 10px;
}
.dropdown-item[data-highlighted] {
background: #eff6ff;
color: #1d4ed8;
}
.tabs-list {
display: flex;
border-bottom: 1px solid #e2e8f0;
gap: 4px;
}
.tabs-trigger {
border: 0;
border-bottom: 2px solid transparent;
background: transparent;
cursor: pointer;
padding: 10px 12px;
}
.tabs-trigger[data-state="active"] {
border-color: #2563eb;
color: #1d4ed8;
font-weight: 700;
}
:focus-visible {
outline: 3px solid #f59e0b;
outline-offset: 2px;
}
Tiga use case
| Use case | Kenapa Radix UI membantu | Yang disambungkan Claude Code |
|---|---|---|
| Konfirmasi hapus atau cancel | Fokus, judul, dan deskripsi lebih aman | API, pending state, error, test |
| Menu akun | Keyboard navigation, separator, radio item | User data, logout, billing, analytics |
| Tabs pengaturan | Relasi tablist, tab, dan panel stabil | Form, save behavior, URL, dirty state |
Use case pertama adalah admin SaaS. Menghapus project, membatalkan subscription, atau mengubah permission membutuhkan pesan yang jelas. Radix Dialog menyediakan perilaku dasar, Claude Code menghubungkan copy dan API.
Use case kedua adalah situs kursus, media, atau membership. Menu akun sering berisi profile, download, purchase history, progress, dan logout. Menu yang hanya bisa diklik akan menyulitkan pengguna keyboard.
Use case ketiga adalah halaman pengaturan. Tabs memisahkan profile, security, dan notifications, tetapi batas penyimpanan harus jelas. Tulis requirement ini di prompt agar Claude Code tidak mencampur semua state.
Pitfall yang sering terjadi
Jangan menghapus Dialog.Title hanya karena desain tidak menampilkan judul. Jika tidak ingin terlihat, gunakan judul yang visually hidden. Dokumentasi MDN tentang dialog juga menekankan label dan focus management.
Jangan menghilangkan focus outline. Mengganti dengan style brand boleh; membuatnya tidak terlihat tidak boleh.
Periksa asChild. Jika muncul button di dalam button, HTML menjadi invalid dan behavior bisa berbeda antar browser.
Jangan menumpuk banyak modal tanpa alasan kuat. WAI-ARIA Modal Dialog Pattern mengharapkan fokus masuk ke dialog dan kembali ke trigger setelah ditutup.
Cek mobile. Dialog fixed 600px, menu terpotong di kanan, atau label tabs terlalu panjang bisa merusak experience walau desktop terlihat rapi.
Link resmi dan checklist
- Radix Primitives Introduction
- Radix Dialog docs
- Radix Dropdown Menu docs
- Radix Tabs docs
- WAI-ARIA Modal Dialog Pattern
- MDN dialog role
- Claude Code getting started
Sebelum publish, buka dan tutup Dialog hanya dengan keyboard, cek Escape, cek fokus kembali ke trigger, navigasi Dropdown dengan arrow keys, ganti Tabs dengan arrow keys, dan pastikan mobile tidak overflow.
Monetization CTA
Topik ini cocok untuk CTA layanan karena pembaca biasanya punya project nyata. Offer yang natural bukan “pakai library ini”, tetapi “bawa repository kamu dan buat UI-nya bisa direview serta aksesibel”.
ClaudeCodeLab dapat membantu melalui training dan konsultasi Claude Code: CLAUDE.md, aturan komponen, accessibility checks, review prompts, dan verification scripts. Untuk solo builder, checklist dan template cukup untuk mulai. Untuk tim, review satu screen nyata biasanya memberi value paling besar.
Hasil yang diuji
Pada screen React percobaan Masa, modal manual diganti dengan Radix Dialog, lalu menu dan tabs dipindah ke Radix. Code sedikit bertambah, tetapi kriteria review menjadi jauh lebih jelas. Saat Claude Code diminta mengecek focus return, nama untuk screen reader, lebar mobile, dan keyboard behavior, feedback-nya lebih berguna daripada sekadar meminta “buat tampilannya lebih bagus”. Kesimpulannya: Radix UI bukan pengganti pemikiran aksesibilitas, tetapi fondasi behavior yang lebih aman untuk Claude Code.
PDF gratis: cheatsheet Claude Code
Masukkan email dan unduh satu halaman berisi command, kebiasaan review, dan workflow aman.
Kami menjaga datamu dan tidak mengirim spam.
Tentang penulis
Masa
Engineer yang berfokus pada workflow Claude Code praktis dan adopsi tim.
Artikel terkait
Workflow Obsidian ke CLAUDE.md untuk Claude Code
Ubah catatan kerja Obsidian menjadi operating note CLAUDE.md agar konteks tidak dijelaskan ulang.
Claude Code Revenue CTA Routing: dari artikel ke PDF, Gumroad, dan konsultasi
Workflow Claude Code untuk mengarahkan pembaca ke PDF gratis, Gumroad, atau konsultasi sesuai intent.
Aturan handoff tim Claude Code: bukti review, permission, rollback, dan jalur revenue
Format handoff Claude Code untuk tim: bukti, permission rule, rollback, PDF gratis, Gumroad, dan konsultasi.