Tips, tricks and tools for building accessible React web apps
- Almero Steyn
- almerosteyn.com
- @kryptos_rsa
- Custom images: Kalina Hristova
"A JavaScript library for building user interfaces." - React docs
"The number of errors found has increased 60% over the last 5 years – from an average of 25 errors in 2011 to 40 errors in 2017." - WebAIM
import React from 'react';
import Content from './Content';
const Root = ({headerText, onLogout}) => (
<main>
<h1>{ headerText }</h1>
<Content onLogout={onLogout}/>
</main>
);
export default Root;
<div className="looks-like-a-button"
onClick={this.onClickHandler}>
Press me please
</div>
import React, { Component } from 'react';
class Demo extends Component {
render() {
return ( <span>{this.props.displayText}</span> );
}
}
export default Demo;
import React, { Component } from 'react';
class Demo extends Component {
render() {
return ( <span>{this.props.displayText}</span> );
}
}
export default Demo;
import React from 'react';
const Demo = ({ displayText }) => (
{displayText}
);
export default Demo;
import React from 'react';
import Demo from './Demo';
const Root = () => (
<Demo displayText="Show this text"/>
);
export default Root;
HTML-like syntactic JavaScript sugar.
<label htmlFor={nameId}>{nameLabelText}</label>
<input id={nameId} type="text" />
Renders to HTML in the DOM.
Bad idea:
<div className="looks-like-button"
onClick={this.onClickHandler}>
Press me
</div>
Good idea:
<button onClick={this.onClickHandler}>
Press
</button>
All ARIA attributes are valid JSX props!
<input id={nameId} aria-label={accessibleLabel} type="text" />
IntelliSense in supported IDEs.
const AppNavigation = () =>
<aside>
<nav>
<ul className="nav">
<li>
<NavLink
to={pathname}
activeClassName="active">
Contact
</NavLink>
</li>
</ul>
</nav>
</aside>;
const AppNavigation = () =>
<aside>
<nav>
<ul className="nav">
<li>
<NavLink
to={pathname}
activeClassName="active">
Contact
</NavLink>
</li>
</ul>
</nav>
</aside>;
const AppNavigation = () =>
<aside>
<nav>
<ul className="nav">
<li>
<NavLink
to={pathname}
activeClassName="active">
Contact
</NavLink>
</li>
</ul>
</nav>
</aside>;
const values = ['a', 'b', 'c'];
const Fragments1 = () =>
values.map((item, index) => <li key={index}>{item}</li>);
const Fragments2 = () => [
<tr key="1"><td>a11y</td></tr>,
<tr key="2"><td>rocks</td></tr>
];
const HeaderWithLevel = ({ headerText, level }) => {
const HeaderLevel = `h${level}`;
return <HeaderLevel>{headerText}</HeaderLevel>;
};
import uuid from 'uuid';
//...
this.inputId = uuid.v4();
//...
render() {
//...
<label htmlFor={this.inputId}>
{labelText}
</label>
<input id={this.inputId}
onChange={this.onChangeHandler}
value={value}
/>
//...
}
github.com/kelektiv/node-uuid
const AppMain = () =>
<Switch>
<Route path="/todos" component={Todos} />
<Route path="/todo" component={Todo} />
<Route path="/contact" component={Contact} />
<Redirect to="/todos" />
</Switch>
;
class PageFocusSection extends Component {
componentDidMount() {
this.header.focus();
}
render() {
const { children, headingText } = this.props;
return (<section>
<h2 tabIndex="-1"
ref={header => {this.header = header;}}>
{headingText}
</h2>
{children}
</section>);
}
}
class WillFocus extends Component {
someHandler() {
//...
this.toFocus.focus();
//..
}
render() {
return <ToFocus ref={toFocus => {this.toFocus = toFocus;}} />;
}
}
const EnhancedComponent = higherOrderComponent(WrappedComponent);
React Router withRouter HOC:
const WrappedInReactRouter = withRouter(WrappedComponent);
React Redux connect HOC:
const WrappedWithReduxConnect =
connect(mapStateToProps)(WrappedComponent);
function Field({ inputRef, ...rest }) {
return <input ref={inputRef} {...rest} />;
}
const EnhancedField = enhance(Field);
<EnhancedField
inputRef={(inputEl) => {this.inputEl = inputEl;}}
/>
this.inputEl.focus();
//...
import DocumentTitle from 'react-document-title';
//...
const SetDocTitle = ({ docTitle, children }) =>
<DocumentTitle title={docTitle}>
{children}
</DocumentTitle>;
github.com/gaearon/react-document-title
const Announcer = ({ message }) =>
<div aria-live="polite"
aria-atomic="true"
aria-relevant="additions">
{message}
</div>;
//...
import { LiveAnnouncer, LiveMessage } from 'react-aria-live';
//...
render() {
return (
<LiveAnnouncer>
<LiveMessage message={this.state.a11yMessage}
aria-live="polite" />
{this.props.children}
</LiveAnnouncer>
);
}
//..
github.com/AlmeroSteyn/react-aria-live
yarn global add create-react-app
create-react-app my-react-app
cd my-react-app
yarn start
"Static AST checker for a11y rules on JSX elements."github.com/evcohen/eslint-plugin-jsx-a11y
More than 30 ESLINT a11y checks.
Rules are configurable.
{
"rules": {
"jsx-a11y/rule-name": "warn"
}
}
Extending config in create-react-app.
{
"extends": ["react-app", "plugin:jsx-a11y/recommended"],
"plugins": ["jsx-a11y"]
}
A11y issues become build warnings.
IDE integration.
"Accessibility auditing for React.js applications."
Setting up react-axe.
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
if (process.env.NODE_ENV !== 'production') {
const axe = require('react-axe');
axe(React, ReactDOM, 1000);
}
ReactDOM.render(<App />, document.getElementById('root'));
Browser console feedback.
Presentation online at:
almerosteyn.com/slides/