Navigation Sidenav

Sidenav

A sidebar navigation component for application dashboards and admin panels. Supports collapsible accordion groups, an icon-strip mode with hover-expand and CSS tooltip fallback for collapsed labels, a mobile slide-in drawer with blurred backdrop, optional localStorage persistence of the collapsed and group state, and a full programmatic API. The same toggle button works on both desktop (collapse/expand) and mobile (open/close drawer).

Real-World Example

The demo below shows a complete admin shell. Click the sidebar toggle button to collapse to icon-strip mode — hover over it to expand temporarily. On mobile the sidebar slides in as a full-screen overlay.

Dashboard

Welcome back, Alex — Tuesday 10 June 2026

Users

1,284

Revenue

$48,250

Active

892

Pending

32

Recent Users

Name Role Status
Jordan Blake Editor Active
Taylor Ross Viewer Active
Morgan Lee Admin Inactive
Casey Kim Editor Pending
HTML
<aside class="wf-sidenav" data-wf-sidenav>

  <!-- Header -->
  <div class="wf-sidenav-header">
    <a class="wf-sidenav-brand" href="/">
      <div class="wf-sidenav-logo-wrap">
        <i class="icon bolt" aria-hidden="true"></i>
      </div>
      <span class="wf-sidenav-brand-name">Acme Inc</span>
    </a>
    <button class="wf-sidenav-collapse-btn" data-wf-sidenav-toggle aria-label="Toggle sidebar">
      <i class="icon chevrons left" aria-hidden="true"></i>
    </button>
  </div>

  <!-- User block -->
  <div class="wf-sidenav-user">
    <div class="wf-sidenav-avatar">AK</div>
    <div class="wf-sidenav-user-info">
      <span class="wf-sidenav-user-name">Alex Kumar</span>
      <span class="wf-sidenav-user-role">Administrator</span>
    </div>
  </div>

  <!-- Body -->
  <div class="wf-sidenav-body">
    <span class="wf-sidenav-label">Main</span>

    <a class="wf-sidenav-item wf-active" href="/dashboard" title="Dashboard" aria-current="page">
      <i class="icon home wf-sidenav-icon" aria-hidden="true"></i>
      <span class="wf-sidenav-text">Dashboard</span>
    </a>

    <a class="wf-sidenav-item" href="/analytics" title="Analytics">
      <i class="icon chart bar wf-sidenav-icon" aria-hidden="true"></i>
      <span class="wf-sidenav-text">Analytics</span>
    </a>

    <hr class="wf-sidenav-divider">
    <span class="wf-sidenav-label">Manage</span>

    <!-- Collapsible group -->
    <div class="wf-sidenav-group" data-wf-sidenav-group>
      <button class="wf-sidenav-item wf-sidenav-item--parent" aria-expanded="false" title="Users">
        <i class="icon user multiple wf-sidenav-icon" aria-hidden="true"></i>
        <span class="wf-sidenav-text">Users</span>
        <i class="icon chevron down wf-sidenav-chevron" aria-hidden="true"></i>
      </button>
      <div class="wf-sidenav-sub">
        <div>
          <a class="wf-sidenav-sub-item" href="/users">All Users</a>
          <a class="wf-sidenav-sub-item" href="/users/roles">Roles</a>
        </div>
      </div>
    </div>

    <!-- Item with badge -->
    <a class="wf-sidenav-item" href="/inbox" title="Inbox">
      <i class="icon mail wf-sidenav-icon" aria-hidden="true"></i>
      <span class="wf-sidenav-text">Inbox</span>
      <span class="wf-sidenav-badge">3</span>
    </a>
  </div>

  <!-- Footer -->
  <div class="wf-sidenav-footer">
    <a class="wf-sidenav-item" href="/settings" title="Settings">
      <i class="icon settings wf-sidenav-icon" aria-hidden="true"></i>
      <span class="wf-sidenav-text">Settings</span>
    </a>
  </div>

</aside>

Anatomy

A sidenav is built from four vertical sections. The wf-sidenav-header holds the brand and the collapse toggle button. The optional wf-sidenav-user block sits immediately below it. The scrollable wf-sidenav-body contains all navigation items, labels, groups, and dividers. The wf-sidenav-footer is pinned to the bottom and is used for utility links like Settings and Help. All sections except the header and body are optional.

