Accessibility Guidelines β
Shelf.nu is committed to providing an accessible experience for all users. This guide outlines our accessibility standards and provides practical guidance for developers.
Standards β
We aim to meet WCAG 2.1 Level AA standards for all user-facing features.
Key Principles β
1. Color Contrast (WCAG 2.1 AA) β
All text must meet minimum contrast ratios:
- 4.5:1 for normal text (under 18pt or 14pt bold)
- 3:1 for large text (18pt+ or 14pt+ bold)
Using the Badge Component β
The <Badge> component automatically ensures WCAG AA compliance:
import { Badge } from "~/components/shared/badge";
// Any hex color will be darkened automatically for proper contrast
<Badge color="#2E90FA">In Custody</Badge>
<Badge color="#5925DC">Checked Out</Badge>
<Badge color="#12B76A">Available</Badge>How it works: The Badge component uses darkenColor() to reduce RGB values by 50%, ensuring text has sufficient contrast against the 30% opacity background.
Test results:
- IN_CUSTODY badge: 6.69:1 β (exceeds AA, approaching AAA)
- CHECKED_OUT badge: 8.59:1 β (exceeds AAA)
- AVAILABLE badge: 6.02:1 β (exceeds AA, approaching AAA)
Color Contrast Utilities β
For custom components, use the utilities in app/utils/color-contrast.ts:
import {
getContrastRatio,
meetsWCAG_AA,
meetsWCAG_AAA,
getAccessibleTextColor,
darkenColor,
} from "~/utils/color-contrast";
// Check if two colors meet WCAG AA
const isAccessible = meetsWCAG_AA("#2E90FA", "#FFFFFF"); // true
// Get contrast ratio
const ratio = getContrastRatio("#2E90FA", "#FFFFFF"); // 6.69
// Determine if text should be black or white
const textColor = getAccessibleTextColor("#2E90FA"); // "#000000"
// Darken a color for better contrast
const darkened = darkenColor("#2E90FA", 0.5); // "#174578"Tailwind Color Guidelines β
When using Tailwind utility classes:
β Good combinations:
<div className="bg-primary-50 text-primary-800">Safe contrast</div>
<div className="bg-gray-100 text-gray-700">Safe contrast</div>
<div className="bg-success-100 text-success-800">Safe contrast</div>β Avoid:
<div className="bg-primary-50 text-primary-500">Poor contrast</div>
<div className="bg-gray-100 text-gray-400">Poor contrast</div>Rule of thumb: For *-50 or *-100 backgrounds, use *-700 or *-800 text.
2. Keyboard Navigation β
All interactive elements must be keyboard accessible.
Focus Indicators β
All focusable elements should have visible focus indicators. The Button component includes focus:ring-2 for primary and danger variants:
// Button component automatically includes focus states
<Button variant="primary">Has focus ring</Button>
<Button variant="danger">Has focus ring</Button>
<Button variant="secondary">Has border focus</Button>For custom interactive elements:
// Add visible focus state
<button className="focus:ring-2 focus:ring-primary focus:outline-none">
Custom Button
</button>Tab Order β
Ensure logical tab order by:
- Using semantic HTML (
<button>,<a>,<input>) - Avoiding
tabindexvalues greater than 0 - Only using
tabindex="-1"to programmatically remove from tab order
// Good - semantic HTML maintains natural tab order
<button onClick={handleClick}>Click me</button>
// Avoid - non-semantic elements require extra attributes
<div onClick={handleClick} role="button" tabIndex={0}>Click me</div>3. Modals and Dialogs β
Escape Key Behavior β
All modals and dialogs must close when the Escape key is pressed.
Radix UI modals (AlertDialog, Sheet) handle this automatically:
import { AlertDialog, AlertDialogContent } from "~/components/shared/modal";
// Escape key handling is built-in
<AlertDialog open={isOpen} onOpenChange={setIsOpen}>
<AlertDialogContent>{/* Content */}</AlertDialogContent>
</AlertDialog>;Native dialog elements require manual handling:
import { Dialog } from "~/components/layout/dialog";
// Dialog component includes useEffect for Escape key
<Dialog open={isOpen} onClose={handleClose} title="My Dialog">
{/* Content */}
</Dialog>;Focus Management β
Modals should:
- β Trap focus within the modal when open
- β Return focus to the trigger element when closed
- β Set initial focus to the first focusable element or close button
Radix UI components handle this automatically. For custom modals, use focus trap libraries like react-focus-lock or @radix-ui/react-focus-scope.
4. Screen Reader Announcements β
Toast Notifications β
The Toast component uses Radix UI Toast which provides automatic aria-live announcements:
import { showNotificationAtom } from "~/atoms/notifications";
// Automatically announced to screen readers
showNotification({
title: "Success",
message: "Your changes have been saved",
icon: { name: "success", variant: "success" },
});Implementation: Toast uses aria-live regions built into Radix UI, plus explicit aria-label on the close button.
Dynamic Content Updates β
For other dynamic content updates:
// Polite announcement (waits for user to pause)
<div aria-live="polite" aria-atomic="true">
{statusMessage}
</div>
// Assertive announcement (interrupts screen reader)
<div aria-live="assertive" aria-atomic="true">
{errorMessage}
</div>Labels and Descriptions β
All interactive elements must have accessible names:
// Buttons with text content
<Button>Submit</Button> // β Text provides accessible name
// Icon-only buttons need aria-label
<Button aria-label="Close dialog">
<XIcon />
</Button>
// Form inputs need labels
<label htmlFor="email">Email</label>
<input id="email" type="email" />
// Or use aria-label when visual label isn't appropriate
<input type="search" aria-label="Search assets" />Testing Checklist β
Automated Testing β
Run color contrast tests:
npm run test app/utils/color-contrast.test.tsManual Testing β
Keyboard Navigation β
- [ ] Tab through all interactive elements
- [ ] Verify visible focus indicators on all focusable elements
- [ ] Test Escape key closes modals and returns focus
- [ ] Test Enter/Space activates buttons and links
- [ ] Verify no keyboard traps (can tab out of all components)
Screen Reader Testing β
macOS (VoiceOver):
# Start VoiceOver
Cmd+F5
# Navigate
Ctrl+Option+Arrow keysWindows (NVDA):
- Download from nvaccess.org
- Navigate with arrow keys
Test:
- [ ] Toast notifications are announced
- [ ] Form labels and errors are announced
- [ ] Button labels are descriptive
- [ ] Dynamic content updates are announced
- [ ] Modal dialogs announce title and content
Color Contrast β
Browser DevTools:
- Inspect element
- Check "Contrast" in Styles panel
- Verify ratio meets 4.5:1 (or 3:1 for large text)
Online tools:
Color Blindness β
Test with browser extensions:
- Chrome: Colorblindly
- Firefox: Colorblind
Verify information isn't conveyed by color alone.
Common Patterns β
Loading States β
// Screen reader announcement for loading
<div role="status" aria-live="polite">
{isLoading ? "Loading..." : null}
</div>Error Messages β
// Associate error with input using aria-describedby
<input
id="email"
aria-invalid={hasError}
aria-describedby={hasError ? "email-error" : undefined}
/>;
{
hasError && (
<div id="email-error" role="alert">
{errorMessage}
</div>
);
}Tooltips β
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "~/components/shared/tooltip";
// Radix UI handles accessibility
<Tooltip>
<TooltipTrigger>
<InfoIcon />
</TooltipTrigger>
<TooltipContent>
<p>Additional information</p>
</TooltipContent>
</Tooltip>;Disabled Buttons with Explanation β
<Button
disabled={{
title: "Action disabled",
reason: "You need admin permissions to perform this action",
}}
>
Delete
</Button>
// Button component shows tooltip on hoverComponent Library β
Shelf uses Radix UI primitives which provide:
- β Built-in keyboard navigation
- β ARIA attributes
- β Focus management
- β Screen reader support
When building new components, prefer Radix UI primitives over custom implementations.
Resources β
- WCAG 2.1 Guidelines
- Radix UI Accessibility
- WebAIM Color Contrast Checker
- a11y Project Checklist
- ARIA Authoring Practices Guide
Getting Help β
If you have questions about accessibility:
- Check this guide and linked resources
- Test with the tools mentioned above
- Ask in the team Discord #development channel
- Review similar patterns in the codebase
Remember: Accessibility is not a featureβit's a requirement for inclusive software.
