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

  1. Add nested sections that expand/collapse.
  2. Remember the last active route on reload.

Checks for Understanding

  1. How is the collapsed state persisted across reloads?
  2. How do you indicate the active item for accessibility?
  3. Where does the active route come from in this example?
Answers
  1. By syncing a boolean to localStorage (e.g., 'sb-collapsed' = '1'/'0') in an effect and initializing state from it.
  2. Use aria-current="page" on the active link so assistive technologies can announce it as the current page.
  3. From location.hash via a small hook that listens to the hashchange event and updates component state.

Checks for Understanding

  1. How is the collapsed state persisted across reloads?
  2. How do you indicate the active item for accessibility?
  3. Where does the active route come from in this example?
Answers
  1. By syncing a boolean to localStorage (e.g., 'sb-collapsed' = '1'/'0') in an effect and initializing state from it.
  2. Use aria-current="page" on the active link so assistive technologies can announce it as the current page.
  3. From location.hash via a small hook that listens to the hashchange event and updates component state.