HTML
<aside class="wf-sidenav" data-wf-sidenav>

  <!-- ① Header — brand + collapse button -->
  <div class="wf-sidenav-header">
    <a class="wf-sidenav-brand" href="/">
      <div class="wf-sidenav-logo-wrap"><i class="icon bolt"></i></div>
      <span class="wf-sidenav-brand-name">Brand</span>
    </a>
    <button class="wf-sidenav-collapse-btn" data-wf-sidenav-toggle aria-label="Toggle sidebar">
      <i class="icon chevrons left"></i>
    </button>
  </div>

  <!-- ② User block (optional) -->
  <div class="wf-sidenav-user">
    <div class="wf-sidenav-avatar">JD</div>
    <div class="wf-sidenav-user-info">
      <span class="wf-sidenav-user-name">Jane Doe</span>
      <span class="wf-sidenav-user-role">Editor</span>
    </div>
  </div>

  <!-- ③ Body — scrollable nav items -->
  <div class="wf-sidenav-body">
    <span class="wf-sidenav-label">Section</span>

    <a class="wf-sidenav-item" href="/page" title="Page">
      <i class="icon home wf-sidenav-icon"></i>
      <span class="wf-sidenav-text">Page</span>
    </a>

    <!-- add groups, badges, dividers here -->
  </div>

  <!-- ④ Footer (optional) — pinned to bottom -->
  <div class="wf-sidenav-footer">
    <a class="wf-sidenav-item" href="/settings" title="Settings">
      <i class="icon settings wf-sidenav-icon"></i>
      <span class="wf-sidenav-text">Settings</span>
    </a>
  </div>

</aside>

Collapsible Groups

Wrap a parent trigger button and a div.wf-sidenav-sub in a div.wf-sidenav-group[data-wf-sidenav-group] . The trigger button must carry the wf-sidenav-item--parent modifier class. The script toggles the data-wf-open attribute on the group element and uses a max-height transition to reveal the sub-list. By default, groups behave as an accordion — opening one closes the others. See Multi-Open to change this.

Add data-wf-open to a group in the HTML to start it open. A group whose sub-list contains an item with wf-active or aria-current="page" is also automatically opened by the script on init, and the parent trigger gains wf-active to highlight the active section.

HTML
<!-- Start open with data-wf-open -->
<div class="wf-sidenav-group" data-wf-sidenav-group data-wf-open>

  <!-- Trigger -->
  <button class="wf-sidenav-item wf-sidenav-item--parent" aria-expanded="false" title="Users">
    <i class="icon users wf-sidenav-icon" aria-hidden="true"></i>
    <span class="wf-sidenav-text">Users</span>
    <i class="icon chevron down wf-sidenav-chevron" aria-hidden="true"></i>
  </button>

  <!-- Sub-list -->
  <div class="wf-sidenav-sub">
    <div>
      <a class="wf-sidenav-sub-item wf-active" href="/users">All Users</a>
      <a class="wf-sidenav-sub-item" href="/users/roles">Roles</a>
      <a class="wf-sidenav-sub-item" href="/users/permissions">Permissions</a>
    </div>
  </div>

</div>

Badges & Dividers

Add a span.wf-sidenav-badge inside any item to show a notification count. The badge is positioned after wf-sidenav-text via margin-inline-start: auto and hidden automatically in collapsed icon-strip mode. Use hr.wf-sidenav-divider between sections of the body or footer to add a subtle horizontal rule.

HTML
<!-- Item with badge -->
<a class="wf-sidenav-item" href="/inbox" title="Inbox">
  <i class="icon mail wf-sidenav-icon" aria-hidden="true"></i>
  <span class="wf-sidenav-text">Inbox</span>
  <span class="wf-sidenav-badge">3</span>
</a>

<!-- Divider between nav sections -->
<hr class="wf-sidenav-divider">

Collapsed Strip

On desktop, clicking the collapse toggle shrinks the sidebar to a 64 px icon-strip by adding data-wf-collapsed to the root element. All text labels, chevrons, and badges are hidden; only icons remain. Hovering the strip temporarily expands it back to full width so the user can read labels and interact with groups — the expansion uses pure CSS ( :hover + @starting-style for a smooth opacity fade-in).

Add a title attribute to every wf-sidenav-item to enable the built-in CSS tooltip that appears next to the icon when the strip is collapsed and not hovered. Start the sidebar already collapsed by adding data-wf-collapsed directly in the HTML.

HTML
<!-- Start in collapsed state -->
<aside class="wf-sidenav" data-wf-sidenav data-wf-collapsed>
  ...

  <!-- title enables the CSS tooltip in stripped mode -->
  <a class="wf-sidenav-item" href="/analytics" title="Analytics">
    <i class="icon chart bar wf-sidenav-icon"></i>
    <span class="wf-sidenav-text">Analytics</span>
  </a>

  ...
