Accessibility
The Coconut Design System is built with accessibility as a core principle. Every component is designed to be perceivable, operable, understandable, and robust for all users, including those who rely on assistive technologies.
WCAG Compliance
The Web Content Accessibility Guidelines (WCAG) 2.1 is the international standard for web accessibility. It defines how to make web content more accessible to people with disabilities, including blindness, low vision, deafness, hearing loss, limited movement, speech disabilities, photosensitivity, and cognitive limitations. The Coconut Design System targets Level AA conformance, which covers the most common barriers for the widest range of users.
| Level | Description | Requirements | Status |
|---|---|---|---|
| A | Minimum | Text alternatives for images, keyboard navigable, no seizure-inducing content, basic page structure | Compliant |
| AA | Recommended | Color contrast ≥ 4.5:1 for text, resizable text up to 200%, visible focus indicators, consistent navigation, error identification | Compliant |
| AAA | Enhanced | Color contrast ≥ 7:1, sign language for multimedia, extended audio descriptions, no timing limits | Partial |
Skip Link
A skip link is the first focusable element on the page. It is visually hidden until the user presses Tab, at which point it slides into view. This allows keyboard and screen reader users to jump directly to the main content, bypassing the topbar and sidebar navigation.
<!-- Place as first child of <body> -->
<a href="#main-content" class="skip-link">
Skip to main content
</a>
/* CSS */
.skip-link {
position: absolute;
top: -100%;
left: 1rem;
background: var(--accent);
color: #fff;
padding: 8px 16px;
border-radius: 6px;
font-size: 0.78rem;
font-weight: 600;
z-index: 1000;
text-decoration: none;
transition: top 0.2s ease;
}
.skip-link:focus {
top: 0.5rem;
}Color Contrast
WCAG 2.1 requires a contrast ratio of at least 4.5:1 for normal text and 3:1 for large text (18px+ bold or 24px+ regular). A ratio of 7:1 or higher achieves AAA level. Every color combination in the design system has been tested to ensure readability.
| Foreground | Background | Sample | Ratio | Level |
|---|---|---|---|---|
--gray-900 #1b1b1f |
--white #ffffff |
Aa |
15.4:1 | AAA |
--muted #616568 |
--white #ffffff |
Aa |
5.2:1 | AA |
--accent #744F28 |
--white #ffffff |
Aa |
6.5:1 | AA |
--red #ba1a1a |
--white #ffffff |
Aa |
5.7:1 | AA |
| Foreground | Background | Sample | Ratio | Level |
|---|---|---|---|---|
--text #ffffff |
--bg #020617 |
Aa |
19.3:1 | AAA |
--muted #94a3b8 |
--bg #020617 |
Aa |
5.8:1 | AA |
Focus States
Every interactive element in the design system displays a clearly visible focus ring when navigated to via keyboard. The standard focus style uses a 2px solid accent-colored outline with 1px offset, ensuring it does not overlap the element content and remains visible on all backgrounds.
/* Global focus style for all interactive elements */
:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 1px;
}
/* Remove default outline for mouse users */
:focus:not(:focus-visible) {
outline: none;
}
/* Enhanced focus for dark mode */
body.dark :focus-visible {
outline-color: #C4956A;
}outline: none without a replacement makes your interface unusable for keyboard-only users.
Keyboard Navigation
Every interactive element in the Coconut Design System is reachable and operable with a keyboard alone. The tab order follows a logical, predictable sequence through the page structure.
| Component | Key | Action |
|---|---|---|
| All interactive | Tab | Move focus to next element |
| All interactive | Shift+Tab | Move focus to previous element |
| Button / Link | Enter | Activate the button or follow the link |
| Button | Space | Activate the button |
| Accordion | Enter / Space | Toggle accordion panel open/closed |
| Toggle Switch | Space | Toggle switch on/off |
| Modal | Escape | Close the modal and return focus to trigger |
| Modal | Tab | Cycle focus within modal (focus trap) |
| Tabs | Arrow Left/Right | Navigate between tab items |
When a modal is open, focus must be trapped inside it. Pressing Tab on the last focusable element loops back to the first, and vice versa.
function trapFocus(modal) {
const focusable = modal.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const first = focusable[0];
const last = focusable[focusable.length - 1];
modal.addEventListener('keydown', (e) => {
if (e.key !== 'Tab') return;
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
});
first.focus();
}ARIA Patterns
ARIA (Accessible Rich Internet Applications) attributes provide extra semantic information to assistive technologies. Use them to communicate state, relationships, and purpose that HTML alone cannot convey. Follow the first rule of ARIA: do not use ARIA if native HTML can do the job.
| Component | Attributes | Purpose |
|---|---|---|
| Accordion |
role="button"aria-expanded="true|false"aria-controls="panel-id"
|
Communicates expandable state to screen readers |
| Toast / Alert |
role="alert"aria-live="assertive"
|
Announces dynamic notifications immediately |
| Modal |
role="dialog"aria-modal="true"aria-labelledby="title-id"
|
Identifies modal and labels it for assistive tech |
| Toggle Switch |
role="switch"aria-checked="true|false"aria-label="Feature name"
|
Communicates on/off state |
| Tabs |
role="tablist" on containerrole="tab" on each tabrole="tabpanel" on panelsaria-selected="true"
|
Establishes tab-panel relationship |
| Icons (decorative) |
aria-hidden="true"
|
Hides decorative icons from screen readers |
| Progress Bar |
role="progressbar"aria-valuenow="45"aria-valuemin="0"aria-valuemax="100"
|
Conveys current progress percentage |
| Navigation |
aria-label="Main navigation"aria-current="page"
|
Labels navigation landmark and current page |
<!-- Accessible Accordion -->
<div class="accordion-header"
role="button"
tabindex="0"
aria-expanded="false"
aria-controls="panel-1">
<span class="accordion-title">Section Title</span>
<span class="accordion-arrow" aria-hidden="true">expand_more</span>
</div>
<div id="panel-1"
class="accordion-body"
role="region"
aria-labelledby="header-1">
Panel content here.
</div><!-- Live region for toast notifications -->
<div class="toast-container"
role="alert"
aria-live="assertive"
aria-atomic="true">
Settings saved successfully.
</div>
<!-- For less urgent updates, use polite -->
<div aria-live="polite">
3 new items loaded.
</div><!-- Decorative icon — hidden from screen readers -->
<span class="material-symbols-outlined"
aria-hidden="true">settings</span>
<!-- Icon-only button — needs aria-label -->
<button aria-label="Open settings">
<span class="material-symbols-outlined"
aria-hidden="true">settings</span>
</button>Reduced Motion
Some users experience motion sickness, vertigo, or migraines from animated content. The prefers-reduced-motion media query detects whether the user has requested minimal animations through their operating system settings. All animations and transitions in the Coconut Design System respect this preference.
/* Disable all transitions and animations when user prefers reduced motion */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
/* Or selectively disable specific animations */
@media (prefers-reduced-motion: reduce) {
.badge-dot {
animation: none; /* Stop pulsing dot */
}
.card:hover {
transform: none; /* No hover lift */
}
.docs-sidebar {
transition: none; /* No slide animation */
}
}You can also detect the preference in JavaScript for more complex animation logic:
const prefersReducedMotion = window.matchMedia(
'(prefers-reduced-motion: reduce)'
);
if (prefersReducedMotion.matches) {
// Skip animations, use instant transitions
chart.update({ animation: false });
}
// Listen for changes (user toggles preference live)
prefersReducedMotion.addEventListener('change', (e) => {
if (e.matches) {
disableAnimations();
} else {
enableAnimations();
}
});Screen Reader Best Practices
Screen readers interpret the DOM and communicate structure, content, and interactive elements to users. Using semantic HTML is the most effective way to ensure compatibility. The Coconut Design System recommends these practices for all implementations.
Landmarks allow screen reader users to jump between major sections of a page. Use them consistently across all pages.
<!-- Correct landmark structure -->
<header class="docs-topbar">...</header>
<nav class="docs-sidebar" aria-label="Main navigation">...</nav>
<main id="main-content">...</main>
<footer class="docs-footer">...</footer>
<!-- Avoid generic divs for structural elements -->
<!-- Bad: <div class="header"> -->
<!-- Good: <header> --><img alt="Server rack with 12 units occupied"><img alt="" role="presentation"><img alt="Line chart showing 12% revenue growth over Q3">Sometimes you need text that is read by screen readers but not visible on screen. Use the .sr-only utility class instead of display: none or visibility: hidden, which hide content from assistive tech too.
/* Visually hide but keep accessible to screen readers */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
<!-- Usage example -->
<button class="btn btn-red">
<span class="material-symbols-outlined" aria-hidden="true">delete</span>
<span class="sr-only">Delete item</span>
</button>Screen reader users navigate by headings. Always maintain a logical heading hierarchy without skipping levels.
h1 directly to h4 breaks the document outline and confuses screen reader navigation.
Touch Targets
WCAG 2.5.5 (AAA) and the Apple Human Interface Guidelines recommend a minimum touch target size of 44 x 44 pixels. This ensures that users with limited dexterity or those using assistive devices can reliably tap interactive elements. All Coconut Design System components are designed to meet this minimum.
| Component | Visual Size | Effective Touch Area | Meets 44px |
|---|---|---|---|
| Button (.btn) | padding: 8px 20px |
Height: ~36px (font 0.78rem + 16px padding). Add min-height: 44px on mobile. |
Yes |
| Sidebar Link | height: 36px |
Height 36px + padding within the row. The spacing between links provides combined 44px area. | Yes |
| Toggle Switch | 44px x 24px |
Width meets minimum. Height boosted to 44px via touch area padding. | Yes |
| Checkbox / Radio | 18px x 18px |
Label wraps the input, extending the clickable area to full row width. | Yes |
| Icon Button | 30px x 30px |
Padding extended on mobile via min-width: 44px; min-height: 44px. |
Yes |
/* Ensure minimum touch target on mobile */
@media (max-width: 900px) {
.btn {
min-height: 44px;
min-width: 44px;
}
.theme-toggle,
.docs-hamburger {
min-width: 44px;
min-height: 44px;
}
.docs-sidebar-link {
min-height: 44px;
}
}Accessibility Checklist
alt text (or alt="" if decorative)
header, nav, main, footer)
<label> elements or aria-label
aria-live regions for screen reader announcements
prefers-reduced-motion media query