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-childand slot yourRouterLink. Aurora merges its styling anddata-activeonto it. - External / plain links —
as="a"with anhref. - 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']" />| Status | Colour | Means |
|---|---|---|
warning | yellow | needs attention |
missing | orange | required data absent |
error | red | something 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
| Prop | Type | Default | Notes |
|---|---|---|---|
as | string | Component | 'button' | Element to render. |
asChild | boolean | false | Render the slotted child as the element instead. |
active | boolean | false | Applies the active styling and data-active. |
dimmed | boolean | false | De-emphasises the row; stays interactive. |
tooltip | string | — | Hover tooltip text. |
Recommended: wrap the primitives
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>