Contents

Chapter 1

Accessibility Guide

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.

Core Principles

1. Semantic HTML First

Every component uses the correct HTML element for its purpose. This provides baseline accessibility that works even without JavaScript:

PurposeElementWhy
NavigationScreen readers announce "navigation" landmark
Main content
Allows "skip to content" functionality
Footer
Identified as "contentinfo" landmark
Clickable actionKeyboard-focusable, announces as "button"
Navigation linkAnnounces destination, right-click works
Form field labelClicking the label focuses the input
Modal
Announces as "dialog" to screen readers
Tab interfacerole="tablist" / role="tab"Proper tab semantics

Anti-pattern to avoid: Using

instead of
// 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"

Focus Trapping in Modals:

When a modal opens, Tab should only cycle through elements inside the modal, not escape to the page behind it:

tsx
// 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
};

Focus Restoration:

When a modal or slide-over closes, focus should return to the element that opened it:

tsx
const previousFocusRef = useRef<HTMLElement | null>(null);

useEffect(() => {
  if (isOpen) {
    previousFocusRef.current = document.activeElement as HTMLElement;
  } else {
    previousFocusRef.current?.focus();
  }
}, [isOpen]);

4. ARIA Attributes Reference

AttributeWhere UsedPurpose
aria-labelIcon-only buttonsProvides accessible name
aria-labelledbyModals, sectionsLinks element to its heading
aria-describedbyForm inputsLinks to error/helper text
aria-requiredRequired inputsAnnounces "required"
aria-invalidInputs with errorsAnnounces "invalid entry"
aria-expandedDropdowns, accordionsAnnounces open/closed state
aria-controlsTabs, accordionsLinks trigger to content
aria-selectedTabs, list itemsAnnounces selected state
aria-current="page"Navigation linksAnnounces current page
aria-sortTable headersAnnounces sort direction
aria-live="polite"Toast/alertsAnnounces dynamic content
aria-modal="true"ModalsMarks as modal dialog
role="alert"Error messagesImmediate announcement
role="status"Success messagesPolite announcement
role="switch"Toggle switchesAnnounces as switch

5. Color and Contrast

Minimum Contrast Ratios (WCAG AA):

Text TypeRequired RatioOur Default
Normal text (< 18px)4.5:15.1:1+
Large text (>= 18px)3:14.2:1+
UI components (borders, icons)3:13.5:1+

Never rely on color alone to convey information:

tsx
// 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

All animations respect the user's motion preference:

css
/* 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;
  }
}

Or per-component with Tailwind:

tsx
className="motion-safe:animate-fade-in motion-reduce:animate-none"

Testing Accessibility

Automated Testing

Run these on every build:

1. axe-core — Catches ~30% of accessibility issues

bash
   npm install -D @axe-core/react

2. eslint-plugin-jsx-a11y — Catches issues at lint time

bash
   npm install -D eslint-plugin-jsx-a11y

Manual Testing Checklist

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

Screen Reader Testing Tips

PlatformScreen ReaderToggle
macOSVoiceOverCmd + F5
WindowsNVDAFree download from nvaccess.org
WindowsNarratorWin + Ctrl + Enter
ChromeChromeVoxExtension

Part of Frontend Developer Pro

Chapter 2

Customization Guide

How to adapt this component library to match your brand, extend components, and integrate with your existing design system.

Theming with Design Tokens

Changing Brand Colors

The primary color is used for buttons, links, focus rings, and active states across all components. To change it:

1. Open tailwind.config.js

2. Replace the primary color scale with your brand colors

3. All components automatically pick up the new colors

javascript
// 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",
  },
}

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

Changing Semantic Colors

The danger, success, warning, and info scales follow the same pattern. You can customize each one independently:

javascript
danger: {
  // Red scale — for errors, destructive actions
  600: "#dc2626",  // Your error color
  // ... generate full scale
},
success: {
  // Green scale — for success states
  600: "#16a34a",
  // ...
},

Typography

Change the font stack to match your brand:

javascript
fontFamily: {
  sans: ["YourBrandFont", "Inter", "system-ui", "sans-serif"],
  mono: ["YourMonoFont", "JetBrains Mono", "monospace"],
},

Then load the font in your HTML or with next/font:

html
<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

Most components accept variant props for visual variations:

tsx
<Alert variant="info">Default info alert</Alert>
<Alert variant="danger">Error alert</Alert>

To add a new variant, find the variant styles object in the component and add your new option:

tsx
// 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

Every component accepts a className prop that gets merged with the default classes:

tsx
// Add custom width
<TextInput className="max-w-xs" />

// Override default padding
<Card className="p-8">
  Larger padding card
</Card>

Important: Tailwind doesn't automatically handle class conflicts. If you need to override a default class, use the ! prefix:

tsx
// Override the default border color
<Card className="!border-purple-500">

Pattern 3: Composition

Components are designed to be composed together:

tsx
<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

Create project-specific wrappers around library components to enforce consistency:

tsx
// 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

1. Set darkMode: "class" in tailwind.config.js (already configured)

2. Add the dark class to your element

3. All components automatically switch to dark styles

Dark Mode Toggle

tsx
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

To override a specific dark mode color, use Tailwind's dark: prefix:

tsx
<Card className="dark:bg-gray-800 dark:border-gray-600">
  Custom dark background
</Card>

Responsive Design

All components are responsive by default. Key breakpoints:

BreakpointWidthCommon 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

tsx
// 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

bash
# 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 use

With Next.js

Same steps, but Next.js has built-in Tailwind support:

bash
# 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

bash
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

# Copy and use

With Vue or Svelte

The Tailwind classes work in any framework. You'll need to convert the JSX syntax:

React (original):

tsx
<button className="bg-primary-600 text-white px-4 py-2 rounded-lg">
  Click me
</button>

Vue:

vue
<button class="bg-primary-600 text-white px-4 py-2 rounded-lg">
  Click me
</button>

Svelte:

svelte
<button class="bg-primary-600 text-white px-4 py-2 rounded-lg">
  Click me
</button>

The class names are identical. Only the component structure and state management change.

Part of Frontend Developer Pro

Tailwind Component Library v1.0.0 — Free Preview