Skip to content

SidebarLayout ​

A responsive two-panel layout with a collapsible sidebar and main content area. On mobile, the sidebar becomes an overlay.

When the sidebar is collapsed it is marked inert, so its contents (including the collapse button) are removed from the tab order and the accessibility tree while visually hidden. The expand button is likewise inert while the sidebar is open. This keeps keyboard and screen-reader focus on whichever control is actually visible.

Demo ​

Open in full window ↗

Tabs Demo (Router Mode) ​

Open in full window ↗

Usage ​

vue
<SidebarLayout>
  <template #sidebar>
    <h2>Controls</h2>
    <NumberInput v-model="value" label="Parameter" slider live />
  </template>
  <h1>Main Content</h1>
  <p>Your charts and data go here.</p>
</SidebarLayout>

Slots ​

SlotDescription
sidebarContent rendered in the left sidebar panel
sidebar-header-startLeft side of the sidebar's header band, e.g. a pane title
sidebar-header-endRight side of the sidebar's header band, beside the collapse button
topbar-startLeft side of the main header: before the tabs, or alone without them
topbar-endRight side of the main header, beside the light/dark toggle (topbar still works as the old name)
defaultMain content area

Buttons, selects and text fields placed in the three header slots take the header's control height (28px), so they line up with the collapse and light/dark toggles:

vue
<SidebarLayout :tabs="tabs">
  <template #sidebar-header-start>
    <h2>Parameters</h2>
  </template>
  <template #sidebar-header-end>
    <Button variant="neutral" @click="reset">Reset</Button>
  </template>
  <template #topbar-end>
    <Button variant="neutral" @click="exportCsv"><Icon icon="download" :size="16" /> Export</Button>
  </template>
  ...
</SidebarLayout>

Props ​

PropTypeDefaultDescription
hideTopbarbooleanfalseHides the topbar that contains the light/dark toggle
tabsTab[]undefinedArray of tab definitions to render in the main area
maxWidthstring | number1024pxMax width of the main content: a CSS length ("none" for full width) or pixels
v-model:tabstringundefinedThe active tab value (two-way binding)

Tab type ​

ts
interface Tab {
  value: string; // unique identifier
  label: string; // display text
  to?: string; // optional route path for vue-router integration
}

Tabs ​

When the tabs prop is provided, a tab bar renders at the top of the main content area. Tabs support two modes:

Local mode ​

Use v-model:tab to control which tab is active. Render content conditionally in the default slot.

vue
<script setup>
import { ref } from "vue";
const activeTab = ref("chart");
</script>

<SidebarLayout
  v-model:tab="activeTab"
  :tabs="[
    { value: 'chart', label: 'Chart' },
    { value: 'data', label: 'Data' },
  ]"
>
  <template #sidebar>
    <h2>Controls</h2>
  </template>
  <div v-if="activeTab === 'chart'">Chart content</div>
  <div v-if="activeTab === 'data'">Data table</div>
</SidebarLayout>

Router mode ​

When tabs include a to property and vue-router is installed, clicking a tab navigates to that route. The active tab is automatically determined from the current route.

vue
<SidebarLayout
  :tabs="[
    { value: 'chart', label: 'Chart', to: '/model/chart' },
    { value: 'data', label: 'Data', to: '/model/data' },
  ]"
>
  <template #sidebar>
    <h2>Controls</h2>
  </template>
  <RouterView />
</SidebarLayout>