tailwindcss-accessibility

Tailwind CSS Accessibility Patterns (WCAG 2.2 - 2025/2026)

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "tailwindcss-accessibility" with this command: npx skills add josiahsiegel/claude-plugin-marketplace/josiahsiegel-claude-plugin-marketplace-tailwindcss-accessibility

Tailwind CSS Accessibility Patterns (WCAG 2.2 - 2025/2026)

WCAG 2.2 Overview (Current Standard)

WCAG 2.2 was released October 2023 and is the current W3C standard. Key additions relevant to Tailwind:

  • 2.5.8 Target Size (Level AA): 24x24 CSS pixels minimum, 44x44 recommended

  • 2.4.11 Focus Not Obscured: Focus indicators must be visible

  • 2.4.13 Focus Appearance: Enhanced focus indicator requirements

  • 3.3.7 Redundant Entry: Don't require re-entering information

  • 3.2.6 Consistent Help: Help mechanisms in consistent locations

Focus Management

Focus Ring Utilities

<!-- Default focus ring --> <button class="focus:outline-none focus:ring-2 focus:ring-brand-500 focus:ring-offset-2"> Button </button>

<!-- Focus-visible for keyboard users only --> <button class="focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-500"> Only shows ring for keyboard focus </button>

<!-- Focus-within for parent containers --> <div class="focus-within:ring-2 focus-within:ring-brand-500 rounded-lg p-1"> <input type="text" class="border-none focus:outline-none" /> </div>

<!-- Custom focus ring component -->

@layer components { .focus-ring { @apply focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 focus-visible:ring-offset-2; }

.focus-ring-inset { @apply focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 focus-visible:ring-inset; } }

Skip Links

<!-- Skip to main content --> <a href="#main-content" class=" sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50 focus:bg-white focus:px-4 focus:py-2 focus:rounded-md focus:shadow-lg focus:ring-2 focus:ring-brand-500 "

Skip to main content </a>

<header>Navigation...</header> <main id="main-content" tabindex="-1"> Main content </main>

Focus Trap Pattern

<!-- Modal with focus management --> <div role="dialog" aria-modal="true" aria-labelledby="modal-title" class="fixed inset-0 z-50 flex items-center justify-center"

<div class="fixed inset-0 bg-black/50" aria-hidden="true"></div> <div class="relative bg-white rounded-xl p-6 max-w-md w-full" role="document"

&#x3C;h2 id="modal-title" class="text-lg font-semibold">Modal Title&#x3C;/h2>
&#x3C;p>Modal content&#x3C;/p>
&#x3C;button class="focus-ring">Close&#x3C;/button>

</div> </div>

Screen Reader Utilities

Visually Hidden Content

<!-- Hidden visually but available to screen readers --> <span class="sr-only">Additional context for screen readers</span>

<!-- Show on focus (skip links) --> <a href="#main" class="sr-only focus:not-sr-only">Skip to main</a>

<!-- Icon with accessible label --> <button> <svg aria-hidden="true">...</svg> <span class="sr-only">Close menu</span> </button>

<!-- Form labels --> <label> <span class="sr-only">Search</span> <input type="search" placeholder="Search..." /> </label>

Announcing Dynamic Content

<!-- Live region for announcements --> <div role="status" aria-live="polite" aria-atomic="true" class="sr-only"

<!-- Dynamic content announced to screen readers --> 3 items added to cart </div>

<!-- Alert for important messages --> <div role="alert" aria-live="assertive" class="bg-red-100 text-red-800 p-4 rounded-lg"

Error: Please correct the form </div>

Color Contrast

High Contrast Patterns

<!-- Ensure sufficient contrast --> <p class="text-gray-700 bg-white">4.5:1 contrast ratio</p> <p class="text-gray-500 bg-white">May not meet WCAG AA (3:1 min for large text)</p>

<!-- Large text (18pt+) needs 3:1 --> <h1 class="text-4xl text-gray-600 bg-white">Large text - 3:1 ratio OK</h1>

<!-- Interactive elements need 3:1 against adjacent colors --> <button class=" bg-brand-500 text-white border-2 border-brand-500 focus:ring-2 focus:ring-brand-500 focus:ring-offset-2 "> Accessible Button </button>

Dark Mode Contrast

<!-- Maintain contrast in both modes --> <p class="text-gray-900 dark:text-gray-100"> High contrast text </p>

<p class="text-gray-600 dark:text-gray-400"> Secondary text with adequate contrast </p>

<!-- Avoid low contrast combinations --> <p class="text-gray-400 dark:text-gray-600"> ⚠️ May have contrast issues in dark mode </p>

Focus Indicator Contrast

@theme { /* High contrast focus ring */ --color-focus: oklch(0.55 0.25 250); --color-focus-offset: oklch(1 0 0); }

