Skip to content

Sidebar ​

Vertical navigation for one area of the app — a list of destinations, optionally grouped, where each entry shows an icon, a label, and (when it matters) a badge or status dots. Pick one item to be active and the rest read as a clear set of places to go.

The sidebar is deliberately dumb: it renders and styles, and you tell it which item is active and where each one links. Routing, permissions, and per-user customisation belong in your app — not in these primitives.

Wrap these in your app

Don't reach for the raw primitives in every screen. Build one app-side wrapper that maps your routes and data onto them, and use that everywhere — see ‘Wrap the primitives’ at the bottom.

The pieces ​

You compose a sidebar from a small family:

  • SidebarItem — one row. The styling shell; everything else nests inside it.
  • SidebarItemIcon · SidebarItemName — the leading icon and the label.
  • SidebarItemBadge — an optional trailing slot (a tier pill, a count, a "New" tag).
  • SidebarItemIndicators — small status dots.
  • SidebarSection — a titled group of items.
  • SidebarLayout — the column-plus-drawer shell the sections live in.
vue
<SidebarItem :active="isActive">
  <SidebarItemIcon name="overview" />
  <SidebarItemName>Overview</SidebarItemName>
  <SidebarItemBadge><TierBadge tier="pro" /></SidebarItemBadge>   <!-- optional -->
  <SidebarItemIndicators :statuses="['warning']" />              <!-- optional -->
</SidebarItem>

Linking & navigation ​

SidebarItem never imports a router — you decide how it navigates. It renders a <button> by default; switch the element for links:

  • In-app routes — use as-child and slot your RouterLink. Aurora merges its styling and data-active onto it.
  • External / plain links — as="a" with an href.
  • Actions (no navigation) — leave it a <button> and listen for @click.
vue
<!-- in-app route -->
<SidebarItem as-child :active="route.name === 'overview'">
  <RouterLink :to="{ name: 'overview' }">
    <SidebarItemIcon name="overview" />
    <SidebarItemName>Overview</SidebarItemName>
  </RouterLink>
</SidebarItem>

<!-- external link -->
<SidebarItem as="a" href="/docs" target="_blank">
  <SidebarItemIcon name="report" /><SidebarItemName>Docs</SidebarItemName>
</SidebarItem>

<!-- action -->
<SidebarItem @click="collapseAll">
  <SidebarItemIcon name="cog-6-tooth" /><SidebarItemName>Settings</SidebarItemName>
</SidebarItem>

The as-child mechanism is reka-ui's Primitive — the slotted element becomes the rendered element, with Aurora's classes and attributes merged in.

Active state ​

Active is a prop — you set it (typically from the current route). The active row gets the navigation accent background and its icon picks up the accent colour. The accent follows your section's --nav-accent-color, falling back to the primary blue when none is set.

vue
<SidebarItem :active="route.name === item.routeName">…</SidebarItem>

Status indicators ​

SidebarItemIndicators takes semantic statuses — Aurora owns the colours, so you never pass raw values:

vue
<SidebarItemIndicators :statuses="['warning', 'missing', 'error']" />
StatusColourMeans
warningyellowneeds attention
missingorangerequired data absent
errorredsomething failed

Badge ​

SidebarItemBadge is a generic trailing slot — drop in whatever belongs at the end of the row:

vue
<SidebarItemBadge><TierBadge tier="scale" /></SidebarItemBadge>

Grouping with sections ​

SidebarSection puts items under an optional uppercase label and disappears from the layout when it has no visible children.

vue
<SidebarSection title="Data collection">
  <SidebarItem …>…</SidebarItem>
  <SidebarItem …>…</SidebarItem>
</SidebarSection>

The shell ​

SidebarLayout is the container the sections live in. On desktop it's the sidebar column beside your content; on mobile it tucks the sidebar into a drawer and shows a sub-nav bar (teleported to the element you name in mobile-subnav-target). It reads --sidebar-width from an ancestor such as StickyHeaderLayout.

vue
<SidebarLayout title="Portfolio" mobile-subnav-target="#mobile-subnav-target">
  <template #sidebar>
    <SidebarSection title="Data collection">…</SidebarSection>
  </template>
  <PageLayout><!-- content --></PageLayout>
</SidebarLayout>

SidebarItem props ​

PropTypeDefaultNotes
asstring | Component'button'Element to render.
asChildbooleanfalseRender the slotted child as the element instead.
activebooleanfalseApplies the active styling and data-active.
dimmedbooleanfalseDe-emphasises the row; stays interactive.
tooltipstring—Hover tooltip text.

A real sidebar entry has to know the current route (for active), your RouterLink, feature flags, status, tier badges — and maybe drag-to-reorder. Repeating all of that at every call site is noise and drifts out of sync.

Build one thin wrapper in your app that maps your domain onto the primitives, and use that everywhere:

vue
<!-- app/components/AppSidebarItem.vue -->
<template>
  <SidebarItem as-child :active="isActiveRoute" :tooltip="tooltip">
    <RouterLink :to="to">
      <SidebarItemIcon :name="icon" />
      <SidebarItemName>{{ label }}</SidebarItemName>
      <SidebarItemBadge v-if="tier"><TierBadge :tier="tier" /></SidebarItemBadge>
      <SidebarItemIndicators :statuses="statuses" />
    </RouterLink>
  </SidebarItem>
</template>

<script setup lang="ts">
import { RouterLink, useRoute } from 'vue-router'
import {
  SidebarItem, SidebarItemIcon, SidebarItemName, SidebarItemBadge, SidebarItemIndicators,
} from '@scaler-tech/aurora/sidebar'
// + your TierBadge, route-active logic, has_warning → statuses mapping…
</script>

Screens then stay declarative and domain-focused — and the wrapper is the single place to evolve routing, features, and customisation:

vue
<SidebarSection title="Data collection">
  <AppSidebarItem :to="{ name: 'overview' }" icon="overview" label="Overview" />
  <AppSidebarItem :to="{ name: 'meters' }" icon="meter" label="Meters" :has-warning="hasWarning" />
</SidebarSection>