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.