<button class=" focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--color-focus-offset)] "> High contrast focus </button>

Motion and Animation

Reduced Motion

<!-- Respect user's motion preferences --> <div class=" animate-bounce motion-reduce:animate-none "> Bouncing element (static for motion-sensitive users) </div>

<!-- Safer alternative animations --> <div class=" transition-opacity duration-300 motion-reduce:transition-none "> Fades in (instant for motion-sensitive) </div>

<!-- Use opacity instead of movement --> <div class=" transition-all hover:scale-105 hover:shadow-lg motion-reduce:hover:scale-100 motion-reduce:hover:shadow-md "> Scales on hover (shadow only for motion-sensitive) </div>

Safe Animation Patterns

@layer components { /* Animations that respect reduced motion */ .animate-fade-in { @apply animate-in fade-in duration-300; @apply motion-reduce:animate-none motion-reduce:opacity-100; }

.animate-slide-up { @apply animate-in slide-in-from-bottom-4 duration-300; @apply motion-reduce:animate-none motion-reduce:translate-y-0; } }

Pause Animation on Hover

<!-- Allow users to pause animations --> <div class=" animate-spin hover:animate-pause motion-reduce:animate-none "> Loading spinner </div>

Form Accessibility

Accessible Form Fields

<div class="space-y-4"> <!-- Text input with label --> <div> <label for="email" class="block text-sm font-medium text-gray-700"> Email address <span class="text-red-500" aria-hidden="true">*</span> </label> <input type="email" id="email" name="email" required aria-required="true" aria-describedby="email-hint email-error" class=" mt-1 block w-full rounded-md border-gray-300 focus:border-brand-500 focus:ring-brand-500 aria-invalid:border-red-500 aria-invalid:ring-red-500 " /> <p id="email-hint" class="mt-1 text-sm text-gray-500"> We'll never share your email </p> <p id="email-error" class="mt-1 text-sm text-red-600 hidden" role="alert"> Please enter a valid email </p> </div>

<!-- Checkbox with accessible label --> <div class="flex items-start gap-3"> <input type="checkbox" id="terms" name="terms" class=" h-4 w-4 rounded border-gray-300 text-brand-500 focus:ring-brand-500 " /> <label for="terms" class="text-sm text-gray-700"> I agree to the <a href="/terms" class="text-brand-500 underline">terms and conditions</a> </label> </div>

<!-- Radio group --> <fieldset> <legend class="text-sm font-medium text-gray-700">Notification preference</legend> <div class="mt-2 space-y-2"> <div class="flex items-center gap-3"> <input type="radio" id="email-pref" name="notification" value="email" class="h-4 w-4" /> <label for="email-pref">Email</label> </div> <div class="flex items-center gap-3"> <input type="radio" id="sms-pref" name="notification" value="sms" class="h-4 w-4" /> <label for="sms-pref">SMS</label> </div> </div> </fieldset> </div>

Error States

<!-- Input with error --> <div> <label for="password" class="block text-sm font-medium text-gray-700"> Password </label> <input type="password" id="password" aria-invalid="true" aria-describedby="password-error" class=" mt-1 block w-full rounded-md border-red-500 text-red-900 focus:border-red-500 focus:ring-red-500 " /> <p id="password-error" class="mt-1 text-sm text-red-600" role="alert"> <span class="sr-only">Error:</span> Password must be at least 8 characters </p> </div>

Form Validation Feedback

/* Style based on aria-invalid attribute */ @custom-variant aria-invalid (&[aria-invalid="true"]);

<input class=" border-gray-300 aria-invalid:border-red-500 aria-invalid:text-red-900 aria-invalid:focus:ring-red-500 " aria-invalid="true" />

Interactive Components

Accessible Buttons

<!-- Button with loading state --> <button type="submit" aria-busy="true" aria-disabled="true" class=" relative aria-busy:cursor-wait aria-disabled:opacity-50 aria-disabled:cursor-not-allowed "

<span class="aria-busy:invisible">Submit</span> <span class="absolute inset-0 flex items-center justify-center aria-busy:visible invisible"> <svg class="animate-spin h-5 w-5" aria-hidden="true">...</svg> <span class="sr-only">Loading...</span> </span> </button>

<!-- Icon button --> <button type="button" aria-label="Close dialog" class="rounded-full p-2 hover:bg-gray-100 focus-ring"

<svg aria-hidden="true" class="h-5 w-5">...</svg> </button>

<!-- Toggle button --> <button type="button" aria-pressed="false" class=" px-4 py-2 rounded-lg border aria-pressed:bg-brand-500 aria-pressed:text-white aria-pressed:border-brand-500 "

<span class="sr-only">Toggle feature</span> Feature </button>

Accessible Dropdowns