</aside>

Mobile Drawer

Below the breakpoint (default 768 px) the sidebar switches to position: fixed and is hidden off-screen. Any element carrying data-wf-sidenav-toggle — whether inside or outside the sidebar — will open or close it. Use an external toggle in your topbar to provide a hamburger button that is visible when the sidebar is hidden. The script appends a wf-sidenav-backdrop to <body> ; clicking it closes the drawer.

HTML
<!-- External toggle (e.g. inside a topbar) -->
<button data-wf-sidenav-toggle aria-label="Open menu">
  <i class="icon menu" aria-hidden="true"></i>
</button>

<!-- Sidebar -->
<aside class="wf-sidenav" data-wf-sidenav>
  <div class="wf-sidenav-header">
    <a class="wf-sidenav-brand" href="/">...</a>
    <!-- Internal close button inside the sidebar header -->
    <button class="wf-sidenav-collapse-btn" data-wf-sidenav-toggle aria-label="Close sidebar">
      <i class="icon chevrons left"></i>
    </button>
  </div>
  ...
</aside>

<!-- Backdrop is injected by the script; no markup needed -->

Multi-Open

By default, the sidenav is in accordion mode — opening a group closes all others. Add data-wf-multi-open to the root element to allow any number of groups to be open at the same time.

HTML
<aside class="wf-sidenav" data-wf-sidenav data-wf-multi-open>
  ...
</aside>

Persistence

Add data-wf-persist-collapsed to remember the collapsed/expanded state in localStorage across page loads. Add data-wf-persist-groups to remember which groups the user has opened. Group state is stored by index position inside the sidebar so it is best suited to a sidebar whose item order does not change between pages.

HTML
<!-- Persist collapsed state only -->
<aside class="wf-sidenav" data-wf-sidenav data-wf-persist-collapsed>...</aside>

<!-- Persist both collapsed state and open groups -->
<aside class="wf-sidenav" data-wf-sidenav data-wf-persist-collapsed data-wf-persist-groups>...</aside>

Data Attributes

Attribute Element Description
data-wf-sidenav .wf-sidenav Initializes the component on this element.
data-wf-sidenav-toggle Any button Marks a button as the collapse/hamburger toggle. Can appear inside or outside the sidebar. Multiple buttons are supported.
data-wf-sidenav-group .wf-sidenav-group Marks an element as a collapsible group container.
data-wf-open [data-wf-sidenav-group] Opens the group on init. Also set/removed by the script at runtime.
data-wf-collapsed .wf-sidenav Starts the sidebar in icon-strip mode. Also set/removed by the script at runtime.
data-wf-multi-open .wf-sidenav Allows multiple groups to be open simultaneously (disables accordion behavior).
data-wf-persist-collapsed .wf-sidenav Saves the collapsed/expanded state to localStorage across page loads.
data-wf-persist-groups .wf-sidenav Saves the open groups (by index) to localStorage across page loads.

CSS Properties

All tokens are scoped to .wf-sidenav so multiple sidebars on the same page can have independent themes.

Property Default Description
--wf-snav-width 260px Full expanded width of the sidebar.
--wf-snav-width-col 64px Width of the collapsed icon-strip.
--wf-snav-bg var(--wf-surface) Background color of the sidebar.
--wf-snav-border var(--wf-border) color of the inline-end border and section dividers.
--wf-snav-text var(--wf-text) Default text color for items, brand, and username.
--wf-snav-muted var(--wf-text-muted) Muted text color for labels, role, chevrons, and sub-items.
--wf-snav-accent var(--wf-primary) Accent color used for the logo box, avatar, badge, and active states.
--wf-snav-hover-bg var(--wf-surface-subtle) Background on item hover.
--wf-snav-active-bg color-mix(in srgb, var(--wf-primary) 10%, transparent) Background for active items.
--wf-snav-active-text var(--wf-primary) Text color for active items and active sub-items.
--wf-snav-item-radius var(--wf-radius-4) Border radius is applied to items, sub-items, and the collapse button.
--wf-snav-speed .22s Transition duration for collapse, group open/close, and hover.
--wf-snav-ease cubic-bezier(.4, 0, .2, 1) Easing function used for all transitions.

Programmatic API

Each initialized sidebar exposes an instance on el._wfSidenav .

