Design handoff and usability review
Document the task, semantic structure, focus flow and element contracts, then evaluate whether the resulting experience helps people complete it.
Two complementary questions
Accessibility asks whether people can perceive, understand and operate the experience with their chosen setup. Usability also asks whether they can find the relevant action, understand its consequence and complete the task effectively. A technically named button can still have confusing wording or inappropriate prominence.
Begin with the person’s goal and the actual product outcome. For a source lookup, returning a record, inspecting evidence and recovering from a failed replacement are distinct outcomes. A visually polished result is not proof of qualified review or successful publication.
| Review lens | Evidence | Limitation |
|---|---|---|
| Semantic and technical | Roles, names, relationships, keyboard paths and state handling | Does not prove comprehension |
| Task usability | Observed completion, errors, recovery and participant explanation | Does not replace technical accessibility checks |
| Content authority | Source, review mandate and permitted audience | Does not follow from attractive styling |
| Performance of the task | Time and effort in the agreed context | Speed alone can conceal misunderstanding |
Prepare an accessibility annotation
A handoff should explain behavior that a still image cannot show. Attach annotations to stable region/control identities and include the default plus applicable waiting, failure, success and recovery states. Keep design intent and verified implementation results in separate fields.
| Annotation | Specify | Implementation evidence |
|---|---|---|
| Structure | Heading levels, landmarks and region purpose | Actual DOM hierarchy and named repeated landmarks |
| Flow | Entry, traversal, activation, dismissal and restoration | Native keyboard sequence and focus coordinates |
| Element | Role, visible label, accessible name and value/state | Computed accessibility tree and actual value changes |
| Relationship | Labels, help, errors, grouping and ownership | Resolved IDs and current DOM associations |
| Feedback | Trigger, wording, urgency and whether focus moves | Exact state change and live-region strategy |
| Adaptation | Narrow space, enlarged text, direction and motion | Rendered task with complete recovery |
| Open issue | Impact, owner and acceptance operation | Repair tested in the same task/context |
Structure, hierarchy and grouping
- Make the document hierarchy express the information hierarchy. Use one clear page purpose, descriptive section headings and appropriate landmarks; do not choose heading levels for their default font size.
- Name multiple navigation regions so people can distinguish primary destinations, record ancestry and related links. An adjacent pane does not automatically need another main landmark.
- Use lists for related items, tables for meaningful row/column relationships and fieldsets/legends for related choices. A border alone cannot expose a programmatic group.
- Separate persistent task information from transient feedback. Put the status or error near its affected operation without repeatedly announcing an entire page.
- Use spacing and containment to show relationships. Keep a source qualification with its record when layout changes.
Color, contrast and clustered controls
Annotate role pairs and applicable states, rather than one attractive default swatch. Text, required boundaries, selection and focus need their own checks against the actual composed background. Color cannot be the only distinction between a draft and an approved record.
When controls cluster, inspect the whole grouping: boundary contrast, labels, target separation and focus ring clearance. A dense group with individually adequate colors can still make the active control hard to locate. Use the existing contrast tool for opaque pairs and rendered verification for layered surfaces.
| Situation | Design response | Verify |
|---|---|---|
| Closely spaced actions | Clear names, hierarchy and independent target/focus space | Pointer cancellation, keyboard focus and neighboring targets |
| Selected record | Stable selected/current indication plus its actual detail | Name/state and record identity agree |
| Error | Field-specific text and associated correction | The message is reachable without color |
| Overlay | Intentional layering and suitable foreground/background | Portalled theme and focus visibility |
| Muted help | Readable complete supporting text | Actual contrast, not an assumed disabled-state exception |
Flow, defaults and key traversal
Start from native document and control behavior. Tab traverses focusable controls; composite widgets can have their documented internal key model. A custom shortcut must have a real advantage, clear scope and a way to avoid conflicts with text input, browser commands and assistive technology.
Define the initial entry point, meaningful defaults, submission, interruption and return. A default can reduce effort when it is appropriate and reversible; it cannot stand in for a person’s permission or a qualified review. Keep a disabled or removed focused control from stranding the task.
| Event | Focus/operation contract |
|---|---|
| Page arrival | A meaningful heading and reachable first task control |
| Invalid submission | Named summary or first error; retained values and correction links |
| Waiting | Reachable cancellation where supported; duplicate effects guarded |
| Ordinary completion | Discoverable result without unnecessary focus theft |
| Overlay open/close | Named context, appropriate containment and surviving return target |
| Record/layout change | Retained identity and deliberate recovery if a focused region disappears |
| Shortcut | Document its modifier, scope, conflicts and equivalent visible action |
Element names and contextual labels
Keep names concise and specific to the action. Native roles already communicate control type, so do not normally append “button” or “checkbox” to the name. Help text may be essential without becoming part of the name; use the appropriate description relationship. Inspect the computed tree because DOM attributes alone do not prove the final name.
| Element | Naming contract | Failure |
|---|---|---|
| Text button | Visible action wording normally supplies its name | Replacing Save draft with unrelated aria-label text |
| Icon action | Name the operation or destination | Announcing only a glyph name or “button button” |
| Repeated action | Include enough record context to distinguish it | Several indistinguishable Open buttons |
| Input | Persistent label; separate associated help and error | Placeholder used as the only name |
| Meaningful image | Alternative for its contextual purpose | Filename or guessed source identification |
| Decorative companion | Remove redundant semantics when adjacent text carries meaning | Repeating the action name from both icon and button |
| State/value | Expose current checked, selected, expanded or numeric value | Writing the state into a label while the actual property disagrees |
Worked request handoff
This existing compiled English fixture gives the handoff a real implementation. Submit empty fields, follow the error links, enter values and exercise the requested local outcome. Compare the actual error, waiting, cancellation and completion behavior with the contracts below. It does not submit to a remote service.
| Region/control | Default contract | Changed state |
|---|---|---|
| Task form | Persistent labels and explanatory text | Values retained through validation/recovery |
| Submit action | Named native action | Duplicate submission guarded while the request is pending |
| Error summary | Absent until explicit invalid submission | Receives focus and links to the invalid fields |
| Help/error text | Associated with its input | Correction remains available while needed |
| Local request | Outcome is explicitly a fixture | Cancellation/failure/completion do not imply a remote write |
| Result feedback | Persistent polite status strategy | Returns to an enabled action without an automatic expiry |
This local request never contacts a service. Choose a simulated outcome; there is no countdown.
Copyable handoff template
Fill the placeholders with actual source/control IDs, behavior and evidence. The pending fields are intentional: copying a template is not an executed review.
{
"status": "unexecuted design handoff",
"task": "Complete the agreed task",
"build": "record exact build/route",
"structure": [
{
"id": "replace-with-region-id",
"purpose": "task region",
"semantics": "choose native heading/landmark/group"
}
],
"elements": [
{
"id": "replace-with-control-id",
"role": "native control role",
"visibleLabel": "actual wording",
"accessibleName": "expected computed name",
"descriptionIds": [],
"applicableStates": [
"default",
"waiting",
"error",
"recovery"
],
"focusOnEntry": "define",
"focusOnExit": "define",
"equivalentAction": "define when a gesture/shortcut exists"
}
],
"observations": [],
"verification": {
"keyboard": "pending",
"accessibilityTree": "pending",
"visualAdaptation": "pending",
"participantTaskReview": "not conducted"
},
"issues": []
}Usability tactics with a purpose
Choose a tactic because it supports the agreed task, not because every expressive technique must appear on one screen. Keep scale, placement, color roles and grouping consistent through the task’s states. In this library, the committed Earth palette and tokens remain the baseline; a usability chapter is not an instruction to rebrand the product.
| Tactic | Task purpose | Failure to avoid |
|---|---|---|
| Color | Maintain role consistency and guide attention | A new palette is treated as proof of improved usability |
| Containment | Group related controls and outcomes | Everything becomes nested cards |
| Motion | Explain a transition or continuity | Movement competes with reading or bypasses reduced motion |
| Shape/morphing | Reinforce a meaningful action/state relationship | An unfamiliar shape conceals the action’s identity |
| Size | Emphasize the main action while preserving targets | All controls scale indiscriminately |
| Typography | Differentiate task heading, content and help | A decorative type treatment reduces legibility |
Evaluate and iterate
Use a concrete task with a known starting state and an observable outcome. Ask people to complete it with their preferred interaction setup; record observed action and explanation separately from a designer’s interpretation. Include discovery, error recovery and delayed/offline behavior when those belong to the task.
Compare candidate changes against the same task and constraints. Define what would count as an improvement before seeing results: fewer wrong actions, clearer consequence understanding, successful recovery or reduced unnecessary effort. A shorter completion time with more misunderstanding is not automatically better.
- Agree participation, retention and quotation choices; use the Inclusive Participation process.
- Capture enough context to reproduce a barrier without collecting unnecessary identifying data.
- Record unresolved effects and a repair owner. Retest the actual repaired task rather than accepting a new screenshot.
- Use multiple kinds of evidence. Automated checks, technical walkthroughs and participant observations answer different questions.
- A lab or unexecuted plan is not a human study. Publish only the scope actually conducted.
Implementation and verification
- Verify native roles, names, state and description relationships in the rendered task.
- Follow Tab and reverse Tab through validation, pending, cancellation and recovery; inspect unobscured focus.
- Check both themes, long labels, 320px reflow, text enlargement, spacing overrides and relevant reduced-motion/forced-color behavior.
- Compare the actual task result with every message and handoff annotation.
- Use a named browser/assistive-technology pair before claiming spoken behavior.
- Record the exact subject/state coverage and remaining findings; do not label a generic green scan as a whole-product usability result.
'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>;
}