All components
Sidebar
App navigation rails with header, grouped nav items, and a scrollable body.
123456789101112131415161718192021222324252627282930import { Sidebar } from "#components/ui/base-ui/sidebar.tsx";import { SidebarNavItem } from "#components/ui/base-ui/sidebar-nav-item.tsx";
export function Example() { return ( // variant/mode/size resolve once on Root; every part inherits them via // context. Width comes from --sidebar-width (280px by default) — // override it with a StyleX style on Root. <Sidebar.Root variant="sidebar" mode="collapsible" size="md"> <Sidebar.Header>Acme Inc</Sidebar.Header> <Sidebar.Body> <Sidebar.Group> <Sidebar.GroupHeader> <Sidebar.GroupTitle>Workspace</Sidebar.GroupTitle> </Sidebar.GroupHeader> <Sidebar.GroupContent> <ul> <SidebarNavItem.Item> <SidebarNavItem.Button aria-current="page"> Dashboard </SidebarNavItem.Button> </SidebarNavItem.Item> </ul> </Sidebar.GroupContent> </Sidebar.Group> </Sidebar.Body> <Sidebar.Footer>{/* user menu */}</Sidebar.Footer> </Sidebar.Root> );}Variants
sidebar
Sizes
md
Install
1npx stylexblocks add sidebarSource
components/ui/base-ui/sidebar.tsx
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318import * as stylex from "@stylexjs/stylex";import { createSlotRecipeContext } from "#lib/base-ui/create-slot-recipe-context.tsx";import { defineSlotRecipe } from "#lib/recipe-props.ts";import { cursor } from '@stylexblocks/ui/tokens/cursor.stylex.ts'import { durations } from '@stylexblocks/ui/tokens/durations.stylex.ts'import { fontSizes } from '@stylexblocks/ui/tokens/font-sizes.stylex.ts'import { fontWeights } from '@stylexblocks/ui/tokens/font-weights.stylex.ts'import { radii } from '@stylexblocks/ui/tokens/radii.stylex.ts'import { semanticColors } from '@stylexblocks/ui/semantic-tokens/colors.stylex.ts'import { semanticRadii } from '@stylexblocks/ui/semantic-tokens/radii.stylex.ts'import { sizes } from '@stylexblocks/ui/tokens/sizes.stylex.ts'import { spacing } from '@stylexblocks/ui/tokens/spacing.stylex.ts'import { zIndices } from '@stylexblocks/ui/tokens/z-indices.stylex.ts'
const sidebarRoot = stylex.create({ base: { '--sidebar-z-index': zIndices.layer3, position: 'relative', display: 'flex', flexDirection: 'column', color: semanticColors.sidebarFg, },})
const sidebarRootVariants = stylex.create({ sidebar: { borderRightWidth: '1px', borderColor: semanticColors.sidebarBorder, },})
const sidebarRootModes = stylex.create({ collapsible: { width: 'var(--sidebar-width, 280px)', maxWidth: '100vw,var(--sidebar-max-width, 320px)', minWidth: 'var(--sidebar-min-width, 220px)', backgroundColor: semanticColors.sidebarBg, transitionProperty: 'margin-left', }, flyout: { position: 'fixed', top: spacing._2, left: spacing._2, bottom: spacing._2, zIndex: 'var(--sidebar-z-index)', width: 'var(--sidebar-width, 280px)', maxWidth: '100vw,var(--sidebar-max-width, 320px)', minWidth: 'var(--sidebar-min-width, 220px)', backgroundColor: semanticColors.sidebarBg, borderColor: semanticColors.sidebarBorder, boxShadow: 'none', borderWidth: '1px', borderRadius: radii.lg, },})
const sidebarBackdrop = stylex.create({ base: { position: 'fixed', inset: 0, '--sidebar-backdrop-z-index': zIndices.layer3, zIndex: 'calc(var(--sidebar-backdrop-z-index) - 2)', },})
const sidebarTrigger = stylex.create({ base: { display: 'inline-flex', appearance: 'none', alignItems: 'center', justifyContent: 'center', userSelect: 'none', whiteSpace: 'nowrap', verticalAlign: 'middle', outline: 'none', },})
const sidebarHeader = stylex.create({ base: { display: 'flex', flexDirection: 'row', paddingBlock: spacing._2, },})
const sidebarHeaderSizes = stylex.create({ md: { paddingInline: spacing._3, },})
const sidebarBody = stylex.create({ base: { display: 'flex', flexDirection: 'column', gap: spacing._4, flex: 1, paddingBlock: spacing._3, overflowY: 'auto', },})
const sidebarBodySizes = stylex.create({ md: { paddingInline: spacing._3, },})
const sidebarFooter = stylex.create({ base: { display: 'flex', flexDirection: 'column', paddingBlock: spacing._2, },})
const sidebarFooterSizes = stylex.create({ md: { paddingInline: spacing._3, },})
const sidebarGroup = stylex.create({ base: { position: 'relative', },})
const sidebarGroupHeader = stylex.create({ base: { display: 'flex', flexDirection: 'row', alignItems: 'center', gap: spacing._1, height: sizes._6, borderRadius: radii.md, fontSize: fontSizes.xs, transitionProperty: 'background-color, border-color, color, fill, stroke, opacity, box-shadow, transform', transitionDuration: durations.fast, },})
const sidebarGroupTitle = stylex.create({ base: { display: 'flex', alignItems: 'center', flex: 1, paddingInline: spacing._2, fontWeight: fontWeights.medium, color: `color-mix(in oklch, ${semanticColors.sidebarFg} 70%, transparent)`, },})
const sidebarGroupEndElement = stylex.create({ base: {},})
const sidebarGroupEndElementSizes = stylex.create({ md: { paddingInlineEnd: spacing._1, },})
const sidebarGroupContent = stylex.create({ base: { paddingTop: spacing._1, display: 'flex', flexDirection: 'column', },})
const sidebarTrack = stylex.create({ base: { display: 'flex', justifyContent: 'center', position: 'absolute', top: 0, right: '-4px', bottom: 0, width: '7px', cursor: cursor.button, '::after': { content: '""', display: 'block', height: '100%', width: '2px', opacity: { default: 0, ':hover': 1, }, transitionProperty: 'opacity', transitionDuration: durations.fast, transitionDelay: '0.2s', backgroundColor: `color-mix(in oklch, ${semanticColors.sidebarAccentFg} 60%, transparent)`, pointerEvents: 'none', }, },})
const sidebarTrackModes = stylex.create({ flyout: { top: '8px', bottom: '8px', },})
const sidebarFlyoutTrigger = stylex.create({ base: { display: 'none', },})
const sidebarFlyoutTriggerModes = stylex.create({ flyout: { display: 'block', position: 'absolute', '--sidebar-flyout-trigger-z-index': zIndices.layer3, zIndex: 'calc(var(--sidebar-flyout-trigger-z-index) - 1)', height: '100%', width: '8px', },})
const sidebarInset = stylex.create({ base: { height: '100%', minHeight: 0, display: 'flex', flexDirection: 'column', backgroundColor: semanticColors.bgContent, },})
const sidebarInsetVariants = stylex.create({ sidebar: {}, inset: { borderColor: semanticColors.sidebarBorder, borderWidth: '1px', borderRadius: semanticRadii.panel, marginBlock: `var(--inset-gap, ${spacing._2})`, marginEnd: `var(--inset-gap, ${spacing._2})`, overflow: 'clip', },})
export type SidebarVariant = keyof typeof sidebarRootVariants
export type SidebarMode = keyof typeof sidebarRootModes
export type SidebarSize = keyof typeof sidebarHeaderSizes
// defineSlotRecipe validates the shape at compile time: default values must// exist in the axis maps, and slot keys must be `styles` or a declared axis.// Axis maps are looked up by pluralized axis name (`mode` → `modes`).const sidebarSlotRecipe = defineSlotRecipe({ slots: { root: { styles: sidebarRoot, variants: sidebarRootVariants, modes: sidebarRootModes, }, backdrop: { styles: sidebarBackdrop }, trigger: { styles: sidebarTrigger }, header: { styles: sidebarHeader, sizes: sidebarHeaderSizes }, body: { styles: sidebarBody, sizes: sidebarBodySizes }, footer: { styles: sidebarFooter, sizes: sidebarFooterSizes }, group: { styles: sidebarGroup }, groupHeader: { styles: sidebarGroupHeader }, groupTitle: { styles: sidebarGroupTitle }, groupEndElement: { styles: sidebarGroupEndElement, sizes: sidebarGroupEndElementSizes, }, groupContent: { styles: sidebarGroupContent }, track: { styles: sidebarTrack, modes: sidebarTrackModes }, flyoutTrigger: { styles: sidebarFlyoutTrigger, modes: sidebarFlyoutTriggerModes, }, inset: { styles: sidebarInset, variants: sidebarInsetVariants }, }, defaultVariants: { variant: "sidebar", mode: "collapsible", size: "md" },});
/** * Sidebar — recipe and component colocated in one file. `<Sidebar.Root * variant mode size>` resolves the recipe once and every part inherits it * through context; each part composes via `render`. Width comes from the * `--sidebar-width` / `--sidebar-min-width` custom properties (280px/220px by * default) — override them through the `style` prop on Root. */const { withProvider, withContext } = createSlotRecipeContext(sidebarSlotRecipe);
export const Sidebar = { // slot names resolve mechanically from the recipe maps above Root: withProvider("aside", "root", "SidebarRoot"), Backdrop: withContext("div", "backdrop", "SidebarBackdrop"), Trigger: withContext("button", "trigger", "SidebarTrigger"), Header: withContext("div", "header", "SidebarHeader"), Body: withContext("div", "body", "SidebarBody"), Footer: withContext("div", "footer", "SidebarFooter"), Group: withContext("div", "group", "SidebarGroup"), GroupHeader: withContext("div", "groupHeader", "SidebarGroupHeader"), GroupTitle: withContext("div", "groupTitle", "SidebarGroupTitle"), GroupEndElement: withContext( "div", "groupEndElement", "SidebarGroupEndElement", ), GroupContent: withContext("div", "groupContent", "SidebarGroupContent"), Track: withContext("div", "track", "SidebarTrack"), FlyoutTrigger: withContext("button", "flyoutTrigger", "SidebarFlyoutTrigger"), Inset: withContext("div", "inset", "SidebarInset"),};