Accessibility
Accessibility isn't a separate checklist — it's part of how every Senco component is built. All components target WCAG 2.2 Level AA as a minimum.
Senco's users include people on damaged screens, people using the app one-handed on a moving vehicle, people whose second or third language is the interface language, and people for whom a failed transfer means a missed rent payment. Accessibility here is not a compliance exercise layered on top of the design — it is the same work as making the product usable at all.
Target standard
WCAG 2.2 Level AA for all components, patterns, and page templates. Level AAA where practical — notably enhanced contrast on financial data and a 48px touch target that exceeds even the AAA requirement.
WCAG 2.2 is backward-compatible with 2.1, so meeting 2.2 AA means meeting 2.1 AA. The criteria added in 2.2 that matter most here:
| Criterion | Level | Senco position |
|---|---|---|
| 2.4.11 Focus Not Obscured (Minimum) | AA | Sticky action bar must not cover the focused field |
| 2.5.7 Dragging Movements | AA | No control requires a drag; every slider has a numeric input |
| 2.5.8 Target Size (Minimum) — 24px | AA | Exceeded; Senco requires 48px |
| 3.2.6 Consistent Help | A | Help entry point sits in the same place on every screen |
| 3.3.7 Redundant Entry | A | Recipient and address details are pre-filled from a prior step |
| 3.3.8 Accessible Authentication (Minimum) | AA | No cognitive-function test; OTP fields accept paste |
Redundant Entry and Accessible Authentication carry real weight in a transfer product. A user must never be asked to re-type an account number they entered two steps ago, and an OTP field that blocks paste forces people to memorize a six-digit code under time pressure.
Color and contrast
| Requirement | Ratio | Applies to |
|---|---|---|
| Normal text | 4.5:1 | Body text, labels, descriptions |
| Large text (≥18.66px bold or ≥24px) | 3:1 | Headings, large labels |
| UI components | 3:1 | Borders, icons, focus rings, form controls |
| Non-text contrast | 3:1 | Charts, graphs, status indicators |
- Never use color alone to convey meaning — always pair with text, icons, or patterns
- Status badges use color + dot indicator + text label
- Error states use red border + error icon + descriptive text
The large-text trap
"Large text" means ≥18.66px bold or ≥24px at any weight. An 18px semibold heading is not large text. Senco's H4 sits exactly there, which is why H2 and H3 are specified at weight 700 and H4 is held to the full 4.5:1. See Typography.
Contrast verification
All token pairings are verified in both light and dark mode. Full tables in Color and Themes.
Two results are not clean, and both are recorded rather than rounded off:
| Item | Measured | Status |
|---|---|---|
Resting input border ({gray-200} on white) | 1.24:1 | Accepted deviation — fields carry a persistent visible label and a 48px control area |
Disabled button label on its fill — light {gray-500} on {gray-300}, dark #7E8EA3 on #334155 | 3.76:1 light, 3.10:1 dark | Exempt — SC 1.4.3 excludes inactive components. Recorded because the exemption is a choice: a disabled control here stays focusable and describes why it is disabled, so its label is still read |
The dark-mode primary button label, previously an open defect at 3.23:1, is resolved by a Platform-layer remap of --ds-on-accent — see Themes.
Visible label above the input stays present when the user types.
Placeholder-only label disappears on focus, leaving no context.
All text and UI element pairings must meet minimum contrast ratios.
| Element | Minimum ratio | Standard |
|---|---|---|
| Normal text (<18px) | 4.5:1 | WCAG 1.4.3 AA |
| Large text (18px+ bold, 24px+) | 3:1 | WCAG 1.4.3 AA |
| UI components & icons | 3:1 | WCAG 1.4.11 |
| Focus indicators | 3:1 | WCAG 2.4.7 |
Keyboard navigation
All interactive components must be fully operable with keyboard only.
| Key | Action |
|---|---|
Tab | Move focus to next interactive element |
Shift+Tab | Move focus to previous element |
Enter / Space | Activate buttons, links, toggles |
Arrow keys | Navigate within menus, tabs, radio groups |
Escape | Close dialogs, tooltips, popovers, dropdowns |
Home / End | Jump to first/last item in a list |
One Tab stop per composite
A radio group, a tab list, a stepper, and a menubar are each one Tab stop. Tab moves between components; arrow keys move within them. A ten-option currency radio group that consumes ten Tab presses is technically operable and practically unusable.
Focus management
- Focus has two forms. Bordered controls (inputs, selects) turn their border
--ds-focusand add a 3px halo at 20% opacity with no offset. Borderless controls (buttons, links, tabs, rows) take a 3px solid--ds-focusoutline with a 2px offset. The rule for choosing is in Tokens - The indicator that must meet 3:1 is the focused border or the outline, both 5.00:1 on white. The halo is decoration and is not counted
- Use
:focus-visible, not:focus— a mouse click should not leave a ring behind, a Tab press must - Focus order follows visual reading order (left-to-right, top-to-bottom)
- Never remove a
:focus-visibleoutline without providing an alternative - Dialogs trap focus and return it to the trigger on close
- After a route change, focus moves to the new page's
<h1>— not to<body>, which strands screen reader users at the top of the chrome - The focused element must never be obscured by the sticky action bar (SC 2.4.11)
The ring is built with color-mix() against --ds-focus, so it re-derives correctly when the theme remaps the focus color. A hard-coded rgba ring would have needed a second dark-mode token and would have drifted.
Destructive and irreversible actions
A transfer confirmation cannot be triggered by Enter from anywhere on the page. The primary action requires focus on the button itself. This is deliberate friction on the one action in the product that cannot be undone.
- Visible focus indicator on all interactive elements
- Focus ring meets 3:1 contrast against adjacent colors
- Custom focus styles consistent across the system
- Never remove outline without providing an alternative
3:1 contrast, 3px ring, visible on the focused element
Keyboard users cannot see where they are
- All interactive elements reachable via Tab
- Logical focus order matching visual order
- Escape key closes modals and overlays
- Arrow keys navigate within composite widgets
- Enter/Space activates buttons and controls
Tab index
Use tabIndex to control keyboard focus order.
| Value | Behavior | When to use |
|---|---|---|
tabIndex="0" | Follows natural DOM order | Custom interactive elements (div with onClick) |
tabIndex="-1" | Focusable via JS, skipped by Tab | Off-screen elements, programmatic focus (modals, error summaries) |
tabIndex="1+" | Forces priority order | Avoid — breaks natural flow, confuses users |
Semantic HTML
- Use native HTML elements whenever possible (
<button>,<input>,<nav>,<main>) - Avoid
<div>and<span>for interactive elements - Use heading hierarchy sequentially (h1 → h2 → h3, no skipping)
- Use landmark elements:
<header>,<nav>,<main>,<footer>,<aside> - Lists use
<ul>/<ol>, not styled divs - One
<h1>per page, and it names the page — not the product
A native <button> brings keyboard activation, focus management, disabled semantics, form participation, and the correct screen reader role for free. Every one of those has to be rebuilt by hand on a <div role="button">, and in practice at least one is always missed.
ARIA patterns
Use ARIA only when native HTML semantics are insufficient.
| Pattern | When to use |
|---|---|
aria-label | Icon-only buttons, unlabeled controls |
aria-describedby | Error messages, hint text, supplementary info |
aria-live | Dynamic content updates (toast, loading status) |
aria-current="page" | Active navigation item, breadcrumb current page |
aria-expanded | Accordion triggers, dropdown toggles |
aria-invalid="true" | Form inputs with validation errors |
role="alert" | Error summaries, critical notifications |
Politeness levels
| Level | Used for | Example |
|---|---|---|
aria-live="polite" | Status that can wait for a pause | "Rate updated", "3 results" |
aria-live="assertive" / role="alert" | Something the user must act on now | Transfer failed, session expiring |
Assertive interrupts whatever the screen reader is currently saying. Using it for a rate refresh means cutting a user off mid-sentence every thirty seconds. In Senco only failures and security events are assertive.
aria-disabled, not disabled
A disabled button is removed from the tab order, so a keyboard user tabbing to the bottom of a form finds nothing there and no explanation of why. Senco uses aria-disabled="true" with the click handler suppressed, so the control stays focusable and its aria-describedby can say what is missing.
The first rule
No ARIA is better than bad ARIA. An incorrect role overrides correct native semantics and produces an element that announces as something it is not. Every ARIA attribute in a component should be justifiable in one sentence.
- Use semantic HTML first, ARIA second
- Apply ARIA roles and landmarks to define page regions
- Use aria-label, aria-describedby, and aria-live where needed
- Never use ARIA to override semantic meaning
Landmarks let assistive technology users jump between page regions.
Screen reader testing
Test all components with at least two screen readers:
| Platform | Screen reader | Browser |
|---|---|---|
| macOS | VoiceOver | Safari |
| Windows | NVDA | Firefox or Chrome |
| Mobile | TalkBack (Android) | Chrome |
| Mobile | VoiceOver (iOS) | Safari |
TalkBack on Chrome is the highest-priority combination, not the last one. It matches the device profile of most of Senco's users.
What to verify
- All interactive elements have accessible names
- Form errors are announced when they appear
- Page title updates on navigation
- Dynamic content changes are announced via
aria-live - Images have meaningful alt text (or
aria-hiddenif decorative) - Amounts announce sensibly — "250 dollars", not "2 5 0 U S D"
- Status changes on a pending transfer are announced without the user re-navigating
Automated coverage is a floor
Automated tooling catches roughly a third of WCAG issues, and none of the ones that matter most here: whether the focus order makes sense, whether an error message explains how to fix the problem, whether a status announcement arrives at a useful moment. Automated checks gate the pull request; manual testing gates the release.
- Meaningful alt text for images
- Hidden descriptive text for icon-only actions
- Live regions for dynamic content updates
- Form labels and error messages announced correctly
Alt text for images
alt="USDT wallet ending 9c1d, selected"alt="icon"alt="wallet"alt="South Korean flag — Min-ji Kim, sent $250 CAD"alt="flag"alt=""Skip to content
A skip link is a hidden anchor that becomes visible on keyboard focus. It lets users bypass repetitive navigation and jump straight to the main content area.
- The skip link must be the first focusable element on the page
- It is visually hidden by default and appears on
:focus - The target must be a landmark with an
id(e.g.,#ds-main-content) - Skip links are required on every page with persistent navigation
- The target must be focusable (
tabindex="-1"), or focus lands on the anchor and the next Tab returns to the navigation
<body>
<a href="#main" class="ds-skip-link">
Skip to main content
</a>
<nav>...</nav>
<main id="main">...</main><body>
<nav>...</nav>
<a href="#main">
Skip to main content
</a>
<main id="main">...</main>Motion and animation
- Respect
prefers-reduced-motion - Disable non-essential animations when reduced motion is preferred
- Spinners stop rotating and stay visible; their text label carries the loading state
- Nothing flashes more than three times per second (SC 2.3.1)
- Motion is never the only carrier of a state change
Handled globally at the Platform layer — see Motion.
Touch targets
- Minimum touch target size: 48×48 CSS pixels (exceeds WCAG 2.5.5 AAA and 2.5.8 AA)
- Minimum spacing between targets: 8px
- The visual element can be smaller — the tappable hit area must be 48×48px
Checklist
Use this checklist for every new component or pattern. Nothing ships with an unchecked box or an unrecorded exception.
- Color contrast passes 4.5:1 for text, 3:1 for UI — in both themes
- Keyboard operable (Tab, Enter, Escape, arrow keys) with one Tab stop per composite
- Focus indicator visible, 3px at 20%, meets contrast, uses
:focus-visible - Focused element is never obscured by sticky chrome
- Screen reader announces name, role, and state
- Error states use text + icon, not color alone
- Error messages say how to fix the problem, not just that one exists
- Touch targets are 48×48px minimum with 8px spacing
-
prefers-reduced-motionis respected and no state is motion-only - Semantic HTML used (no div buttons)
- ARIA attributes correct and minimal;
aria-disabledoverdisabled - Tested with VoiceOver and NVDA; TalkBack for anything on the transfer path
- Text reflows at 320px and 200% zoom without horizontal scroll
- Works at 200% browser text size without clipping
Responsibilities
| Role | Owns |
|---|---|
| Design | Contrast, target size, focus order in the layout, error copy that explains the fix |
| Engineering | Semantic markup, keyboard behavior, ARIA, live regions, focus management |
| QA | Screen reader passes, zoom and reflow, both themes, the checklist above |
Accessibility failures routed back by QA are classified by source rather than filed generically — a contrast failure goes to design with the criterion named, a missing live region goes to engineering.
Known gaps
| Item | Status |
|---|---|
forced-colors / Windows high contrast | Not implemented or tested |
| Resting input border | Accepted deviation with rationale |
| Automated a11y gate in CI | Not configured |
| Per-component accessibility status table | Not published — Carbon-style per-variant reporting is open work |
| Cognitive accessibility review | Not formally conducted, despite a multilingual, time-pressured audience |