Let's make an accessible component
-
git clone https://github.com/ericmasiello/accessibility-demo.git
-
cd accessibility-demo
-
npm install
This is how you're doing it wrong.
(Demo)
Tabbed interface
- Visual presentation
- Visual behavior
- Keyboard behavior
Tabs are not a native web paradigm. They come from desktop interfaces.
Goals
- Retain the expected visual design
- Operable via non-pointer/touch input
- Operable and understandable by screen reader
The Plan
- Review existing code
- Implement basic keyboard support
- Implement expected tab interface behaviors
- Implement easy HTML wins
- Improve screen reader support
Starter HTML (JSX)
<div className="tablist">
<div className="tab">Tab name</div>
...
</div>
- Interactive elements should be keyboard focusable
- How can we make the "tab" keyboard focusable?
That was easy
<div className="tablist">
<div className="tab">Tab name</div>
<button className="tab">Tab name</button>
...
</div>
git checkout 02-basic-keyboard-focus
Don’t forget focus states
- Users who rely on non pointer/touch input (e.g. keyboard) rely on focus states to know where they are in the interface.
- Like many AX considerations, these affordances often benefit the non-disabled
button:focus,
a:focus {
/* Don't do this without adding back a focus state! */
outline: none;
}
How should a tabbed interface work?
https://www.w3.org/TR/wai-aria-practices/#tabpanel
- Left and Right arrow keys move between tabs
- Tab key and down arrow key should move focus from selected tab to tab panel (content)
- Shift + Tab move focus from tab panel back to selected tab
problems to solve
- Take non-selected tabs out of the normal tab flow
- Make the tab panel focusable
- Pressing the left or right arrow key will select the adjacent tab and set focus on it
House keeping: <TabManager />
- Move selected state management up from <Tabs /> to <TabManager />
- Maintain ref for selected tab
- Maintain ref for tab panel
- onKeyDown methods for left, right, and down arrow keys
- Expose data (state), refs, and keyboard event handlers via render props
git checkout 03-proper-tab-keyboard-controls
Tab focus and behavior
{diamonddogs.map(member => ( <button tabIndex={tabState.selected !== member.id ? -1 : undefined} ref={tabState.selected === member.id && tabState.selectedTabRef} onKeyDown={tabState.handleTabKeyDown} onClick={() => { tabState.setSelected(member.id); }} className={classNames(['tab', {'tab--selected': tabState.selected === member.id }])} > {member.name} </button> ))}
Manage Tab Panel Focus
<section ref={tabState.panelRef} tabIndex={0}>
...
</section>
Keyboard Events
handleKeyDown = event => { switch (event.which) { case KEYS.right: { this.setState({ selected: findNext( this.props.tabIds, this.state.selected ) }, () => { this.state.selectedTabRef.current.focus(); }); break; } case KEYS.left: { // ...
Keyboard Events
handleKeyDown = event => { switch (event.which) { // ... case KEYS.down: { this.state.panelRef.current.focus(); break;
// ...
What about screen readers?
MacOS voiceover
- Cmd + Fn + F5: Turn on/off VO
- Ctrl + Option + A: Start reading
- Ctrl: Stop reading
- Ctrl + Option + Right: Read next
- Ctrl + Option + Right: Read previous
Back to Basic HTML
-
<title>Diamond Dog Team Page</title>
-
<h1 className="page-title">Diamond Dogs</h1>
-
<img
className="bio-photo"
src={selectedMember.photo}
alt={selectedMember.photoDesc}
/>
git checkout 04-additional-html-semantics
marking up the bio
<dl className="bio"> <dt className="bio__label">Hometown</dt> <dd className="bio__item">{selectedMember.hometown}</dd> <dt className="bio__label">Time at Vistaprint</dt> <dd className="bio__item">{selectedMember.employment}</dd> <dt style={{ display: "none" }}>Biography</dt> <dd className="bio__item">{selectedMember.bio}</dd> </dl>
Visually hidden / Announced by Screen readers
<dt style={{ display: "none" }}>Biography</dt>
display: none; is not announced by screen reader
Visually hidden / Announced by Screen readers
<dt className="visually-hidden">Biography</dt>
.visually-hidden {
border: 0;
clip: rect(0 0 0 0);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
white-space: nowrap;
}
Still not enough
HTML lacks all the necessary elements to accurately describe our app to assistive technology
ARIA
Accessible Rich Internet Applications: supplements HTML with additional roles, attributes, and states to describe UI widgets
https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA
Tab List
<div role="tablist" className="tablist" aria-label="Team members">
git checkout 05-aria
Tab
<button
role="tab"
aria-selected={tabState.selected === member.id}
aria-controls={tabState.selected === member.id
? 'tab-panel'
: undefined
}
...
Tab Panel
<section
ref={tabState.panelRef}
id="tab-panel"
tabIndex={0}
role="tabpanel"
aria-label={selectedMember.name}
>
Accessibility helps everyone
There’s really no right or wrong in inclusive design. It’s just about trying your hardest to provide a valuable experience to as many people as you can... no matter how they are operating or reading your interface...
Did we do it?
Other considerations:
- Cognitive disabilities
- Visual impairments (e.g. low vision, color blindness)
References
Let's make an accessible component
By Eric Masiello
Let's make an accessible component
- 588