Sidebar Nav

Free

The reusable half of a dashboard sidebar: grouped items, sub-navigation, a collapsed rail and a mobile drawer — with no router dependency and no brand welded in. Bring your own header and footer.

</>sv-sidebar-nav sidebarnavigationshellraildrawermenudashboard
import { SvSidebarNavComponent } from '@styloviz/sidebar-nav';

Demo & usage

Basic usage

Usage example — open the gallery for the full interactive demo.

An interactive demo for this component lives in the gallery.

Open live demo

More examples

With projected chrome

example.htmlAngular
<sv-sidebar-nav [menuGroups]="groups" [activeRoute]="router.url">
  <a slot="header" href="/">Acme</a>
  <span slot="footer">v2.4</span>
</sv-sidebar-nav>

Collapsed rail, no footer

example.htmlAngular
<sv-sidebar-nav [menuGroups]="groups" [collapsed]="true" [showFooter]="false" />

Wiring it to the Angular router

example.htmlAngular
// One provider, once, at the app root. The component never imports the
// router itself — that is what makes it usable in a hybrid app.
provideSvLinkAdapter(() => {
  const router = inject(Router);
  return {
    kind: 'router',
    href: (t) => router.serializeUrl(router.createUrlTree([t].flat())),
    navigate: (t) => { void router.navigateByUrl(`${t}`); return true; },
  };
})

When to use

  • The primary navigation of a dashboard, admin panel or internal tool.
  • You want the rail and the mobile drawer without writing route-sync three times.
  • Your app does not route the way Angular's router does — a hybrid shell, a micro-frontend, a widget embedded in somebody else's page.
  • For a horizontal header, use Mega Menu; for in-page sections, use Anchor. This is the persistent side rail.

Async states

None — this component has nothing to load, so it has no loading, empty or error state to drive.

Inputs

NameTypeDefaultDescription
menuGroupsreadonly MenuGroup[][]Grouped navigation items.
activeRoutestring''The path the reader is on, so one item can be marked current. Controlled rather than derived. See the class note: matching a route is the router's job, and a component that guessed at it would be wrong in a different way in every app. `[activeRoute]="router.url"` is the whole wiring for a routed app.
childKeystring | null'c'URL parameter naming the open sub-item — the `?c=…` `sv-sidebar` has always used. Read through `SvNavLocation`, so it is inert until the app provides one. `null` turns sub-item tracking off.
activeChildIdstring | nullnullPin the open sub-item, ignoring the URL. For a host that keeps this in its own state rather than in the address.
collapsedboolean (two-way)falseIcon-only rail. Two-way via `[(collapsed)]`.
mobileOpenboolean (two-way)falseMobile drawer open. Two-way via `[(mobileOpen)]`.
mobileBreakpointnumber1024Viewport width below which the sidebar becomes an off-canvas drawer driven by `mobileOpen`. Set to `0` to keep it docked at every size — e.g. when it is embedded in a panel rather than used as the page's shell.
ariaLabelstring'Main navigation'Accessible name of the `<nav>` landmark. A page with two of these — a primary sidebar and a secondary panel — needs them told apart.
showHeaderbooleantrueRender the header / footer slots at all. A rail with no chrome is a common shape and an empty bordered box is not.
showFooterbooleantrueRender the footer area holding the `[slot=footer]` content and, outside the mobile drawer, the collapse toggle.
surfaceIdstring | nullnullNames this sidebar in the shared navigation-analytics stream. Ignored unless `provideSvNavTelemetry()` is wired up.
customClassstring''Additional CSS classes on the `<aside>`.

Outputs

EventPayload typeDescription
itemSelectedSidebarNavSelectionSomething was picked. Carries the group and, for a sub-item, the child — a host closing a drawer needs the event, and a host doing its own analytics needs to know *which* thing was clicked, not just that one was.
mobileClosevoidThe drawer should close. Emitted alongside `mobileOpen` being set to `false`, for a host that has its own shell state to keep in step.
toggleCollapsebooleanThe rail toggle was pressed. `collapsed` is already flipped.
collapsedChangebooleanEmits the new `collapsed` whenever it changes — the output half of `[(collapsed)]`.
mobileOpenChangebooleanEmits the new `mobileOpen` whenever it changes — the output half of `[(mobileOpen)]`.

Customization & theming

  • Expanded, collapsed rail and mobile drawer out of one model — `N-2` Bottom Navigation and `N-3` Rail reuse the exported model and matcher rather than re-implementing route sync.
  • Badges per item, with four tints; sub-navigation groups per item.
  • `childKey` names the URL parameter that tracks the open sub-item, read through `SvNavLocation`; `activeChildId` pins it from host state instead.
  • `showHeader` / `showFooter` remove the chrome boxes entirely for a bare rail.

Best practices

  • Do: Bind `activeRoute` to your router's current URL. The component deliberately does not derive it: matching a route is the router's hardest job, and a component that guessed would be wrong differently in every app.
  • Do: Project your own header and footer. The component ships no logo, no wordmark and no resource links — that welded-in chrome is exactly what made the previous `sv-sidebar` unusable outside this documentation site.
  • Tip: Give the `<nav>` a distinct `ariaLabel` when a page has two of them. A primary sidebar and a secondary settings panel are two landmarks, and a screen-reader user needs them told apart.
  • Don't: Don't keep sub-navigation open in the collapsed rail. There is no room for it, and a tooltip is not a sub-menu.

Accessibility

  • Every destination is a real `<a href>`, so middle-click, ⌘-click and "copy link address" work — which they cannot when a router directive owns the element.
  • The current item carries `aria-current="page"`, as does the open sub-item.
  • The collapse control is a real `<button>` with `aria-expanded` and a label that says which way it will go.
  • A modified click is never intercepted: that gesture means "open this somewhere else".
  • The `<nav>` landmark takes its accessible name from `ariaLabel`.

Feedback on Sidebar Nav

Spotted a gap in this page, or something that didn't work as described? Comments are GitHub Discussions in the public styloviz/community repo, so posting needs a GitHub account.

Loads giscus.app and GitHub — nothing is fetched until you click.