- Prop
width- Type
number | undefined- Default
236- Requirement
- optional
- Description
- Rail width in px.
Docs Components Layout
Sidebar
- parts
- —
- nodes
- —
- size
- —
- part
- —
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> );}- parts
- —
- nodes
- —
- size
- —
- part
- —
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>- parts
- —
- nodes
- —
- size
- —
- part
- —
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;- parts
- —
- nodes
- —
- size
- —
- part
- —
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>- parts
- —
- nodes
- —
- size
- —
- part
- —
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
widthnumber | undefined236optional
Rail width in px.
openboolean | undefined—
optional
Controlled committed open state.
defaultOpenboolean | undefined—
optional
Uncontrolled initial open state.
collapsedboolean | 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
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
| Event | Payload |
|---|---|
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
widthnumber | undefined236optional
openboolean | undefined—
optional
defaultOpenboolean | undefined—
optional
collapsedboolean | undefinedfalseoptional
- 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
| Event | Payload |
|---|---|
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
widthnumber236optional
openboolean—
optional
defaultOpenboolean—
optional
collapsedbooleanfalseoptional
- 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
| Event | Payload |
|---|---|
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
widthnumber236optional
openboolean—
optional
defaultOpenboolean—
optional
collapsedboolean—
optional
onOpenChange(detail: { open: boolean; reason: string }) => void—
optional
classstring—
optional
classNamestring—
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
| Event | Payload |
|---|---|
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
widthnumber—
optional
openboolean—
optional
defaultOpenboolean—
optional
collapsedboolean—
optional
onOpenChange(detail: { open: boolean; reason: string }) => void—
optional
classNamestring—
optional
styleJSX.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
| Event | Payload |
|---|---|
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.