Skip to main content
ENKO

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:

CriterionLevelSenco position
2.4.11 Focus Not Obscured (Minimum)AASticky action bar must not cover the focused field
2.5.7 Dragging MovementsAANo control requires a drag; every slider has a numeric input
2.5.8 Target Size (Minimum) — 24pxAAExceeded; Senco requires 48px
3.2.6 Consistent HelpAHelp entry point sits in the same place on every screen
3.3.7 Redundant EntryARecipient and address details are pre-filled from a prior step
3.3.8 Accessible Authentication (Minimum)AANo 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

RequirementRatioApplies to
Normal text4.5:1Body text, labels, descriptions
Large text (≥18.66px bold or ≥24px)3:1Headings, large labels
UI components3:1Borders, icons, focus rings, form controls
Non-text contrast3:1Charts, 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:

ItemMeasuredStatus
Resting input border ({gray-200} on white)1.24:1Accepted 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 #3341553.76:1 light, 3.10:1 darkExempt — 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.

Do / Don't
Do
You send
$0.00
CA flagCAD

Visible label above the input stays present when the user types.

Don't
$0.00
US flagUSD

Placeholder-only label disappears on focus, leaving no context.

All text and UI element pairings must meet minimum contrast ratios.

ElementMinimum ratioStandard
Normal text (<18px)4.5:1WCAG 1.4.3 AA
Large text (18px+ bold, 24px+)3:1WCAG 1.4.3 AA
UI components & icons3:1WCAG 1.4.11
Focus indicators3:1WCAG 2.4.7
Aa
Heading on white
17.74:1
PASS
Aa
Body on white
10.31:1
PASS
Aa
White on primary
5.1:1
PASS
Aa
Primary on white
5.1:1
PASS
Aa
White on destructive
5.01:1
PASS
Aa
Muted on white
5.54:1
PASS
Aa
Muted on surface
5.04:1
PASS
Aa
Placeholder text
4.5:1
PASS

Keyboard navigation

All interactive components must be fully operable with keyboard only.

KeyAction
TabMove focus to next interactive element
Shift+TabMove focus to previous element
Enter / SpaceActivate buttons, links, toggles
Arrow keysNavigate within menus, tabs, radio groups
EscapeClose dialogs, tooltips, popovers, dropdowns
Home / EndJump 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-focus and add a 3px halo at 20% opacity with no offset. Borderless controls (buttons, links, tabs, rows) take a 3px solid --ds-focus outline 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-visible outline 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
Do— Visible focus ring

3:1 contrast, 3px ring, visible on the focused element

Don't— No focus indicator

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.

ValueBehaviorWhen to use
tabIndex="0"Follows natural DOM orderCustom interactive elements (div with onClick)
tabIndex="-1"Focusable via JS, skipped by TabOff-screen elements, programmatic focus (modals, error summaries)
tabIndex="1+"Forces priority orderAvoid — breaks natural flow, confuses users
1
Devon
Tab ↓
2
Kim
Tab ↓
3
88 Queens Quay W
Tab ↓
4
Tab NextShift+Tab PreviousEnter ActivateEsc Close

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.

PatternWhen to use
aria-labelIcon-only buttons, unlabeled controls
aria-describedbyError messages, hint text, supplementary info
aria-liveDynamic content updates (toast, loading status)
aria-current="page"Active navigation item, breadcrumb current page
aria-expandedAccordion triggers, dropdown toggles
aria-invalid="true"Form inputs with validation errors
role="alert"Error summaries, critical notifications

Politeness levels

LevelUsed forExample
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 nowTransfer 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
<nav>
<header>
<main>
role="status"

Landmarks let assistive technology users jump between page regions.

Screen reader testing

Test all components with at least two screen readers:

PlatformScreen readerBrowser
macOSVoiceOverSafari
WindowsNVDAFirefox or Chrome
MobileTalkBack (Android)Chrome
MobileVoiceOver (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-hidden if 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
Do— Descriptive labels
Visa ****7788
Expires 11/27
Edit
RBC ****4400
Chequing
Edit
“Visa ending 7788, selected, Expires 11/27”
“Edit, Visa ending 7788, button”
Don't— Missing labels
Visa ****7788
Expires 11/27
Edit
RBC ****4400
Chequing
Edit
“radio button”
“button”

Alt text for images

Do— Describe method + state
USDT ****9c1d
Tether
Edit
BTC ****k8xf
Bitcoin
Edit
alt="USDT wallet ending 9c1d, selected"
Don't— Generic label
USDT ****9c1d
Tether
Edit
BTC ****k8xf
Bitcoin
Edit
alt="icon"
alt="wallet"
Do— Flag + recipient context
South Korea flag
Min-ji Kim
Mar 28 · Sent
-$250.00 CAD
Completed
United Kingdom flag
James Wilson
Mar 25 · Received
+$80.00 CAD
Pending
alt="South Korean flag — Min-ji Kim, sent $250 CAD"
Don't— No flag context
Min-ji Kim
Mar 28 · Sent
-$250.00 CAD
Completed
James Wilson
Mar 25 · Received
+$80.00 CAD
Pending
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
Skip to content
Default — hidden
On Tab — visible
Skip to main content
Do— First focusable element on the page
<body>
  <a href="#main" class="ds-skip-link">
    Skip to main content
  </a>
  <nav>...</nav>
  <main id="main">...</main>
Don't— Skip link buried after navigation
<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-motion is respected and no state is motion-only
  • Semantic HTML used (no div buttons)
  • ARIA attributes correct and minimal; aria-disabled over disabled
  • 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

RoleOwns
DesignContrast, target size, focus order in the layout, error copy that explains the fix
EngineeringSemantic markup, keyboard behavior, ARIA, live regions, focus management
QAScreen 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

ItemStatus
forced-colors / Windows high contrastNot implemented or tested
Resting input borderAccepted deviation with rationale
Automated a11y gate in CINot configured
Per-component accessibility status tableNot published — Carbon-style per-variant reporting is open work
Cognitive accessibility reviewNot formally conducted, despite a multilingual, time-pressured audience