Method Description
toggle() Desktop: toggle between collapsed and expanded. Mobile: open or close the drawer.
collapse() Collapse to an icon-strip (desktop). No-op if already collapsed.
expand() Restore to full width (desktop). No-op if already expanded.
openMobile() Slide the sidebar in on mobile. No-op if already open.
closeMobile() Slide the sidebar out and remove the backdrop on mobile. No-op if already closed.
destroy() Remove all event listeners, close the mobile drawer, and remove the instance reference from the element.
JS
// Auto-init instance
const sidebar = document.querySelector('[data-wf-sidenav]')._wfSidenav

sidebar.collapse()
sidebar.expand()
sidebar.toggle()
sidebar.openMobile()
sidebar.closeMobile()
sidebar.destroy()

// Manual init with options
import { WojoSidenav } from '/assets/js/wf-production.min.js'

const instance = new WojoSidenav(el, {
    multiOpen:        false,
    collapsed:        false,
    breakpoint:       1024,
    persistCollapsed: true,
    persistGroups:    true,
}).init()

Events

All events are dispatched on the sidebar root element and bubble normally.

Event Detail Description
wf:sidenav-collapse { collapsed: Boolean } Fired when the sidebar is collapsed or expanded. detail.collapsed is true when collapsing.
wf:sidenav-open { group: HTMLElement } Fired when a collapsible group opens.
wf:sidenav-close { group: HTMLElement } Fired when a collapsible group closes.
wf:sidenav-mobile-open Fired when the mobile drawer slides in.
wf:sidenav-mobile-close Fired when the mobile drawer slides out.
JS
const sidebar = document.querySelector('[data-wf-sidenav]')

sidebar.addEventListener('wf:sidenav-collapse', e => {
  console.log('Collapsed:', e.detail.collapsed)
  document.querySelector('.main-content').classList.toggle('sidebar-collapsed', e.detail.collapsed)
})

sidebar.addEventListener('wf:sidenav-mobile-open', () => {
  document.body.classList.add('no-scroll')
})

sidebar.addEventListener('wf:sidenav-mobile-close', () => {
  document.body.classList.remove('no-scroll')
})

Class Reference

All modifier classes available on .wf-sidenav

Class Description
wf-sidenav Root sidebar element. Defines all CSS custom properties and handles width transitions.
wf-sidenav-header Top section that holds the brand link and the collapse toggle button.
wf-sidenav-brand Brand link inside the header. Contains the logo wrap and brand name.
wf-sidenav-logo-wrap Square icon box inside the brand link.
wf-sidenav-brand-name Text the label inside the brand link. Hidden (width:0) in collapsed mode.
wf-sidenav-collapse-btn Icon-only toggle button inside the header. Requires data-wf-sidenav-toggle.
wf-sidenav-user Optional user row below the header with avatar and user info.
wf-sidenav-avatar Circular avatar element inside the user block. Display text initials or an <img>.
wf-sidenav-user-info Text wrapper holding the username and role. Hidden in collapsed mode.
wf-sidenav-user-name User's full name inside wf-sidenav-user-info.
wf-sidenav-user-role User's role or subtitle inside wf-sidenav-user-info.
wf-sidenav-body Scrollable flex column that holds all nav items, labels, groups, and dividers.
wf-sidenav-footer Optional section pinned to the bottom of the sidebar, separated by a top border.
wf-sidenav-label Small all-caps section heading inside the body or footer.
wf-sidenav-item Nav link or button. Apply wf-active or aria-current="page" to mark the current page.
wf-sidenav-item--parent Modifier on a wf-sidenav-item button that acts as the trigger for a collapsible group.
wf-sidenav-icon Icon element inside an item. Remains visible in collapsed mode.
wf-sidenav-text Text label inside an item. Hidden in collapsed mode.
wf-sidenav-chevron Chevron icon on a parent trigger. Rotates 180° when the group is open. Hidden in collapsed mode.
wf-sidenav-badge Pill notification counts inside an item. Hidden in collapsed mode.
wf-sidenav-divider Horizontal rule divider between nav sections.
wf-sidenav-group Collapsible group wrapper. Requires data-wf-sidenav-group.
wf-sidenav-sub Collapsible sub-item list inside a group. Animated via max-height.
wf-sidenav-sub-item Link inside a sub-list. Has a small dot indicator via ::before. Apply wf-active for the current page.
wf-sidenav-backdrop Blurred overlay injected into <body> by JS while the mobile drawer is open.
data-wf-open State attribute added/removed by JS on [data-wf-sidenav-group] when a group is open; also added to the root element when the mobile drawer is open.
data-wf-collapsed State attribute added/removed by JS on the root element when the sidebar is in icon-strip mode.
is-visible State class added by JS to .wf-sidenav-backdrop to trigger its fade-in transition.
Show More