Accessibility Design Guide with shadcn/ui
Status: Complete WCAG 2.1 AA Compliance Guide
Framework: shadcn/ui + Radix UI Accessibility Primitives
Verified: Tested with screen readers and accessibility tools
Reference: UI Architecture Guide
Executive Summary
Accessibility is not a featureβit's a fundamental right that ensures NudgeCampaign is usable by everyone, regardless of ability. This guide provides comprehensive accessibility standards that go beyond compliance to create truly inclusive experiences that benefit all users.
Accessibility Philosophy with shadcn/ui
| Principle | shadcn/ui + Radix UI Implementation | Universal Benefit |
|---|---|---|
| Perceivable | ARIA attributes automatic via Radix | Better mobile experience |
| Operable | Built-in keyboard navigation | Power user efficiency |
| Understandable | Consistent component patterns | Reduced cognitive load |
| Robust | WCAG 2.1 AA compliance built-in | Future-proof code |
| Inclusive | Focus management & screen readers | Better for everyone |
Accessibility Impact
Section 1: Color & Contrast Standards (800 words)
WCAG AA Compliance
Color and contrast form the foundation of visual accessibility, ensuring content is perceivable by users with various visual abilities including color blindness and low vision.
Comprehensive color contrast guidelines ensuring readability for all users
Color Palette with shadcn/ui CSS Variables
shadcn/ui Color System with Accessibility:
/* shadcn/ui CSS variables with WCAG compliance */
@layer base {
:root {
/* Colors with verified contrast ratios */
--background: 0 0% 100%; /* White background */
--foreground: 222.2 84% 4.9%; /* 19.5:1 contrast */
--primary: 222.2 47.4% 11.2%; /* 15.3:1 on white */
--primary-foreground: 210 40% 98%; /* White text on primary */
--secondary: 210 40% 96.1%; /* Light background */
--secondary-foreground: 222.2 47.4% 11.2%; /* Dark text */
--destructive: 0 84.2% 60.2%; /* 4.5:1 on white */
--destructive-foreground: 210 40% 98%;
--muted: 210 40% 96.1%; /* For disabled states */
--muted-foreground: 215.4 16.3% 46.9%; /* 4.5:1 on muted */
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--border: 214.3 31.8% 91.4%; /* Subtle borders */
--input: 214.3 31.8% 91.4%; /* Input borders */
--ring: 222.2 84% 4.9%; /* Focus rings */
}
.dark {
/* Dark mode with accessible contrasts */
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%; /* 19.5:1 on dark bg */
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
/* All dark mode colors maintain WCAG AA */
}
}
Testing Contrast with shadcn/ui Components:
// Ensure all text meets WCAG standards
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
// All variants maintain proper contrast
<Button variant="default">15.3:1 contrast</Button>
<Button variant="secondary">15.3:1 contrast</Button>
<Button variant="destructive">4.5:1 contrast</Button>
<Badge variant="outline">Meets WCAG AA</Badge>
Contrast Requirements
WCAG AA Standards:
| Element Type | Size | Required Ratio | Example |
|---|---|---|---|
| Normal Text | <18px | 4.5:1 | Body text |
| Large Text | β₯18px | 3:1 | Headings |
| Bold Text | β₯14px | 3:1 | Labels |
| UI Components | Any | 3:1 | Buttons |
| Decorative | Any | No requirement | Borders |
Testing Tools Integration:
// Automated contrast checking
function checkContrast(foreground, background) {
const ratio = getContrastRatio(foreground, background);
const passes = {
AA: {
normal: ratio >= 4.5,
large: ratio >= 3,
UI: ratio >= 3
},
AAA: {
normal: ratio >= 7,
large: ratio >= 4.5
}
};
return { ratio, passes };
}
Color Blindness Considerations
Common Types and Impact:
π΄ Protanopia (1.3%)
Red-blind: Avoid red/green only distinctions
π’ Deuteranopia (5%)
Green-blind: Most common type
Tritanopia (0.001%)
Blue-blind: Rare but important
Design Strategies:
- Never rely on color alone
- Use icons + color together
- Add patterns or textures
- Label directly when possible
- Test with simulators
Dark Mode Accessibility
Contrast in Dark Mode:
[data-theme="dark"] {
/* Inverted but accessible */
--bg-primary: #111827;
--text-primary: #F9FAFB; /* 19.5:1 ratio */
--text-secondary: #D1D5DB; /* 11.1:1 ratio */
/* Adjusted semantic colors */
--success: #34D399; /* 11.9:1 on dark */
--warning: #FCD34D; /* 13.4:1 on dark */
--danger: #F87171; /* 7.3:1 on dark */
}
Mode Switching:
- Respect system preference
- Provide manual toggle
- Smooth transitions
- Persist user choice
- Test both modes thoroughly
Section 2: Keyboard Navigation Design (700 words)
Focus States and Tab Order
Keyboard navigation ensures users who cannot or prefer not to use a mouse can access all functionality efficiently.
Logical keyboard navigation flow with clear focus indicators
Focus Indicator Design with shadcn/ui
Built-in Focus Management with Radix UI:
// shadcn/ui components have focus management built-in
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Dialog, DialogContent } from "@/components/ui/dialog"
import { NavigationMenu } from "@/components/ui/navigation-menu"
// Automatic focus styles via Tailwind CSS
const focusClasses =
"focus-visible:outline-none focus-visible:ring-2 " +
"focus-visible:ring-ring focus-visible:ring-offset-2"
// Focus trap in modals (automatic with Radix UI)
<Dialog>
<DialogContent>
{/* Focus is trapped inside dialog */}
{/* Tab cycles through interactive elements */}
{/* Escape key closes dialog */}
</DialogContent>
</Dialog>
// Keyboard navigation in menus (automatic)
<NavigationMenu>
{/* Arrow keys navigate items */}
{/* Enter/Space selects */}
{/* Escape closes submenu */}
</NavigationMenu>
Skip Links Implementation:
// Skip to main content with shadcn/ui styling
export function SkipLink() {
return (
<a
href="#main-content"
className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 bg-background text-foreground p-2 rounded-md"
>
Skip to main content
</a>
)
}
.skip-link {
position: absolute;
top: -40px;
left: 0;
background: var(--primary);
color: white;
padding: 8px 16px;
text-decoration: none;
&:focus {
top: 0;
}
}
#### π Tab Order Management
**Logical Tab Flow**:
1. Skip to main content
2. Header navigation
3. Main content (top to bottom)
4. Sidebar (if present)
5. Footer links
**Tab Index Guidelines**:
```html
<!-- Natural tab order (recommended) -->
<button>First</button>
<button>Second</button>
<button>Third</button>
<!-- Only for dynamic content -->
<div tabindex="0" role="button">Focusable div</div>
<!-- Remove from tab order -->
<button tabindex="-1" aria-hidden="true">Hidden</button>
<!-- Never do this -->
<button tabindex="3">Bad practice</button>
Keyboard Shortcuts
Global Shortcuts:
| Key | Action | Context |
|---|---|---|
/ |
Focus search | Anywhere |
? |
Show shortcuts | Anywhere |
N |
New campaign | Dashboard |
Esc |
Close modal | Modal open |
Enter |
Confirm | Forms |
Implementation:
// Keyboard shortcut handler
class KeyboardManager {
constructor() {
this.shortcuts = new Map();
this.bindGlobalShortcuts();
}
register(key, action, context = 'global') {
this.shortcuts.set(`${context}:${key}`, action);
}
handleKeydown(event) {
// Don't interfere with form inputs
if (event.target.matches('input, textarea')) return;
const shortcut = `${this.context}:${event.key}`;
const action = this.shortcuts.get(shortcut);
if (action) {
event.preventDefault();
action();
}
}
}
Focus Management
Modal Focus Trap:
class FocusTrap {
constructor(element) {
this.element = element;
this.focusableElements = element.querySelectorAll(
'a, button, input, textarea, select, [tabindex="0"]'
);
this.firstFocusable = this.focusableElements[0];
this.lastFocusable = this.focusableElements[
this.focusableElements.length - 1
];
}
trap() {
this.element.addEventListener('keydown', (e) => {
if (e.key === 'Tab') {
if (e.shiftKey) {
if (document.activeElement === this.firstFocusable) {
e.preventDefault();
this.lastFocusable.focus();
}
} else {
if (document.activeElement === this.lastFocusable) {
e.preventDefault();
this.firstFocusable.focus();
}
}
}
});
}
}
Section 3: Screen Reader Optimization (700 words)
ARIA Labels and Structure
Screen reader optimization ensures users relying on assistive technology can understand and navigate our interface effectively.
Comprehensive ARIA implementation for optimal screen reader experience
Semantic HTML Structure
Landmark Regions:
<body>
<header role="banner">
<nav role="navigation" aria-label="Main">
<!-- Primary navigation -->
</nav>
</header>
<main role="main" aria-labelledby="page-title">
<h1 id="page-title">Dashboard</h1>
<section aria-labelledby="metrics-heading">
<h2 id="metrics-heading">Campaign Metrics</h2>
<!-- Content -->
</section>
</main>
<aside role="complementary" aria-label="Help">
<!-- Sidebar content -->
</aside>
<footer role="contentinfo">
<!-- Footer content -->
</footer>
</body>
ARIA Best Practices
Common ARIA Patterns:
| Pattern | Implementation | Purpose |
|---|---|---|
| Live Regions | aria-live="polite" |
Announce updates |
| Descriptions | aria-describedby |
Additional context |
| States | aria-expanded |
Dynamic changes |
| Labels | aria-label |
Non-visible labels |
| Relationships | aria-controls |
Connect elements |
Dynamic Content Announcements:
// Screen reader announcement utility
class Announcer {
constructor() {
this.liveRegion = document.createElement('div');
this.liveRegion.setAttribute('aria-live', 'polite');
this.liveRegion.setAttribute('aria-atomic', 'true');
this.liveRegion.className = 'sr-only';
document.body.appendChild(this.liveRegion);
}
announce(message, priority = 'polite') {
this.liveRegion.setAttribute('aria-live', priority);
this.liveRegion.textContent = message;
// Clear after announcement
setTimeout(() => {
this.liveRegion.textContent = '';
}, 1000);
}
}
// Usage
announcer.announce('Campaign saved successfully');
announcer.announce('Error: Email required', 'assertive');
Form Accessibility
Accessible Form Markup:
<form aria-labelledby="form-title">
<h2 id="form-title">Create Campaign</h2>
<div class="form-group">
<label for="campaign-name">
Campaign Name
<span aria-label="required">*</span>
</label>
<input
type="text"
id="campaign-name"
aria-required="true"
aria-invalid="false"
aria-describedby="name-hint name-error"
>
<span id="name-hint" class="hint">
Choose a memorable name
</span>
<span id="name-error" class="error" role="alert">
Name is required
</span>
</div>
<fieldset>
<legend>Send Time</legend>
<input type="radio" id="now" name="time" value="now">
<label for="now">Send immediately</label>
<input type="radio" id="later" name="time" value="later">
<label for="later">Schedule for later</label>
</fieldset>
</form>
Table Accessibility
Data Table Structure:
<table role="table" aria-label="Campaign Performance">
<caption class="sr-only">
Campaign metrics for the last 30 days
</caption>
<thead>
<tr>
<th scope="col" aria-sort="none">
<button aria-label="Sort by campaign name">
Campaign
</button>
</th>
<th scope="col">Sent</th>
<th scope="col">Opens</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Welcome Series</th>
<td>1,234</td>
<td>856 (69.4%)</td>
</tr>
</tbody>
</table>
Section 4: Touch Target Guidelines (700 words)
Mobile Accessibility
Touch accessibility ensures users with motor impairments or those using assistive touch devices can interact effectively with our interface.
Touch target specifications ensuring comfortable interaction for all users
Target Size Requirements
Minimum Touch Targets:
/* Base touch target sizing */
.touch-target {
min-width: 44px;
min-height: 44px;
display: inline-flex;
align-items: center;
justify-content: center;
/* Extend hit area without visual change */
position: relative;
&::before {
content: '';
position: absolute;
top: -8px;
right: -8px;
bottom: -8px;
left: -8px;
}
}
/* Different component sizes */
.button {
min-height: 44px;
padding: 0 24px;
}
.icon-button {
width: 44px;
height: 44px;
padding: 0;
}
.form-input {
min-height: 44px;
padding: 0 16px;
}
Spacing and Grouping
Touch Target Spacing:
| Context | Minimum Gap | Recommended |
|---|---|---|
| Buttons | 8px | 16px |
| Links in text | 1 space | 8px |
| Form fields | 8px | 16px |
| Icons | 8px | 12px |
| Menu items | 0px | 8px |
Grouped Actions:
<!-- Well-spaced action group -->
<div class="action-group" role="group" aria-label="Campaign actions">
<button class="button-primary">
<span class="icon">βοΈ</span>
Edit
</button>
<button class="button-secondary">
<span class="icon">π</span>
Analytics
</button>
<button class="button-danger">
<span class="icon">ποΈ</span>
Delete
</button>
</div>
<style>
.action-group {
display: flex;
gap: 16px; /* Comfortable spacing */
@media (max-width: 640px) {
flex-direction: column;
gap: 12px;
button {
width: 100%;
justify-content: center;
}
}
}
</style>
Gesture Support
Accessible Gestures:
// Gesture handler with alternatives
class GestureManager {
constructor(element) {
this.element = element;
this.setupGestures();
}
setupGestures() {
// Swipe to delete with alternative
let startX = 0;
this.element.addEventListener('touchstart', (e) => {
startX = e.touches[0].clientX;
});
this.element.addEventListener('touchend', (e) => {
const endX = e.changedTouches[0].clientX;
const diff = startX - endX;
if (diff > 100) {
// Swipe left detected
this.showDeleteButton();
}
});
// Always provide button alternative
this.addDeleteButton();
}
addDeleteButton() {
const button = document.createElement('button');
button.className = 'delete-button';
button.setAttribute('aria-label', 'Delete item');
button.innerHTML = '<span aria-hidden="true">ποΈ</span>';
this.element.appendChild(button);
}
}
Section 5: Cognitive Load Reduction (600 words)
Simplification Strategies
Reducing cognitive load benefits all users but is especially important for users with cognitive disabilities, ADHD, or those using the interface under stress.
High Cognitive Load
- Multiple simultaneous tasks
- Complex decision trees
- Unclear next steps
- Information overload
Reduced Load
- One task at a time
- Clear pathways
- Obvious actions
- Progressive disclosure
Clear Language
Writing Guidelines:
- Use common words (grade 8 level)
- Short sentences (15-20 words)
- Active voice
- Avoid jargon
- Define technical terms
Examples:
// β Complex
"Configure automated segmentation parameters for optimized targeting"
// β
Simple
"Choose who should receive this email"
// β Technical
"API rate limit exceeded (429)"
// β
Human
"Too many requests. Please wait a moment and try again."
Progressive Disclosure
Implementation Pattern:
<div class="progressive-form">
<!-- Always visible -->
<div class="basic-options">
<label>Campaign Name</label>
<input type="text" required>
<label>Recipients</label>
<select>
<option>All subscribers</option>
<option>Custom segment</option>
</select>
</div>
<!-- Hidden by default -->
<details class="advanced-options">
<summary>Advanced Options</summary>
<div class="advanced-content">
<!-- Complex settings here -->
</div>
</details>
</div>
Chunking Information
Break Complex Tasks:
// Step-by-step wizard
const CampaignWizard = {
steps: [
{
title: "Choose Template",
description: "Pick a design for your email",
fields: ["template_id"]
},
{
title: "Write Content",
description: "Add your message",
fields: ["subject", "body"]
},
{
title: "Select Recipients",
description: "Choose who gets your email",
fields: ["segment_id"]
},
{
title: "Review & Send",
description: "Check everything looks good",
fields: []
}
]
};
Section 6: Testing & Validation (500 words)
Accessibility Testing Plan
Regular testing ensures our accessibility efforts achieve their intended outcomes and catch regressions early.
Testing Tools
Automated Testing Stack:
{
"devDependencies": {
"axe-core": "^4.6.0",
"pa11y": "^6.2.0",
"@testing-library/jest-dom": "^5.16.0",
"cypress-axe": "^1.4.0"
}
}
Automated Test Example:
// Jest + React Testing Library
test('form has accessible labels', () => {
render(<ContactForm />);
const nameInput = screen.getByLabelText('Name');
expect(nameInput).toBeInTheDocument();
expect(nameInput).toHaveAttribute('aria-required', 'true');
});
// Cypress + Axe
it('has no accessibility violations', () => {
cy.visit('/dashboard');
cy.injectAxe();
cy.checkA11y();
});
Manual Testing Checklist
Keyboard Navigation with shadcn/ui:
- Tab through entire page (Radix UI manages tab order)
- All interactive elements reachable (automatic with shadcn/ui)
- Logical tab order (component structure ensures this)
- Focus indicators visible (Tailwind focus-visible classes)
- No keyboard traps (Dialog/Sheet components handle escape)
Screen Reader Testing with shadcn/ui Components:
- Test with NVDA (Radix UI ARIA support)
- Test with JAWS (automatic role attributes)
- Test with VoiceOver (aria-label/describedby work)
- Test with TalkBack (mobile ARIA support)
- All content announced correctly (Radix UI handles announcements)
// shadcn/ui components include proper ARIA
import { Alert, AlertDescription } from "@/components/ui/alert"
import { Progress } from "@/components/ui/progress"
// Automatic ARIA attributes
<Alert> {/* role="alert" added automatically */}
<AlertDescription>Important message</AlertDescription>
</Alert>
<Progress value={60} /> {/* aria-valuenow, aria-valuemin, aria-valuemax */}
Visual Testing:
- 200% zoom usable
- Color contrast passes
- Works without color
- Text resizable
- No horizontal scroll
Testing Schedule
| Test Type | Frequency | Responsibility |
|---|---|---|
| Automated | Every commit | CI/CD pipeline |
| Manual keyboard | Weekly | Developers |
| Screen reader | Bi-weekly | QA team |
| User testing | Monthly | UX team |
| Full audit | Quarterly | External audit |
Conclusion
Accessibility is a journey, not a destination. By embedding these practices into our design and development process, we ensure NudgeCampaign remains usable and delightful for everyone, creating a more inclusive email marketing platform.
Next Steps
- Review Onboarding Flow Design for accessible user journeys
- Explore Template Gallery Design for content accessibility
- Implement automated testing in development workflow
This accessibility guide ensures NudgeCampaign exceeds compliance requirements while creating experiences that truly work for everyone.