This guide explains the accessibility patterns used across every component in the library. Follow these patterns when customizing components to maintain WCAG 2.1 AA compliance.
Every component uses the correct HTML element for its purpose. This provides baseline accessibility that works even without JavaScript:
Anti-pattern to avoid: Using Every interactive component works with the keyboard: Visible Focus Indicators: All focusable elements have a visible focus ring using Tailwind's Focus Trapping in Modals: When a modal opens, Tab should only cycle through elements inside the modal, not escape to the page behind it: Focus Restoration: When a modal or slide-over closes, focus should return to the element that opened it: Minimum Contrast Ratios (WCAG AA): Never rely on color alone to convey information: All animations respect the user's motion preference: Or per-component with Tailwind: Run these on every build: 1. axe-core — Catches ~30% of accessibility issues 2. eslint-plugin-jsx-a11y — Catches issues at lint time 1. Tab through the page — Can you reach every interactive element? Is the tab order logical? 2. Use a screen reader — VoiceOver (Mac), NVDA (Windows), or JAWS. Does it announce elements correctly? 3. Zoom to 200% — Does the layout still work? Is text readable? 4. Turn off CSS — Does the content still make sense? 5. Use keyboard only — Can you complete every task without a mouse? 6. Check color contrast — Use the browser's DevTools accessibility inspector Part of Frontend Developer Pro How to adapt this component library to match your brand, extend components, and integrate with your existing design system. The primary color is used for buttons, links, focus rings, and active states across all components. To change it: 1. Open 2. Replace the 3. All components automatically pick up the new colors How to generate a color scale from a single hex: 1. Go to Tailwind CSS Color Generator (or similar) 2. Paste your brand color 3. Copy the generated scale into your config The Change the font stack to match your brand: Then load the font in your HTML or with Most components accept variant props for visual variations: To add a new variant, find the variant styles object in the component and add your new option: Every component accepts a Important: Tailwind doesn't automatically handle class conflicts. If you need to override a default class, use the Components are designed to be composed together: Create project-specific wrappers around library components to enforce consistency: 1. Set 2. Add the 3. All components automatically switch to dark styles To override a specific dark mode color, use Tailwind's All components are responsive by default. Key breakpoints: Same steps, but Next.js has built-in Tailwind support: The Tailwind classes work in any framework. You'll need to convert the JSX syntax: React (original): Vue: Svelte: The class names are identical. Only the component structure and state management change. Part of Frontend Developer Pro. A div has no keyboard support, no screen reader announcement, and no built-in focus styling.
2. Keyboard Navigation
Component Tab Enter/Space Escape Arrow Keys Buttons Focus Activate - - Text inputs Focus Submit form - Move cursor Checkboxes Focus Toggle - - Radio groups Focus group Select - Next/previous option Toggle switch Focus Toggle - - Tabs Focus tab list Select tab - Next/previous tab Modals Focus first element Activate focused element Close modal - Dropdowns Focus trigger Open/close Close Navigate items Command palette Focus search Select item Close Navigate items 3. Focus Management
focus-visible:ring-2:// We use focus-visible instead of focus so the ring only appears
// during keyboard navigation, not mouse clicks
className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500"// The Modal component traps focus by intercepting Tab and Shift+Tab
// and wrapping around to the first/last focusable element
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key !== "Tab") return;
const focusable = modalRef.current.querySelectorAll<HTMLElement>(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
// ... wrap focus between first and last
};const previousFocusRef = useRef<HTMLElement | null>(null);
useEffect(() => {
if (isOpen) {
previousFocusRef.current = document.activeElement as HTMLElement;
} else {
previousFocusRef.current?.focus();
}
}, [isOpen]);4. ARIA Attributes Reference
Attribute Where Used Purpose aria-labelIcon-only buttons Provides accessible name aria-labelledbyModals, sections Links element to its heading aria-describedbyForm inputs Links to error/helper text aria-requiredRequired inputs Announces "required" aria-invalidInputs with errors Announces "invalid entry" aria-expandedDropdowns, accordions Announces open/closed state aria-controlsTabs, accordions Links trigger to content aria-selectedTabs, list items Announces selected state aria-current="page"Navigation links Announces current page aria-sortTable headers Announces sort direction aria-live="polite"Toast/alerts Announces dynamic content aria-modal="true"Modals Marks as modal dialog role="alert"Error messages Immediate announcement role="status"Success messages Polite announcement role="switch"Toggle switches Announces as switch 5. Color and Contrast
Text Type Required Ratio Our Default Normal text (< 18px) 4.5:1 5.1:1+ Large text (>= 18px) 3:1 4.2:1+ UI components (borders, icons) 3:1 3.5:1+ // Bad: Color is the only indicator
<span className="text-red-500">Error</span>
// Good: Icon + color + text
<span className="text-danger-600">
<ErrorIcon /> Error: Field is required
</span>6. Motion and Animation
/* In your global CSS */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}className="motion-safe:animate-fade-in motion-reduce:animate-none"Testing Accessibility
Automated Testing
npm install -D @axe-core/react npm install -D eslint-plugin-jsx-a11yManual Testing Checklist
Screen Reader Testing Tips
Platform Screen Reader Toggle macOS VoiceOver Cmd + F5 Windows NVDA Free download from nvaccess.org Windows Narrator Win + Ctrl + Enter Chrome ChromeVox Extension Customization Guide
Theming with Design Tokens
Changing Brand Colors
tailwind.config.jsprimary color scale with your brand colors// tailwind.config.js
colors: {
primary: {
50: "#f0f9ff", // Lightest — backgrounds
100: "#e0f2fe",
200: "#bae6fd",
300: "#7dd3fc",
400: "#38bdf8",
500: "#0ea5e9", // Medium — less important text
600: "#0284c7", // Default — buttons, links, focus rings
700: "#0369a1", // Hover — button hover state
800: "#075985",
900: "#0c4a6e", // Darkest — headings on light backgrounds
950: "#082f49",
},
}Changing Semantic Colors
danger, success, warning, and info scales follow the same pattern. You can customize each one independently:danger: {
// Red scale — for errors, destructive actions
600: "#dc2626", // Your error color
// ... generate full scale
},
success: {
// Green scale — for success states
600: "#16a34a",
// ...
},Typography
fontFamily: {
sans: ["YourBrandFont", "Inter", "system-ui", "sans-serif"],
mono: ["YourMonoFont", "JetBrains Mono", "monospace"],
},next/font:<link href="https://fonts.googleapis.com/css2?family=YourBrandFont:wght@400;500;600;700&display=swap" rel="stylesheet">Component Customization Patterns
Pattern 1: Props-Based Variants
<Alert variant="info">Default info alert</Alert>
<Alert variant="danger">Error alert</Alert>// Before: 4 variants
const styles = {
info: "bg-info-50 border-info-200 ...",
success: "...",
warning: "...",
danger: "...",
};
// After: 5 variants (added "purple" brand variant)
const styles = {
info: "bg-info-50 border-info-200 ...",
success: "...",
warning: "...",
danger: "...",
brand: "bg-purple-50 border-purple-200 text-purple-800",
};Pattern 2: className Override
className prop that gets merged with the default classes:// Add custom width
<TextInput className="max-w-xs" />
// Override default padding
<Card className="p-8">
Larger padding card
</Card>! prefix:// Override the default border color
<Card className="!border-purple-500">Pattern 3: Composition
<Card>
<CardHeader
title="Monthly Revenue"
actions={<Badge variant="success">+12%</Badge>}
/>
<CardBody>
<BarChart items={revenueData} />
</CardBody>
<CardFooter>
<a href="/analytics" className="text-sm text-primary-600">
View full report
</a>
</CardFooter>
</Card>Pattern 4: Wrapper Components
// components/ui/Button.tsx — Your project's button
import { type ReactNode } from "react";
interface ButtonProps {
children: ReactNode;
variant?: "primary" | "secondary" | "danger";
size?: "sm" | "md" | "lg";
loading?: boolean;
disabled?: boolean;
onClick?: () => void;
type?: "button" | "submit";
}
const VARIANT_CLASSES = {
primary: "bg-primary-600 text-white hover:bg-primary-700 focus:ring-primary-500",
secondary: "bg-white text-gray-700 border border-gray-300 hover:bg-gray-50 focus:ring-primary-500",
danger: "bg-danger-600 text-white hover:bg-danger-700 focus:ring-danger-500",
};
const SIZE_CLASSES = {
sm: "px-3 py-1.5 text-sm",
md: "px-4 py-2 text-sm",
lg: "px-6 py-3 text-base",
};
export function Button({
children,
variant = "primary",
size = "md",
loading = false,
disabled = false,
onClick,
type = "button",
}: ButtonProps) {
return (
<button
type={type}
onClick={onClick}
disabled={disabled || loading}
className={`
inline-flex items-center justify-center gap-2 rounded-lg font-medium
transition-colors duration-150
focus:outline-none focus:ring-2 focus:ring-offset-2
disabled:cursor-not-allowed disabled:opacity-50
${VARIANT_CLASSES[variant]}
${SIZE_CLASSES[size]}
`}
>
{loading && (
<svg className="h-4 w-4 animate-spin" viewBox="0 0 24 24" fill="none">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
)}
{children}
</button>
);
}Dark Mode
Enabling Dark Mode
darkMode: "class" in tailwind.config.js (already configured)dark class to your elementDark Mode Toggle
function DarkModeToggle() {
const [isDark, setIsDark] = useState(false);
useEffect(() => {
// Check for saved preference or system preference
const saved = localStorage.getItem("theme");
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
if (saved === "dark" || (!saved && prefersDark)) {
document.documentElement.classList.add("dark");
setIsDark(true);
}
}, []);
const toggle = () => {
const newValue = !isDark;
setIsDark(newValue);
document.documentElement.classList.toggle("dark", newValue);
localStorage.setItem("theme", newValue ? "dark" : "light");
};
return (
<button onClick={toggle} className="rounded-lg p-2 hover:bg-gray-100 dark:hover:bg-gray-800">
{isDark ? "Light Mode" : "Dark Mode"}
</button>
);
}Custom Dark Colors
dark: prefix:<Card className="dark:bg-gray-800 dark:border-gray-600">
Custom dark background
</Card>Responsive Design
Breakpoint Width Common Use sm:640px+ 2-column layouts md:768px+ Desktop navigation visible lg:1024px+ 3-column layouts, sidebars xl:1280px+ Wide content areas 2xl:1536px+ Ultra-wide monitors Mobile-First Examples
// Stack on mobile, side-by-side on desktop
<div className="flex flex-col md:flex-row gap-4">
<Sidebar className="md:w-64" />
<main className="flex-1">...</main>
</div>
// 1 column on mobile, 2 on tablet, 3 on desktop
<Grid cols={{ default: 1, sm: 2, lg: 3 }}>
<Card>...</Card>
<Card>...</Card>
<Card>...</Card>
</Grid>
// Hide on mobile, show on desktop
<Sidebar className="hidden lg:block" />Integration with Existing Projects
With Create React App
# 1. Install Tailwind
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
# 2. Copy tailwind.config.js (merge with yours if you have one)
cp tailwind-component-library/tailwind.config.js ./tailwind.config.js
# 3. Copy components you need
cp -r tailwind-component-library/components/forms src/components/ui/
# 4. Import and useWith Next.js
# If Tailwind isn't set up yet
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
# Copy components
cp -r tailwind-component-library/components/* src/components/ui/With Vite
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
# Copy and useWith Vue or Svelte
<button className="bg-primary-600 text-white px-4 py-2 rounded-lg">
Click me
</button><button class="bg-primary-600 text-white px-4 py-2 rounded-lg">
Click me
</button><button class="bg-primary-600 text-white px-4 py-2 rounded-lg">
Click me
</button>