Create a List component with keyboard navigation in React
Iva Kop
· 4 min read
Lists are ubiquitous in modern web apps. And keyboard accessibility is becoming increasingly important.
So let's build a keyboard-friendly list in React!
The List component
First, we should create the list component itself. In its most simplified form, it might look something like this:
const list = ['🍎 apple', '🍊 orange', '🍍 pineapple', '🍌 banana'];
const List = () => {
return (
<div>
{list.map((item) => (
<div key={item}>{item}</div>
))}
</div>
);
};
useKeyPress custom hook
Next, we need to know when a user pressed ArrowUp
or ArrowDown
keys so we can implement the arrow key navigation logic based on these events. Let's create a custom hook to detect a key press.
const useKeyPress = (targetKey) => {
const [keyPressed, setKeyPressed] = useState(false);
useEffect(() => {
const downHandler = ({ key }) => {
if (key === targetKey) {
setKeyPressed(true);
}
};
const upHandler = ({ key }) => {
if (key === targetKey) {
setKeyPressed(false);
}
};
window.addEventListener('keydown', downHandler);
window.addEventListener('keyup', upHandler);
return () => {
window.removeEventListener('keydown', downHandler);
window.removeEventListener('keyup', upHandler);
};
}, [targetKey]);
return keyPressed;
};
The useKeyPress custom hook heavily inspired by useHooks but slightly modified so that we comply with the rules of hooks as defined in the React docs and more specifically - exhaustive-deps
.
Detect a key press with useEffect
Next step is to detect when the user pressed arrowUp
or arrowDown
in our list component. To do this, we will need our new useKeyPress
hook and useEffect
.
const List = () => {
const arrowUpPressed = useKeyPress('ArrowUp');
const arrowDownPressed = useKeyPress('ArrowDown');
useEffect(() => {
if (arrowUpPressed) {
console.log('arrowUpPressed');
}
}, [arrowUpPressed]);
useEffect(() => {
if (arrowDownPressed) {
console.log('arrowDownPressed');
}
}, [arrowDownPressed]);
return (
<div>
{list.map((item, i) => (
<div key={item}>{item}</div>
))}
</div>
);
};
Perfect! We are now able to detect the relevant key press events in our component.
Arrow key navigation logic with useReducer
Next, we need to implement the arrow key navigation logic of the component with useReducer
.
const initialState = { selectedIndex: 0 };
const reducer = (state, action) => {
switch (action.type) {
case 'arrowUp':
return {
selectedIndex:
state.selectedIndex !== 0 ? state.selectedIndex - 1 : list.length - 1,
};
case 'arrowDown':
return {
selectedIndex:
state.selectedIndex !== list.length - 1 ? state.selectedIndex + 1 : 0,
};
case 'select':
return { selectedIndex: action.payload };
default:
throw new Error();
}
};
const List = () => {
const arrowUpPressed = useKeyPress('ArrowUp');
const arrowDownPressed = useKeyPress('ArrowDown');
const [state, dispatch] = useReducer(reducer, initialState);
useEffect(() => {
if (arrowUpPressed) {
dispatch({ type: 'arrowUp' });
}
}, [arrowUpPressed]);
useEffect(() => {
if (arrowDownPressed) {
dispatch({ type: 'arrowDown' });
}
}, [arrowDownPressed]);
return (
<div>
{list.map((item, i) => (
<div
key={item}
onClick={() => {
dispatch({ type: 'select', payload: i });
}}
style={{
cursor: 'pointer',
color: i === state.selectedIndex ? 'red' : 'black',
}}
>
{item}
</div>
))}
</div>
);
};
export default List;
Let's break it down!
We've created a reducer with three possible actions - arrowUp
, arrowDown
and select
. The first two actions are dispatched their respective useEffect
hooks if the relevant key is pressed. The select
action is dispatched if a user clicks on an item with the mouse.
Enable tab navigation & accessibility
Finally, to make sure our component is fully accessible and usable for those who use the tab
key to navigate web pages, let's add tabIndex
, role
, aria-pressed
and onKeyPress
handler to our list item:
<div
key={item}
onClick={() => {
dispatch({ type: 'select', payload: i });
}}
style={{
cursor: 'pointer',
color: i === state.selectedIndex ? 'red' : 'black',
}}
role="button"
aria-pressed={i === state.selectedIndex}
tabIndex={0}
onKeyPress={(e) => {
if (e.key === 'Enter') {
dispatch({ type: 'select', payload: i });
e.target.blur();
}
}}
>
{item}
</div>
That's it! We now have a fully accessible and keyboard-friendly React list. 🚀
Please keep in mind this is a simplified example. The goal is to shine a light on the main steps in the implementation rather than to provide production-ready code!
Curious to play with the code yourself? Here is a working CodeSandbox example.
Happy coding! ✨
Join my newsletter
Subscribe to get my latest content by email.
I will never send you spam. You can unsubscribe at any time.