<div class="relative"> <button type="button" aria-haspopup="menu" aria-expanded="false" aria-controls="dropdown-menu" class="flex items-center gap-2 px-4 py-2 rounded-lg border focus-ring"

Options
&#x3C;svg aria-hidden="true" class="h-4 w-4">...&#x3C;/svg>

</button>

<ul id="dropdown-menu" role="menu" aria-labelledby="dropdown-button" class=" absolute top-full mt-1 w-48 rounded-lg bg-white shadow-lg border hidden aria-expanded:block "

&#x3C;li role="none">
  &#x3C;a
    href="#"
    role="menuitem"
    tabindex="-1"
    class="block px-4 py-2 hover:bg-gray-100 focus:bg-gray-100 focus:outline-none"
  >
    Edit
  &#x3C;/a>
&#x3C;/li>
&#x3C;li role="none">
  &#x3C;a
    href="#"
    role="menuitem"
    tabindex="-1"
    class="block px-4 py-2 hover:bg-gray-100 focus:bg-gray-100 focus:outline-none"
  >
    Delete
  &#x3C;/a>
&#x3C;/li>

</ul> </div>

Accessible Tabs

<div> <div role="tablist" aria-label="Account settings" class="flex border-b"> <button role="tab" aria-selected="true" aria-controls="panel-1" id="tab-1" class=" px-4 py-2 border-b-2 aria-selected:border-brand-500 aria-selected:text-brand-500 hover:text-gray-700 focus-visible:ring-2 focus-visible:ring-inset " > Profile </button> <button role="tab" aria-selected="false" aria-controls="panel-2" id="tab-2" tabindex="-1" class="px-4 py-2 border-b-2 border-transparent" > Settings </button> </div>

<div role="tabpanel" id="panel-1" aria-labelledby="tab-1" tabindex="0" class="p-4 focus:outline-none focus-visible:ring-2 focus-visible:ring-inset"

Profile content

</div>

<div role="tabpanel" id="panel-2" aria-labelledby="tab-2" tabindex="0" hidden class="p-4"

Settings content

</div> </div>

Touch Targets (WCAG 2.2 - Critical for 2025/2026)

WCAG 2.2 Target Size Requirements

Level Requirement Tailwind Class

AA (2.5.8) 24x24 CSS pixels minimum min-h-6 min-w-6

Recommended 44x44 CSS pixels min-h-11 min-w-11

AAA (2.5.5) 44x44 CSS pixels min-h-11 min-w-11

Optimal 48x48 CSS pixels min-h-12 min-w-12

Platform guidelines comparison:

  • Apple iOS: 44x44 points minimum

  • Google Android: 48x48 dp minimum

  • Microsoft Fluent: 44x44 pixels minimum

Minimum Touch Target Size

<!-- WCAG 2.2 Level AA minimum (24px) --> <button class="min-h-6 min-w-6 p-1"> <svg class="h-4 w-4">...</svg> </button>

<!-- Recommended size (44px) - preferred for mobile --> <button class="min-h-11 min-w-11 p-2.5"> <svg class="h-6 w-6" aria-hidden="true">...</svg> <span class="sr-only">Action</span> </button>

<!-- Optimal for primary actions (48px) --> <button class="min-h-12 min-w-12 px-6 py-3 text-base font-medium"> Primary Action </button>

Extend Touch Target Beyond Visible Element

<!-- Small visible link with extended tap area --> <a href="#" class="relative inline-block text-sm"> Small Link Text <span class="absolute -inset-3" aria-hidden="true"></span> </a>

<!-- Icon button with extended target --> <button class="relative p-2 -m-2 rounded-lg hover:bg-gray-100"> <svg class="h-5 w-5" aria-hidden="true">...</svg> <span class="sr-only">Close menu</span> </button>

<!-- Card with full-surface tap target --> <article class="relative p-4 rounded-lg border hover:shadow-md"> <h3>Card Title</h3> <p>Description text</p> <a href="/details" class="after:absolute after:inset-0"> <span class="sr-only">View details</span> </a> </article>

Spacing Between Interactive Elements

WCAG 2.2 requires 24px spacing OR targets must be 24px minimum:

<!-- Adequate spacing between touch targets (12px gap minimum) --> <div class="flex gap-3"> <button class="min-h-11 px-4 py-2">Button 1</button> <button class="min-h-11 px-4 py-2">Button 2</button> </div>

<!-- Stacked links with adequate height and spacing --> <nav class="flex flex-col"> <a href="#" class="py-3 px-4 min-h-11 border-b border-gray-100">Link 1</a> <a href="#" class="py-3 px-4 min-h-11 border-b border-gray-100">Link 2</a> <a href="#" class="py-3 px-4 min-h-11">Link 3</a> </nav>

