javascript
37 lines · 6 steps
Building an accessible Sidebar in React
A data-driven nav that computes its own active state and mirrors it into both classes and ARIA.
Explained by
highlit
1import { NavLink, useLocation } from 'react-router-dom';
2
3const NAV_ITEMS = [
4 { to: '/', label: 'Dashboard', end: true },
5 { to: '/projects', label: 'Projects' },
6 { to: '/team', label: 'Team' },
7 { to: '/settings', label: 'Settings' },
8];
9
10export default function Sidebar() {
11 const { pathname } = useLocation();
12
13 const isActive = (to, end) =>
14 end ? pathname === to : pathname === to || pathname.startsWith(`${to}/`);
15
16 return (
17 <nav className="sidebar" aria-label="Main navigation">
18 <ul>
19 {NAV_ITEMS.map(({ to, label, end }) => {
20 const active = isActive(to, end);
21 return (
22 <li key={to}>
23 <NavLink
24 to={to}
25 end={end}
26 aria-current={active ? 'page' : undefined}
27 className={`nav-link${active ? ' nav-link--active' : ''}`}
28 >
29 {label}
30 </NavLink>
31 </li>
32 );
33 })}
34 </ul>
35 </nav>
36 );
37}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Driving navigation from a data array keeps markup uniform and makes adding links a one-line change.
- 2Prefix matching with an `end` escape hatch lets parent routes stay highlighted for nested children while the index route matches exactly.
- 3Reflecting active state into `aria-current` alongside a CSS class keeps the UI accessible, not just visually styled.
Related explainers
javascript
import { useState, useRef } from "react"; export function TagInput({ initialTags = [], onChange }) { const [tags, setTags] = useState(initialTags);
Building a tag input in React
controlled-inputs
state-management
keyboard-handling
Intermediate
8 steps
rust
use axum::{ extract::Path, http::StatusCode, routing::{get, post},
Building a REST resource in Axum
rest-api
routing
serialization
Intermediate
9 steps
javascript
function escapeHtml(str) { return str.replace(/[&<>"']/g, (ch) => ({ '&': '&', '<': '<',
Safely highlighting search matches in text
html-escaping
regex
search-highlighting
Intermediate
7 steps
javascript
import { useEffect, useRef } from "react"; import { useBlocker } from "react-router-dom"; export function useUnsavedChangesPrompt(isDirty, message = "You have unsaved changes. Leave anyway?") {
Guarding unsaved changes with a React hook
custom-hooks
navigation-guard
event-listeners
Intermediate
7 steps
rust
use axum::{ body::Body, http::{header, HeaderValue, StatusCode, Uri}, response::{IntoResponse, Response},
Serving embedded static files in Axum
static-assets
embedding
http-headers
Intermediate
7 steps
javascript
function deepFreeze(obj) { const propNames = Reflect.ownKeys(obj); for (const name of propNames) {
Recursively freezing a nested object
recursion
immutability
object-freezing
Intermediate
6 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/building-an-accessible-sidebar-in-react-explained-javascript-fe5e/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.