Docs Components Layout

Sidebar

React example follows below.
ts
/** One nav row. Every optional region React's SidebarItem exposes is optional here too. */
interface SidebarFixtureItem {
label: string;
/** Single text glyph; every adapter wraps it in `<span aria-hidden="true">`. */
icon?: string;
count?: string;
trailing?: string;
active?: boolean;
href?: string;
}
interface SidebarFixtureSection {
title?: string;
items: SidebarFixtureItem[];
}
interface SidebarFixture {
id: string;
width: number;
collapsedWidth: number;
brand: string;
sections: SidebarFixtureSection[];
account: string;
}
/**
* Canonical CSP-S6 Sidebar fixture — the whole compound rail in one neutral
* data shape, so all five adapters mount the same copy, the same section
* order, the same active row and the same trailing content.
*
* `icon`/`trailing` are plain text glyphs (as EmptyState's `icon` is): the
* fixture may not carry JSX, and every adapter wraps the glyph in the same
* `<span aria-hidden="true">` so the rendered DOM matches element for element.
* `width`/`collapsedWidth` pin the two rail states the render-parity proof
* compares.
*
* The interfaces above are not decoration: `satisfies` would infer a distinct
* literal type per row, and a demo that reads `item.icon` while looping would
* not compile against the one row that has no icon.
*/
const sidebarFixture: SidebarFixture = {
id: "sidebar",
width: 236,
collapsedWidth: 64,
brand: "Northwind",
sections: [
{
title: "Platform",
items: [
{ label: "Services", icon: "▦", count: "38", active: true },
{ label: "Deploys", icon: "↑" },
{ label: "Incidents", icon: "△", trailing: "●" },
],
},
{
items: [{ label: "Team", href: "/team" }],
},
],
account: "Alex Mercer",
};
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarHeader,
SidebarItem,
SidebarSection,
} from "@grassroot/ui-react";
export default function SidebarDemo() {
return (
<div>
<Sidebar width={sidebarFixture.width}>
<SidebarHeader>
<span>{sidebarFixture.brand}</span>
</SidebarHeader>
<SidebarContent>
{sidebarFixture.sections.map((section, index) => (
<SidebarSection key={section.title ?? index} title={section.title}>
{section.items.map((item) => (
<SidebarItem
key={item.label}
href={item.href}
active={item.active}
icon={item.icon ? <span aria-hidden="true">{item.icon}</span> : undefined}
count={item.count ? <span>{item.count}</span> : undefined}
trailing={
item.trailing ? <span aria-hidden="true">{item.trailing}</span> : undefined
}
>
{item.label}
</SidebarItem>
))}
</SidebarSection>
))}
</SidebarContent>
<SidebarFooter>
<span>{sidebarFixture.account}</span>
</SidebarFooter>
</Sidebar>
</div>
);
}
ts
<script setup lang="ts">
/** One nav row. Every optional region React's SidebarItem exposes is optional here too. */
interface SidebarFixtureItem {
label: string;
/** Single text glyph; every adapter wraps it in `<span aria-hidden="true">`. */
icon?: string;
count?: string;
trailing?: string;
active?: boolean;
href?: string;
}
interface SidebarFixtureSection {
title?: string;
items: SidebarFixtureItem[];
}
interface SidebarFixture {
id: string;
width: number;
collapsedWidth: number;
brand: string;
sections: SidebarFixtureSection[];
account: string;
}
/**
* Canonical CSP-S6 Sidebar fixture — the whole compound rail in one neutral
* data shape, so all five adapters mount the same copy, the same section
* order, the same active row and the same trailing content.
*
* `icon`/`trailing` are plain text glyphs (as EmptyState's `icon` is): the
* fixture may not carry JSX, and every adapter wraps the glyph in the same
* `<span aria-hidden="true">` so the rendered DOM matches element for element.
* `width`/`collapsedWidth` pin the two rail states the render-parity proof
* compares.
*
* The interfaces above are not decoration: `satisfies` would infer a distinct
* literal type per row, and a demo that reads `item.icon` while looping would
* not compile against the one row that has no icon.
*/
const sidebarFixture: SidebarFixture = {
id: "sidebar",
width: 236,
collapsedWidth: 64,
brand: "Northwind",
sections: [
{
title: "Platform",
items: [
{ label: "Services", icon: "▦", count: "38", active: true },
{ label: "Deploys", icon: "↑" },
{ label: "Incidents", icon: "△", trailing: "●" },
],
},
{
items: [{ label: "Team", href: "/team" }],
},
],
account: "Alex Mercer",
};
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarHeader,
SidebarItem,
SidebarSection,
} from "@grassroot/ui-vue";
</script>
<template>
<div>
<Sidebar :width="sidebarFixture.width">
<SidebarHeader
><span>{{ sidebarFixture.brand }}</span></SidebarHeader
>
<SidebarContent>
<SidebarSection
v-for="(section, index) in sidebarFixture.sections"
:key="section.title ?? index"
:title="section.title"
>
<!-- The label sits flush against the closing tag: Vue keeps the
whitespace around an interpolation, and a leading space inside
the label span is a real rendering difference against React. -->
<SidebarItem
v-for="item in section.items"
:key="item.label"
:href="item.href"
:active="item.active"
><template v-if="item.icon" #icon
><span aria-hidden="true">{{ item.icon }}</span></template
><template v-if="item.count" #count
><span>{{ item.count }}</span></template
><template v-if="item.trailing" #trailing
><span aria-hidden="true">{{ item.trailing }}</span></template
>{{ item.label }}</SidebarItem
>
</SidebarSection>
</SidebarContent>
<SidebarFooter
><span>{{ sidebarFixture.account }}</span></SidebarFooter
>
</Sidebar>
</div>
</template>
ts
import { Component } from "@angular/core";
/** One nav row. Every optional region React's SidebarItem exposes is optional here too. */
interface SidebarFixtureItem {
label: string;
/** Single text glyph; every adapter wraps it in `<span aria-hidden="true">`. */
icon?: string;
count?: string;
trailing?: string;
active?: boolean;
href?: string;
}
interface SidebarFixtureSection {
title?: string;
items: SidebarFixtureItem[];
}
interface SidebarFixture {
id: string;
width: number;
collapsedWidth: number;
brand: string;
sections: SidebarFixtureSection[];
account: string;
}
/**
* Canonical CSP-S6 Sidebar fixture — the whole compound rail in one neutral
* data shape, so all five adapters mount the same copy, the same section
* order, the same active row and the same trailing content.
*
* `icon`/`trailing` are plain text glyphs (as EmptyState's `icon` is): the
* fixture may not carry JSX, and every adapter wraps the glyph in the same
* `<span aria-hidden="true">` so the rendered DOM matches element for element.
* `width`/`collapsedWidth` pin the two rail states the render-parity proof
* compares.
*
* The interfaces above are not decoration: `satisfies` would infer a distinct
* literal type per row, and a demo that reads `item.icon` while looping would
* not compile against the one row that has no icon.
*/
const sidebarFixture: SidebarFixture = {
id: "sidebar",
width: 236,
collapsedWidth: 64,
brand: "Northwind",
sections: [
{
title: "Platform",
items: [
{ label: "Services", icon: "▦", count: "38", active: true },
{ label: "Deploys", icon: "↑" },
{ label: "Incidents", icon: "△", trailing: "●" },
],
},
{
items: [{ label: "Team", href: "/team" }],
},
],
account: "Alex Mercer",
};
import {
GrassrootSidebar,
GrassrootSidebarContent,
GrassrootSidebarCount,
GrassrootSidebarFooter,
GrassrootSidebarHeader,
GrassrootSidebarIcon,
GrassrootSidebarItem,
GrassrootSidebarSection,
GrassrootSidebarTrailing,
} from "@grassroot/ui-angular";
@Component({
selector: "app-sidebar-demo",
standalone: true,
imports: [
GrassrootSidebar,
GrassrootSidebarContent,
GrassrootSidebarFooter,
GrassrootSidebarHeader,
GrassrootSidebarItem,
GrassrootSidebarSection,
GrassrootSidebarIcon,
GrassrootSidebarTrailing,
GrassrootSidebarCount,
],
template: `
<div>
<grassroot-sidebar [width]="fixture.width">
<grassroot-sidebar-header
><span>{{ fixture.brand }}</span></grassroot-sidebar-header
>
<grassroot-sidebar-content>
@for (section of fixture.sections; track $index) {
<grassroot-sidebar-section [title]="section.title">
@for (item of section.items; track item.label) {
<!-- The projected regions and the label run together with no
whitespace between them: Angular keeps the whitespace
around an interpolation, and a leading space inside the
label span is a real rendering difference against React. -->
<grassroot-sidebar-item [href]="item.href" [active]="!!item.active">@if (item.icon) {<span grassrootSidebarIcon aria-hidden="true">{{ item.icon }}</span>}{{ item.label }}@if (item.count) {<span grassrootSidebarCount>{{ item.count }}</span>}@if (item.trailing) {<span grassrootSidebarTrailing aria-hidden="true">{{ item.trailing }}</span>}</grassroot-sidebar-item>
}
</grassroot-sidebar-section>
}
</grassroot-sidebar-content>
<grassroot-sidebar-footer
><span>{{ fixture.account }}</span></grassroot-sidebar-footer
>
</grassroot-sidebar>
</div>
`,
})
export class SidebarDemoComponent {
protected readonly fixture = sidebarFixture;
}
export default SidebarDemoComponent;
ts
<script lang="ts">
/** One nav row. Every optional region React's SidebarItem exposes is optional here too. */
interface SidebarFixtureItem {
label: string;
/** Single text glyph; every adapter wraps it in `<span aria-hidden="true">`. */
icon?: string;
count?: string;
trailing?: string;
active?: boolean;
href?: string;
}
interface SidebarFixtureSection {
title?: string;
items: SidebarFixtureItem[];
}
interface SidebarFixture {
id: string;
width: number;
collapsedWidth: number;
brand: string;
sections: SidebarFixtureSection[];
account: string;
}
/**
* Canonical CSP-S6 Sidebar fixture — the whole compound rail in one neutral
* data shape, so all five adapters mount the same copy, the same section
* order, the same active row and the same trailing content.
*
* `icon`/`trailing` are plain text glyphs (as EmptyState's `icon` is): the
* fixture may not carry JSX, and every adapter wraps the glyph in the same
* `<span aria-hidden="true">` so the rendered DOM matches element for element.
* `width`/`collapsedWidth` pin the two rail states the render-parity proof
* compares.
*
* The interfaces above are not decoration: `satisfies` would infer a distinct
* literal type per row, and a demo that reads `item.icon` while looping would
* not compile against the one row that has no icon.
*/
const sidebarFixture: SidebarFixture = {
id: "sidebar",
width: 236,
collapsedWidth: 64,
brand: "Northwind",
sections: [
{
title: "Platform",
items: [
{ label: "Services", icon: "▦", count: "38", active: true },
{ label: "Deploys", icon: "↑" },
{ label: "Incidents", icon: "△", trailing: "●" },
],
},
{
items: [{ label: "Team", href: "/team" }],
},
],
account: "Alex Mercer",
};
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarHeader,
SidebarItem,
SidebarSection,
} from "@grassroot/ui-svelte";
</script>
<div>
<Sidebar width={sidebarFixture.width}>
{#snippet children()}
<SidebarHeader>
{#snippet children()}<span>{sidebarFixture.brand}</span>{/snippet}
</SidebarHeader>
<SidebarContent>
{#snippet children()}
{#each sidebarFixture.sections as section (section.title ?? section.items[0].label)}
<SidebarSection title={section.title}>
{#snippet children()}
{#each section.items as item (item.label)}
<!-- An inline snippet is always defined, so each optional
region is handed over only when the fixture row actually
has it — otherwise SidebarItem would render an empty
wrapper span the other four adapters never produce. -->
{#snippet icon()}<span aria-hidden="true">{item.icon}</span>{/snippet}
{#snippet count()}<span>{item.count}</span>{/snippet}
{#snippet trailing()}<span aria-hidden="true">{item.trailing}</span>{/snippet}
{#snippet label()}{item.label}{/snippet}
<SidebarItem
href={item.href}
active={item.active}
icon={item.icon ? icon : undefined}
count={item.count ? count : undefined}
trailing={item.trailing ? trailing : undefined}
children={label}
/>
{/each}
{/snippet}
</SidebarSection>
{/each}
{/snippet}
</SidebarContent>
<SidebarFooter>
{#snippet children()}<span>{sidebarFixture.account}</span>{/snippet}
</SidebarFooter>
{/snippet}
</Sidebar>
</div>
ts
/** @jsxImportSource solid-js */
import { For, type JSX } from "solid-js";
/** One nav row. Every optional region React's SidebarItem exposes is optional here too. */
interface SidebarFixtureItem {
label: string;
/** Single text glyph; every adapter wraps it in `<span aria-hidden="true">`. */
icon?: string;
count?: string;
trailing?: string;
active?: boolean;
href?: string;
}
interface SidebarFixtureSection {
title?: string;
items: SidebarFixtureItem[];
}
interface SidebarFixture {
id: string;
width: number;
collapsedWidth: number;
brand: string;
sections: SidebarFixtureSection[];
account: string;
}
/**
* Canonical CSP-S6 Sidebar fixture — the whole compound rail in one neutral
* data shape, so all five adapters mount the same copy, the same section
* order, the same active row and the same trailing content.
*
* `icon`/`trailing` are plain text glyphs (as EmptyState's `icon` is): the
* fixture may not carry JSX, and every adapter wraps the glyph in the same
* `<span aria-hidden="true">` so the rendered DOM matches element for element.
* `width`/`collapsedWidth` pin the two rail states the render-parity proof
* compares.
*
* The interfaces above are not decoration: `satisfies` would infer a distinct
* literal type per row, and a demo that reads `item.icon` while looping would
* not compile against the one row that has no icon.
*/
const sidebarFixture: SidebarFixture = {
id: "sidebar",
width: 236,
collapsedWidth: 64,
brand: "Northwind",
sections: [
{
title: "Platform",
items: [
{ label: "Services", icon: "▦", count: "38", active: true },
{ label: "Deploys", icon: "↑" },
{ label: "Incidents", icon: "△", trailing: "●" },
],
},
{
items: [{ label: "Team", href: "/team" }],
},
],
account: "Alex Mercer",
};
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarHeader,
SidebarItem,
SidebarSection,
} from "@grassroot/ui-solid";
export default function SidebarDemo(): JSX.Element {
return (
<div>
<Sidebar width={sidebarFixture.width}>
<SidebarHeader>
<span>{sidebarFixture.brand}</span>
</SidebarHeader>
<SidebarContent>
<For each={sidebarFixture.sections}>
{(section) => (
<SidebarSection title={section.title}>
<For each={section.items}>
{(item) => (
<SidebarItem
href={item.href}
active={item.active}
icon={item.icon ? <span aria-hidden="true">{item.icon}</span> : undefined}
count={item.count ? <span>{item.count}</span> : undefined}
trailing={
item.trailing ? <span aria-hidden="true">{item.trailing}</span> : undefined
}
>
{item.label}
</SidebarItem>
)}
</For>
</SidebarSection>
)}
</For>
</SidebarContent>
<SidebarFooter>
<span>{sidebarFixture.account}</span>
</SidebarFooter>
</Sidebar>
</div>
);
}

Installation

API Reference

Import

import {
  Sidebar,
  SidebarContent,
  SidebarFooter,
  SidebarHeader,
  SidebarItem,
  SidebarSection,
} from "@grassroot/ui-react";

Props

Prop
Type
Default
Requirement
Description
width
number | undefined
236
optional
Rail width in px.
open
boolean | undefined
—
optional
Controlled committed open state.
defaultOpen
boolean | undefined
—
optional
Uncontrolled initial open state.
collapsed
boolean | undefined
—
optional
Presentational collapsed state; it does not create a second binding.
onOpenChange
((detail: import("@grassroot/ui-headless-core/sidebar").SidebarOpenChangeDetail) => void) | undefined
—
optional
Called when the headless sidebar requests an open-state change.
Prop
width
Type
number | undefined
Default
236
Requirement
optional
Description
Rail width in px.
Prop
open
Type
boolean | undefined
Default
—
Requirement
optional
Description
Controlled committed open state.
Prop
defaultOpen
Type
boolean | undefined
Default
—
Requirement
optional
Description
Uncontrolled initial open state.
Prop
collapsed
Type
boolean | undefined
Default
—
Requirement
optional
Description
Presentational collapsed state; it does not create a second binding.
Prop
onOpenChange
Type
((detail: import("@grassroot/ui-headless-core/sidebar").SidebarOpenChangeDetail) => void) | undefined
Default
—
Requirement
optional
Description
Called when the headless sidebar requests an open-state change.

Events

EventPayload
onOpenChange((detail: import("@grassroot/ui-headless-core/sidebar").SidebarOpenChangeDetail) => void) | undefined

Content regions

  • children

Native attributes

Standard DOM and ARIA attributes not listed above spread onto the root element.

Import

import {
  Sidebar,
  SidebarContent,
  SidebarFooter,
  SidebarHeader,
  SidebarItem,
  SidebarSection,
} from "@grassroot/ui-vue";

Props

Prop
Type
Default
Requirement
width
number | undefined
236
optional
open
boolean | undefined
—
optional
defaultOpen
boolean | undefined
—
optional
collapsed
boolean | undefined
false
optional
Prop
width
Type
number | undefined
Default
236
Requirement
optional
Prop
open
Type
boolean | undefined
Default
—
Requirement
optional
Prop
defaultOpen
Type
boolean | undefined
Default
—
Requirement
optional
Prop
collapsed
Type
boolean | undefined
Default
false
Requirement
optional

Events

EventPayload
openChange[detail: { open: boolean; reason: string }]

Content regions

  • default

Import

import { Component } from "@angular/core";
import {
  GrassrootSidebar,
  GrassrootSidebarContent,
  GrassrootSidebarCount,
  GrassrootSidebarFooter,
  GrassrootSidebarHeader,
  GrassrootSidebarIcon,
  GrassrootSidebarItem,
  GrassrootSidebarSection,
  GrassrootSidebarTrailing,
} from "@grassroot/ui-angular";

Props

Prop
Type
Default
Requirement
width
number
236
optional
open
boolean
—
optional
defaultOpen
boolean
—
optional
collapsed
boolean
false
optional
Prop
width
Type
number
Default
236
Requirement
optional
Prop
open
Type
boolean
Default
—
Requirement
optional
Prop
defaultOpen
Type
boolean
Default
—
Requirement
optional
Prop
collapsed
Type
boolean
Default
false
Requirement
optional

Events

EventPayload
openChange{ open: boolean; reason: string }

Content regions

  • default

Import

import {
    Sidebar,
    SidebarContent,
    SidebarFooter,
    SidebarHeader,
    SidebarItem,
    SidebarSection,
  } from "@grassroot/ui-svelte";

Props

Prop
Type
Default
Requirement
width
number
236
optional
open
boolean
—
optional
defaultOpen
boolean
—
optional
collapsed
boolean
—
optional
onOpenChange
(detail: { open: boolean; reason: string }) => void
—
optional
class
string
—
optional
className
string
—
optional
Prop
width
Type
number
Default
236
Requirement
optional
Prop
open
Type
boolean
Default
—
Requirement
optional
Prop
defaultOpen
Type
boolean
Default
—
Requirement
optional
Prop
collapsed
Type
boolean
Default
—
Requirement
optional
Prop
onOpenChange
Type
(detail: { open: boolean; reason: string }) => void
Default
—
Requirement
optional
Prop
class
Type
string
Default
—
Requirement
optional
Prop
className
Type
string
Default
—
Requirement
optional

Events

EventPayload
onOpenChange(detail: { open: boolean; reason: string }) => void

Content regions

  • children

Import

import { For, type JSX } from "solid-js";
import {
  Sidebar,
  SidebarContent,
  SidebarFooter,
  SidebarHeader,
  SidebarItem,
  SidebarSection,
} from "@grassroot/ui-solid";

Props

Prop
Type
Default
Requirement
width
number
—
optional
open
boolean
—
optional
defaultOpen
boolean
—
optional
collapsed
boolean
—
optional
onOpenChange
(detail: { open: boolean; reason: string }) => void
—
optional
className
string
—
optional
style
JSX.CSSProperties | string
—
optional
Prop
width
Type
number
Default
—
Requirement
optional
Prop
open
Type
boolean
Default
—
Requirement
optional
Prop
defaultOpen
Type
boolean
Default
—
Requirement
optional
Prop
collapsed
Type
boolean
Default
—
Requirement
optional
Prop
onOpenChange
Type
(detail: { open: boolean; reason: string }) => void
Default
—
Requirement
optional
Prop
className
Type
string
Default
—
Requirement
optional
Prop
style
Type
JSX.CSSProperties | string
Default
—
Requirement
optional

Events

EventPayload
onOpenChange(detail: { open: boolean; reason: string }) => void

Content regions

  • children

Native attributes

Standard DOM and ARIA attributes not listed above spread onto the root element.