<!-- Button group with safe spacing --> <div class="flex flex-wrap gap-3"> <button class="min-h-11 px-4 py-2 border rounded-lg">Cancel</button> <button class="min-h-11 px-4 py-2 bg-blue-600 text-white rounded-lg">Confirm</button> </div>

Touch Target Exceptions (WCAG 2.2)

Targets can be smaller than 24x24 if:

  • Inline text links within sentences

  • Browser-provided controls (scrollbars)

  • Size is essential to information

  • A larger equivalent target exists on same page

Text Accessibility

Readable Text

<!-- Adequate line height for body text --> <p class="leading-relaxed"> Long form content with comfortable line height </p>

<!-- Limit line length for readability --> <article class="max-w-prose"> <p class="leading-relaxed"> Content with optimal line length (45-75 characters) </p> </article>

<!-- Adequate paragraph spacing --> <div class="space-y-6"> <p>Paragraph 1</p> <p>Paragraph 2</p> </div>

Text Resizing

<!-- Use relative units for text --> <p class="text-base">Scales with user's font size preferences</p>

<!-- Don't use fixed pixel values for text --> <p class="text-[14px]">⚠️ Won't scale with browser zoom</p>

<!-- Container that doesn't break on text zoom --> <div class="min-h-[auto]"> Content height adjusts with text size </div>

Semantic HTML with Tailwind

Landmark Regions

<body class="min-h-screen flex flex-col"> <header class="sticky top-0 bg-white shadow z-50"> <nav aria-label="Main navigation">...</nav> </header>

<main id="main-content" class="flex-1"> <article> <h1>Page Title</h1> <section aria-labelledby="section-1"> <h2 id="section-1">Section Title</h2> <p>Content...</p> </section> </article>

&#x3C;aside aria-label="Related content" class="hidden lg:block">
  Sidebar content
&#x3C;/aside>

</main>

<footer class="bg-gray-800 text-white"> <nav aria-label="Footer navigation">...</nav> </footer> </body>

Heading Hierarchy

<article class="prose"> <h1 class="text-4xl font-bold">Main Title (H1)</h1>

<section> <h2 class="text-2xl font-semibold">Section (H2)</h2>

&#x3C;section>
  &#x3C;h3 class="text-xl font-medium">Subsection (H3)&#x3C;/h3>
  &#x3C;p>Content...&#x3C;/p>
&#x3C;/section>

</section> </article>

Testing Accessibility

Browser DevTools Checklist

  • Color contrast: Use contrast checker

  • Focus order: Tab through the page

  • Zoom: Test at 200% zoom

  • Reduced motion: Enable in OS settings

Automated Testing

// axe-core integration import { axe, toHaveNoViolations } from 'jest-axe';

expect.extend(toHaveNoViolations);

test('component is accessible', async () => { const { container } = render(<Component />); const results = await axe(container); expect(results).toHaveNoViolations(); });

Best Practices Summary (WCAG 2.2 - 2025/2026)

Pattern Implementation WCAG Level

Focus visible focus-visible:ring-2 focus-visible:ring-offset-2

2.4.7 (AA)

Screen reader only sr-only

1.3.1 (A)

Skip links sr-only focus:not-sr-only focus:absolute

2.4.1 (A)

Reduced motion motion-reduce:animate-none motion-reduce:transition-none

2.3.3 (AAA)

Touch targets (min) min-h-6 min-w-6 (24px) 2.5.8 (AA)

Touch targets (rec) min-h-11 min-w-11 (44px) 2.5.5 (AAA)

Touch spacing gap-3 (12px minimum between targets) 2.5.8 (AA)

Text contrast 4.5:1 for normal, 3:1 for large text 1.4.3 (AA)

Form errors aria-invalid="true"

  • role="alert"

3.3.1 (A)

Focus not obscured Avoid z-index covering focused elements 2.4.11 (AA)

Quick Reference: Touch-Friendly Component

<!-- Accessible, touch-friendly button component --> <button type="button" class=" /* Touch target size (44px minimum) */ min-h-11 min-w-11 px-4 py-2.5

/* Typography */
text-sm md:text-base font-medium

/* Colors with sufficient contrast */
bg-blue-600 text-white
hover:bg-blue-700

/* Focus indicator (visible, not obscured) */
focus:outline-none
focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2

/* Shape */
rounded-lg

/* Disabled state */
disabled:opacity-50 disabled:cursor-not-allowed

/* Respect motion preferences */
transition-colors motion-reduce:transition-none

"

Button Text </button>

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

tailwindcss-advanced-layouts

No summary provided by upstream source.

Repository SourceNeeds Review
General

tailwindcss-animations

No summary provided by upstream source.

Repository SourceNeeds Review
General

tailwindcss-mobile-first

No summary provided by upstream source.

Repository SourceNeeds Review
General

docker-best-practices

No summary provided by upstream source.

Repository SourceNeeds Review