React - Admin Panel Left Menu
Overview
Estimated time: 20–30 minutes
Build a collapsible left sidebar menu with active item state synchronized to location hash and persisted collapse preference.
Try it: Collapsible sidebar
View source
function useHashPath(){
const [path, setPath] = React.useState(() => location.hash.replace('#','') || '/dashboard');
React.useEffect(() => { const on=()=> setPath(location.hash.replace('#','')||'/dashboard'); addEventListener('hashchange', on); return ()=> removeEventListener('hashchange', on); }, []);
return [path, (p)=> (location.hash=p)];
}
function Sidebar({collapsed, onToggle, active}){
const items = [
{to:'/dashboard', label:'Dashboard'},
{to:'/users', label:'Users'},
{to:'/settings', label:'Settings'},
];
return (
<aside style={{width: collapsed? 56: 200, transition:'width .2s', borderRight:'1px solid var(--border)', padding:8}}>
<button onClick={onToggle} title="Toggle">{collapsed? '»' : '«'}</button>
<nav style={{display:'grid', gap:6, marginTop:8}} aria-label="Sidebar">
{items.map(it => (
<a key={it.to} href={'#'+it.to} aria-current={active===it.to? 'page': undefined} style={{display:'block', padding:'6px 8px', borderRadius:6, background: active===it.to? 'var(--accent-subtle)': 'transparent'}}>
{collapsed? it.label[0] : it.label}
</a>
))}
</nav>
</aside>
);
}
function Main({path}){
return <div style={{padding:12}}><h3>{path.slice(1)}</h3><p>Content for {path}</p></div>}
function App(){
const [path, nav] = useHashPath();
const [collapsed, setCollapsed] = React.useState(() => localStorage.getItem('sb-collapsed')==='1');
React.useEffect(() => { localStorage.setItem('sb-collapsed', collapsed? '1':'0'); }, [collapsed]);
return (<div style={{display:'grid', gridTemplateColumns:'auto 1fr', minHeight:220}}>
<Sidebar collapsed={collapsed} onToggle={()=> setCollapsed(c=>!c)} active={path} />
<Main path={path} />
</div>);
}
ReactDOM.createRoot(document.getElementById('try-sidebar')).render(<App />);
Syntax primer
- Persist UI preferences (collapsed) in localStorage.
- Use aria-current on active navigation items.
Common pitfalls
- Collapsing without tooltips—consider adding accessible titles or tooltips for icons.
Exercises
- Add nested sections that expand/collapse.
- Remember the last active route on reload.
Checks for Understanding
- How is the collapsed state persisted across reloads?
- How do you indicate the active item for accessibility?
- Where does the active route come from in this example?
Answers
- By syncing a boolean to localStorage (e.g., 'sb-collapsed' = '1'/'0') in an effect and initializing state from it.
- Use aria-current="page" on the active link so assistive technologies can announce it as the current page.
- From location.hash via a small hook that listens to the hashchange event and updates component state.
Checks for Understanding
- How is the collapsed state persisted across reloads?
- How do you indicate the active item for accessibility?
- Where does the active route come from in this example?
Answers
- By syncing a boolean to localStorage (e.g., 'sb-collapsed' = '1'/'0') in an effect and initializing state from it.
- Use aria-current="page" on the active link so assistive technologies can announce it as the current page.
- From location.hash via a small hook that listens to the hashchange event and updates component state.