Accessibility
Requirements, implementation choices and verification for complete web tasks. Component names and automated passes do not establish whole-product conformance.
Scope and responsibilities
Use WCAG 2.2 AA as the web acceptance target and the linked W3C guidance for the full criteria, definitions and exceptions. This page is implementation guidance for this library, not a conformance report. Material’s accessibility guidance is a coverage reference; this package renders web DOM rather than native Android or iOS widgets.
The library supplies styling and primitive behavior. Authors supply names, meaningful content, source context, relationships and complete flows. Test the final composition with actual browser and assistive technology combinations. A DOM assertion checks structure; it does not prove what a screen reader speaks.
| Owner | Responsibility | Evidence |
|---|---|---|
| Library | Usable semantics, forwarding, focus states and documented behavior. | Source/API checks plus real interactions. |
| Application | Labels, meaningful order, validation, requests, route changes and recovery. | Complete user task and persisted outcome where relevant. |
| Content | Alternatives, language, captions/transcripts and understandable messages. | Content review with appropriate authority. |
| Verification | Automated rules, manual keyboard, zoom, forced colors and assistive technology. | Named setup, operations, results and unresolved findings. |
Adaptation, early learning and standards
Treat different access needs as ordinary design inputs. Support individual preferences without assuming one permanent default fits someone throughout their life or even throughout a day. Keep system/browser settings useful and offer application choices only when their behavior and persistence are clear.
Learn while defining the task and interaction model. Work with people whose setups expose barriers before the screen is treated as finished; investigate unanticipated outcomes and close the loop on repairs. Inclusive Participation supplies concrete collaboration roles and a bounded plan.
WCAG requirements establish a baseline, not a ceiling on usability or evidence of a completed audit. Record which user need a requirement serves, then examine the whole task and its recovery path. Do not treat primitive-library adoption, an automated score or one successful participant as proof for everyone.
Assistive technology and interaction ownership
People can combine tools and change input methods. Do not select a capability or infer a disability from screen size. The library exposes web semantics and controls, while browser/OS and assistive technology manage speech, braille, scanning or tracking. A larger visual icon does not by itself enlarge its interactive target.
| Setup | What the interface must preserve | Task evidence |
|---|---|---|
| Keyboard, directional controller or trackball | Logical focus, native activation and the documented composite-widget key model. | Every required action is reachable; no positive tabindex or arbitrary global key interception. |
| Screen reader with speech or braille output | Names, roles, relationships, text and meaningful state in the accessibility tree. | Record actual reader/browser/output setup, navigation and announcements; DOM assertions alone are insufficient. |
| Switch scanning | Reachable actions, adequate time and a dependable route through the task. | Observe the actual scan/selection setup; a keyboard test is useful but not a switch-device result. |
| Magnification or tracking input | Usable targets, visible focus/context and controls that remain reachable after reflow. | Complete narrow, enlarged and moving-focus tasks without overlapping targets. |
| Voice input | Stable visible labels included in accessible names. | Exercise actual named commands; a named DOM control is not proof of speech recognition. |
Accessible text, images and nearby context
Keep essential instructions as real text where possible. Adjacent explanation and visible captions give context to all readers; image alternatives supply the meaning or action appropriate to the image’s use. Decorative or redundant images can have an empty alternative rather than repeating the same caption.
Do not assume a reader will recover text baked into an image. Provide its essential information in accessible text and use a structured detailed equivalent for charts or other complex information. The Alternative Text guide has original fixtures and purpose-specific examples.
The captured Material accessibility-writing page suggests a 125-character alternative-text length. That is not an HTML attribute limit or a universal rule for conveying meaning. The WAI image decision tree instead distinguishes function, context, redundancy and complexity. Keep short alternatives purposeful and put detailed information in navigable text; do not silently cut an alternative or source qualification to meet a counter.
- Check essential text and meaningful graphics against their actual backgrounds in every applicable state.
- Decorative detail can be treated differently from information needed to understand or operate the task.
- A caption is visible context; it is not automatically an adequate alternative for every image purpose.
- An accessibility name must explain the actual control, rather than exposing a raw icon-font ligature or a filename.
Truncation is a disclosure decision
Prefer wrapping and flexible component dimensions for critical information. If a compact view uses an ellipsis, preserve the full value and provide a discoverable way to read it. A hover title alone is a weak route for touch, keyboard, magnification and assistive-technology users.
A link can lead to the full record; a disclosure can reveal the complete label in place. Keep that route available without a precision gesture and preserve focus when it expands or collapses. Do not clip recovery instructions, permission consequences or source/review qualifiers merely to keep a row short.
Truncation changes presentation, not the stored source string. Check both the visual rendering and the accessible content. A DOM text assertion does not prove what every screen reader speaks, and a tooltip is not a general waiver for missing content.
Working text visibility and enlargement
Original English interface fixture. Change its presentation without deleting the retained text.
Local field notes for the reading and source comparison session
Rendered content width: 0 CSS px. Text: Not measured. The current view does not clip text.
The complete string stays in this paragraph’s DOM text; CSS ellipsis changes its visual presentation. The button exposes the same text in a wrapping view for pointer, touch and keyboard users. Actual speech, braille and voice-control behavior still need a named browser/assistive-technology check.
Text size affects this specimen, not a saved application preference. Root font changes and browser zoom are different mechanisms. The width is bounded by its actual container; wrapping grows vertically.
'use client';
import {useEffect, useId, useRef, useState} from 'react';
import './text-visibility.css';
export function TextVisibilityExample(){
const id=useId();
const [text,setText]=useState('Local field notes for the reading and source comparison session');
const [mode,setMode]=useState('wrap');
const [width,setWidth]=useState('240');
const [scale,setScale]=useState('100');
const [expanded,setExpanded]=useState(false);
const [measurement,setMeasurement]=useState({width:0,font:'Not measured',clipped:false});
const preview=useRef<HTMLParagraphElement>(null);
useEffect(()=>{
const node=preview.current;if(!node)return;
const measure=()=>{
const next={width:node.clientWidth,font:getComputedStyle(node).fontSize,clipped:mode==='ellipsis'&&!expanded&&node.scrollWidth>node.clientWidth};
setMeasurement(previous=>JSON.stringify(previous)===JSON.stringify(next)?previous:next);
};
measure();const resize=new ResizeObserver(measure);resize.observe(node);
const root=new MutationObserver(measure);root.observe(document.documentElement,{attributes:true,attributeFilter:['style','class']});
return()=>{resize.disconnect();root.disconnect();};
},[text,mode,width,scale,expanded]);
return <div data-text-visibility-lab className="text-visibility-lab">
<p>Original English interface fixture. Change its presentation without deleting the retained text.</p>
<label htmlFor={`${id}-text`}>Preview text</label>
<textarea id={`${id}-text`} data-text-content dir="auto" className="mt-textarea mt-textarea-resize-vertical" rows={3} value={text} onChange={event=>setText(event.target.value)} />
<div className="text-visibility-controls">
<label>Presentation<select data-text-mode className="mt-input" value={mode} onChange={event=>{setMode(event.target.value);setExpanded(false);}}><option value="wrap">Wrap all text</option><option value="ellipsis">Ellipsis</option></select></label>
<label>Maximum preview width<select data-text-width className="mt-input" value={width} onChange={event=>setWidth(event.target.value)}>{[160,240,360,480].map(value=><option key={value} value={value}>{value} CSS px</option>)}</select></label>
<label>Specimen text size<select data-text-scale className="mt-input" value={scale} onChange={event=>setScale(event.target.value)}>{[100,150,200].map(value=><option key={value} value={value}>{value}%</option>)}</select></label>
</div>
{mode==='ellipsis'&&<button type="button" data-text-expand className="mt-btn mt-btn-outline mt-btn-md" aria-controls={`${id}-preview`} aria-expanded={expanded} onClick={()=>setExpanded(value=>!value)}>{expanded?'Collapse full text':'Show full text'}</button>}
<p id={`${id}-preview`} ref={preview} data-text-preview dir="auto" data-clipped={mode==='ellipsis'&&!expanded?true:undefined} style={{inlineSize:`min(100%, ${width}px)`,fontSize:`${scale}%`}}>{text||'No preview text entered.'}</p>
<p data-text-measurement>Rendered content width: {measurement.width} CSS px. Text: {measurement.font}. {measurement.clipped?'The one-line view clips visually; use Show full text.':'The current view does not clip text.'}</p>
<p>The complete string stays in this paragraph’s DOM text; CSS ellipsis changes its visual presentation. The button exposes the same text in a wrapping view for pointer, touch and keyboard users. Actual speech, braille and voice-control behavior still need a named browser/assistive-technology check.</p>
<p>Text size affects this specimen, not a saved application preference. Root font changes and browser zoom are different mechanisms. The width is bounded by its actual container; wrapping grows vertically.</p>
</div>;
}
Compiled text visibility styles
.text-visibility-lab { display:grid; gap:.75rem; min-inline-size:0; }
.text-visibility-controls { display:grid; gap:1rem; grid-template-columns:repeat(auto-fit,minmax(min(100%,16rem),1fr)); }
.text-visibility-lab label { min-inline-size:0; overflow-wrap:anywhere; }
.text-visibility-lab .mt-input { display:block; inline-size:100%; margin-block-start:.5rem; }
.text-visibility-lab textarea { inline-size:100%; block-size:auto; min-block-size:6rem; }
.text-visibility-lab p { overflow-wrap:anywhere; }
.text-visibility-lab [data-text-preview] { box-sizing:content-box; max-inline-size:100%; margin:0; border-block:1px solid var(--color-muted-foreground); padding-block:12px; background:var(--color-card); color:var(--color-card-foreground); white-space:pre-wrap; overflow-wrap:anywhere; line-height:1.5; }
.text-visibility-lab [data-text-preview][data-clipped] { white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.text-visibility-lab [data-text-expand] { justify-self:start; min-inline-size:0; max-inline-size:100%; min-block-size:44px; block-size:auto; padding:8px 12px; white-space:normal; overflow-wrap:anywhere; }
Text enlargement, units and constrained layouts
Support text enlargement through the user agent without loss of content or function. The web acceptance target includes 200% enlargement; also inspect intermediate settings. Grow containers, stack long actions and allow ordinary vertical reading flow before requiring two-axis navigation.
Material’s native example scales text and line height while retaining 1x padding and gaps, and leaves non-text components unchanged. This rem-based web system behaves differently when the root font changes or browser zoom scales the whole presentation. The local text-size selector demonstrates paragraph text scaling with fixed 12 CSS px block padding; it does not implement an OS preference or global type setting.
For constrained bars or navigation, first keep essential labels available through reflow or another explicit, operable route. The native reference’s touch-and-hold enlarged tooltip pattern is not a reason to disable web text enlargement or hide the only full label behind long press. Our wrapping header and named controls remain subject to actual viewport/focus checks.
- Test a long spaced string, an unbroken identifier, multiline text, empty content and the actual scripts the product uses.
- Check 100%, 150% and 200% specimen text, root text changes and browser zoom as separate mechanisms.
- Apply user line/letter/word/paragraph spacing and verify retained content, action names and focus.
- At a narrow width, confirm the page does not overflow and the disclosure exposes the exact retained text.
- Retest native control and tooltip behavior with the intended browser/device; desktop emulation is bounded evidence.
Structure, names and relationships
- Use one main landmark, meaningful headings and navigation names. A skip link must move focus to content, not merely scroll it.
- Name controls by purpose. Visible text should occur in the accessible name so voice input can find the action.
- Use label/htmlFor or a primitive’s documented label relationship. A placeholder disappears while typing and cannot replace a label.
- Connect help and error text with aria-describedby. Expose invalid state only when an error exists; preserve the entered value.
- Hide decorative SVGs from accessibility APIs. Supply useful alternatives for meaningful images; describe complex diagrams in surrounding text.
- Set page and content language from actual metadata. Do not label English examples as an Indigenous language or invent language text.
Keyboard and focus contracts
Focus must remain visible and unobscured by sticky headers, floating actions or overlays. This styleguide reserves mobile header clearance, but each product needs its own obstruction check. Keep a clear focus indicator in forced colors as well as ordinary themes.
| Context | Expected operation | Author checks |
|---|---|---|
| Document navigation | Tab/Shift+Tab follow meaningful DOM order; links activate with Enter. | No positive tabindex or visual ordering that contradicts reading order. |
| Action | Native buttons activate with Enter/Space. | One interactive element, explicit form type and a meaningful name. |
| Composite widget | Its documented arrow, Home/End and selection model. | Do not add global key handlers that fight the installed primitive. |
| Modal | Focus enters, remains in the open modal and returns appropriately on dismissal. | Name the dialog, test Escape and provide a reachable close/cancel action. |
| Validation | Focus the first error or a named summary after explicit submission. | Do not move focus on every keystroke; error links reach the correct fields. |
| Asynchronous completion | Status is available without unnecessary focus movement. | If the active control disappears or becomes disabled, move focus to a logical enabled destination. |
Visual requirements and adaptations
The contrast tool checks opaque color pairs. Real transparency, gradients and images require measuring the actual composed background. Disabled states have criterion-specific exceptions; low contrast still affects comprehension and should be reviewed. Passing twelve semantic pairs does not cover every component state.
| Check | Web target | Verification context |
|---|---|---|
| Text contrast | 4.5:1 normal text; 3:1 large text under WCAG’s definition. | Measure actual foreground and background, including hover and overlays. |
| Non-text contrast | 3:1 for required visual information identifying controls and states. | Inspect boundaries, meaningful icons and state indicators in context. |
| Color independence | Status remains understandable without color. | Use explicit text and meaningful symbols; inspect grayscale as a review aid. |
| Text enlargement | Usable at 200% text size. | No clipped labels, lost controls or fixed-height text containers. |
| Reflow | Content at 320 CSS px without two-dimensional scrolling except qualifying content. | Check reading and operation; tables may use a named horizontal region where necessary. |
| Text spacing | User spacing overrides do not lose content or operation. | Test line 1.5, paragraph 2, letter .12 and word .16 times font size. |
| Focus visibility | Visible and not entirely hidden by author-created content. | Keyboard through headers, dialogs and floating controls. |
Pointer, touch and alternate input
Prefer at least 44 × 44 CSS px for frequent standalone action targets as a product design target. WCAG 2.2 AA’s pointer-target criterion uses 24 × 24 CSS px with defined spacing and other exceptions; 44 is not its universal AA minimum. Native platform units and browser CSS pixels are distinct.
Provide a single-pointer alternative to dragging and keyboard access to the same result. A tooltip cannot be the only way to learn essential information. Hover/focus content needs deliberate dismissal and persistence behavior; verify touch and keyboard paths separately.
- Do not commit destructive actions on pointer-down before the user can cancel the gesture.
- Do not require multi-finger gestures, device movement or a long press as the sole path.
- A small visible icon can sit inside a large target. Inspect the actual hit region and adjacent targets.
- Keep labels and actions usable with long interface translations and right-to-left layout.
Validation, status and recovery in practice
Try submitting the empty form. Its named error summary receives focus, and each error link focuses the affected field. Enter your own text and submit again; input becomes read-only while remaining reachable. The submit action stays focusable with aria-disabled and an explicit duplicate-submission guard.
A persistent polite status region reports the local outcome. Completion, failure and cancellation retain values and return focus to the enabled submit action. There is no automatic expiry. This demonstrates DOM relationships and focus behavior; the spoken result still requires assistive-technology testing.
This local request never contacts a service. Choose a simulated outcome; there is no countdown.
'use client';
import { useEffect, useId, useRef, useState } from 'react';
import { Button, Input } from '@mobtranslate/ui';
const actionStyle = {minHeight:44,height:"auto",minWidth:0,maxWidth:"100%",whiteSpace:"normal",overflowWrap:"anywhere",paddingInline:".5rem"} as const;
export function AccessibleRequestExample() {
const id = useId();
const [title, setTitle] = useState('');
const [note, setNote] = useState('');
const [errors, setErrors] = useState<string[]>([]);
const [phase, setPhase] = useState<'editing' | 'pending' | 'failed' | 'complete'>('editing');
const [announcement, setAnnouncement] = useState('');
const [invalidAttempt, setInvalidAttempt] = useState(0);
const summary = useRef<HTMLDivElement>(null);
const submit = useRef<HTMLElement>(null);
useEffect(() => { if (invalidAttempt) summary.current?.focus(); }, [invalidAttempt]);
const pending = phase === 'pending';
function edit() { setPhase('editing'); setAnnouncement(''); }
function finish(next: 'editing' | 'failed' | 'complete', message: string) {
setPhase(next);
setAnnouncement(message);
submit.current?.focus();
}
return <div data-accessibility-lab className="space-y-4">
<p>This local request never contacts a service. Choose a simulated outcome; there is no countdown.</p>
<form noValidate className="space-y-3" onSubmit={event => {
event.preventDefault();
if (pending) return;
const invalid = [!title.trim() && 'title', !note.trim() && 'note'].filter((field): field is string => Boolean(field));
setErrors(invalid);
if (invalid.length) {
setAnnouncement('');
setPhase('editing');
setInvalidAttempt(attempt => attempt + 1);
return;
}
setPhase('pending');
setAnnouncement('Local request pending. Choose an outcome or cancel below.');
}}>
{errors.length > 0 && <div ref={summary} tabIndex={-1} aria-labelledby={`${id}-summary`} className="border-2 border-[var(--color-destructive)] rounded-lg p-4">
<h3 id={`${id}-summary`} className="font-bold">Check {errors.length} {errors.length === 1 ? 'field' : 'fields'}</h3>
<ul className="guidance-list">{errors.map(field => <li key={field}><a href={`#${id}-${field}`} onClick={event => {
event.preventDefault();
document.getElementById(`${id}-${field}`)?.focus();
}}>{field === 'title' ? 'Title: enter a title.' : 'Note: enter a note.'}</a></li>)}</ul>
</div>}
<label htmlFor={`${id}-title`} className="block font-medium">Title (required)</label>
<Input id={`${id}-title`} name="title" required readOnly={pending} value={title} state={errors.includes('title') ? 'error' : 'default'}
aria-describedby={`${id}-help${errors.includes('title') ? ` ${id}-title-error` : ''}`} onChange={event => {
setTitle(event.target.value); setErrors(current => current.filter(field => field !== 'title')); edit();
}} />
<p id={`${id}-help`} className="text-sm">Use your own English text to try the interaction.</p>
{errors.includes('title') && <p id={`${id}-title-error`}>Enter a title.</p>}
<label htmlFor={`${id}-note`} className="block font-medium">Note (required)</label>
<textarea id={`${id}-note`} name="note" className="mt-textarea w-full" required readOnly={pending} value={note}
aria-invalid={errors.includes('note') || undefined} aria-describedby={errors.includes('note') ? `${id}-note-error` : undefined}
onChange={event => { setNote(event.target.value); setErrors(current => current.filter(field => field !== 'note')); edit(); }} />
{errors.includes('note') && <p id={`${id}-note-error`}>Enter a note.</p>}
<Button style={actionStyle} ref={submit} type="submit" aria-disabled={pending || undefined} aria-busy={pending || undefined}>Try accessible request</Button>
</form>
<div className="flex flex-wrap gap-3">
<Button style={actionStyle} type="button" variant="secondary" disabled={!pending} onClick={() => finish('complete', 'Local example complete. Your text is retained; nothing was sent.')}>Simulate completion</Button>
<Button style={actionStyle} type="button" variant="outline" disabled={!pending} onClick={() => finish('failed', 'Simulated connection failure. Your text is retained. Try accessible request again to retry.')}>Simulate failure</Button>
<Button style={actionStyle} type="button" variant="ghost" disabled={!pending} onClick={() => finish('editing', 'Local request cancelled. Your text is retained.')}>Cancel local request</Button>
</div>
<p role="status" aria-atomic="true" className="min-h-6">{announcement}</p>
</div>;
}
Announcements and timing
Use a persistent role=status for ordinary changes that should be discoverable without taking focus. Reserve urgent alerts for events requiring immediate attention. Repeatedly announcing every keystroke can obscure the task. An aria-live attribute on an otherwise hidden message is not proof it is announced.
A focused error summary and an assertive alert can duplicate the same message. Choose an intentional strategy and test it. Avoid announcing an entire changing results table when a concise count is enough.
- Expose meaningful progress where available; do not fabricate percentages.
- Keep important errors and recovery actions available beyond a short toast timeout.
- If a real task has a time limit, support extension or the applicable exception and explain it before loss.
- Respect reduced motion and offer pause/stop for relevant ongoing movement. Global CSS does not automatically stop JavaScript animations.
- Avoid automatic focus changes when a background request finishes unless focus would otherwise be lost.
Audio, language and authentication
- Provide a text alternative for audio-only information, captions for applicable video and any required description. Identify the source and whether a voice is human or synthetic.
- Do not auto-play audio unexpectedly. Make play/pause and level controls operable, and preserve the user’s choice.
- Keep source language, interface language and pronunciation metadata distinct. A locale fallback must not fabricate a translation.
- Allow password-manager entry and paste. Do not require memory puzzles as the sole authentication method.
- Avoid collecting the same information again when it can be safely supplied from the same process; clearly distinguish permission choices that truly require reconfirmation.
- Accessibility does not waive content authority, current consent or purpose restrictions.
Verification procedure
| Pass | Operations | Record |
|---|---|---|
| Automated | Lint/types/build, route/link checks, accessible-name/relationship rules and actual interactions. | Versions, pages, states, failures and scope; never label this a full audit alone. |
| Keyboard | Use Tab, reverse Tab, activation and widget-specific keys through a complete task. | Focus location, visibility, order, trap/return and recovery. |
| Screen reader | Use a named browser/reader pair to navigate landmarks/headings, controls, errors and outcomes. | Actual names, states, relationships and announcements. |
| Visual adaptation | Both themes, 200% text, 320 CSS px reflow, spacing overrides and forced colors. | Clipping, overlap, required contrasts and target usability. |
| Motion and pointer | Reduced motion, interrupted transitions, cancellation and alternate drag paths. | The same task remains possible without the original gesture. |
| Content and data | Check alternatives, language, source, consent and actual persistence. | Do not substitute simulated results for product writes. |
Failure cases and release boundary
Before a release, record the tested task scope and unresolved findings. The current browser suite verifies named local flows and some shell/foundation behavior. Initial-page automated sweeps have separate recorded evidence; mounted interactive states and manual assistive-technology coverage still need their own scope. This page does not claim whole-product conformance.
- An icon-only control without a name, or aria-label that replaces the visible action with unrelated wording.
- A div pretending to be a button without native keyboard and focus behavior.
- A colored border as the only error indication, or a success toast before a save is verified.
- A portalled overlay that loses its label, theme, focus return or meaningful reading order.
- Disabled focused controls that strand keyboard users while a request is pending.
- A passing automated scan presented as proof of screen-reader operation or entire WCAG conformance.