Sidebar
Overview
The Sidebar is a layout-and-navigation component for application shells: a fixed-width, full-height panel that holds branding, grouped navigation menus, and a footer. It is composed of fifteen recipe parts that work together — useSidebarRecipe() for the panel surface, region recipes (header, content, footer, group, inset), list recipes (menu, menu-sub), the interactive menu-button / menu-sub-button, and accent parts (group-label, separator, menu-action, menu-badge, group-action). Each composable creates a fully configured recipe with compound variants that handle the color-style combinations automatically.
The Sidebar recipes integrate directly with the default design tokens preset and generate type-safe utility classes at build time with zero runtime CSS.
collapsed state (or the .-collapsed class) is all the recipe needs to switch to the icon rail.Why use the Sidebar recipe?
The Sidebar recipe helps you:
- Ship a full shell fast: Fifteen coordinated parts give you panel, header, scrollable content, grouped menus, sub-menus, footer, and an icon rail out of the box.
- Stay internally consistent: Every part shares the same
sizeaxis and the samelight/dark/neutralcolor system, so the whole surface stays coherent. - Get a collapsible rail for free: Set
collapsedand the panel narrows to an icon rail — labels, sub-menus, badges, and actions hide; icons center — with no behavioral code. - Customize without forking: Override base styles, default variants, the panel widths, or filter out options you don't need — all through the options API.
- Stay type-safe: Full TypeScript support means your editor catches invalid color, variant, or size values at compile time.
Usage
Register the recipes
Add the Sidebar recipes to a local Styleframe instance. The global styleframe.config.ts provides design tokens and utilities, while the component-level file registers the recipes themselves:
import { styleframe } from 'virtual:styleframe';
import {
useSidebarRecipe,
useSidebarHeaderRecipe,
useSidebarContentRecipe,
useSidebarFooterRecipe,
useSidebarGroupRecipe,
useSidebarGroupLabelRecipe,
useSidebarMenuRecipe,
useSidebarMenuButtonRecipe,
useSidebarMenuSubRecipe,
useSidebarMenuSubButtonRecipe,
useSidebarSeparatorRecipe,
useSidebarMenuActionRecipe,
useSidebarMenuBadgeRecipe,
useSidebarGroupActionRecipe,
useSidebarInsetRecipe,
} from '@styleframe/theme';
const s = styleframe();
const sidebar = useSidebarRecipe(s);
const sidebarHeader = useSidebarHeaderRecipe(s);
const sidebarContent = useSidebarContentRecipe(s);
const sidebarFooter = useSidebarFooterRecipe(s);
const sidebarGroup = useSidebarGroupRecipe(s);
const sidebarGroupLabel = useSidebarGroupLabelRecipe(s);
const sidebarMenu = useSidebarMenuRecipe(s);
const sidebarMenuButton = useSidebarMenuButtonRecipe(s);
const sidebarMenuSub = useSidebarMenuSubRecipe(s);
const sidebarMenuSubButton = useSidebarMenuSubButtonRecipe(s);
const sidebarSeparator = useSidebarSeparatorRecipe(s);
const sidebarMenuAction = useSidebarMenuActionRecipe(s);
const sidebarMenuBadge = useSidebarMenuBadgeRecipe(s);
const sidebarGroupAction = useSidebarGroupActionRecipe(s);
const sidebarInset = useSidebarInsetRecipe(s);
export default s;
Build the component
Import the runtime functions from the virtual module and compose the parts. The menu button wraps its label in a sidebar-menu-button-label span so the collapsed rail can hide it:
import {
sidebar,
sidebarHeader,
sidebarContent,
sidebarFooter,
sidebarGroup,
sidebarGroupLabel,
sidebarMenu,
sidebarMenuButton,
} from "virtual:styleframe";
export function Sidebar({ collapsed = false }: { collapsed?: boolean }) {
return (
<aside className={sidebar({ collapsed: collapsed ? "true" : "false" })}>
<header className={sidebarHeader({})}>Acme Inc</header>
<div className={sidebarContent({})}>
<section className={sidebarGroup({})}>
<div className={sidebarGroupLabel({})}>Platform</div>
<ul className={sidebarMenu({})}>
<li>
<button className={sidebarMenuButton({ active: "true" })} aria-current="page">
<span className="sidebar-menu-button-label">Dashboard</span>
</button>
</li>
<li>
<button className={sidebarMenuButton({})}>
<span className="sidebar-menu-button-label">Analytics</span>
</button>
</li>
</ul>
</section>
</div>
<footer className={sidebarFooter({})}>
<button className={sidebarMenuButton({})}>
<span className="sidebar-menu-button-label">Account</span>
</button>
</footer>
</aside>
);
}
See it in action
Anatomy
The Sidebar is composed of fifteen independent recipes. Compose the parts you need — only the panel surface and the menu button are required for a basic sidebar.
| Part | Recipe | Axes | Role |
|---|---|---|---|
| Panel | useSidebarRecipe() | color, variant, size, collapsed | Fixed-width, full-height surface (<aside>) |
| Header | useSidebarHeaderRecipe() | size | Sticky top region for branding |
| Content | useSidebarContentRecipe() | size | Scrollable region between header and footer |
| Footer | useSidebarFooterRecipe() | size | Bottom region pinned with margin-top: auto |
| Group | useSidebarGroupRecipe() | size | Section wrapper for a label + menu |
| Group label | useSidebarGroupLabelRecipe() | color, size | Muted heading for a group |
| Group action | useSidebarGroupActionRecipe() | size | Icon button on a group header |
| Menu | useSidebarMenuRecipe() | size | Reset <ul> of menu items |
| Menu button | useSidebarMenuButtonRecipe() | color, variant, size, active, disabled | Full-width interactive nav item |
| Menu action | useSidebarMenuActionRecipe() | size | Icon button on a menu item (hover affordance) |
| Menu badge | useSidebarMenuBadgeRecipe() | size | Count/status chip on a menu item |
| Sub-menu | useSidebarMenuSubRecipe() | size | Nested <ul> with an indented guide line |
| Sub-button | useSidebarMenuSubButtonRecipe() | color, variant, size, active, disabled | Nested nav item |
| Separator | useSidebarSeparatorRecipe() | color | 1px divider between sections |
| Inset | useSidebarInsetRecipe() | size | Main content area rendered beside the panel |
<aside class="sidebar(...)">
<header class="sidebarHeader(...)">…</header>
<div class="sidebarContent(...)">
<section class="sidebarGroup(...)">
<div class="sidebarGroupLabel(...)">Platform</div>
<ul class="sidebarMenu(...)">
<li><button class="sidebarMenuButton({ active: 'true' })">…</button></li>
</ul>
</section>
</div>
<footer class="sidebarFooter(...)">…</footer>
</aside>
menu-button, menu-sub-button) carry the color, variant, active, and disabled axes; the structural regions only take size. Pass size consistently across the panel and its parts so spacing stays coordinated.menu-badge, menu-action, and group-action anchor themselves absolutely to the trailing edge of their row, so their container — the menu item (<li>) or the group-label header — must be position: relative.Colors
The panel surface and the interactive parts support 3 colors: light, dark, and neutral. Like the Card and Nav recipes, the Sidebar uses neutral-spectrum colors designed for structural surfaces rather than status communication. The neutral color adapts automatically — light surface in light mode, dark surface in dark mode — making it the safest default.
| Color | Behavior | Use Case |
|---|---|---|
light | Stays light across themes | A sidebar that must remain light in dark mode |
dark | Stays dark across themes | A dark sidebar against a light app |
neutral | Adaptive (light ↔ dark) | Default — follows the current color scheme |
Variants
Three surface styles control how the panel reads against the page. Each is combined with the selected color through compound variants, so you always get the correct background, text, and border for your chosen color.
| Variant | Appearance |
|---|---|
solid | Filled surface matching the page elevation — the default |
soft | Faintly tinted surface, no border |
subtle | Tinted surface with a border |
solid (the default) suits shells where the panel already sits on a distinct background. Reach for soft or subtle when the panel shares the page background and needs a tint or border to stand apart.The interactive menu buttons use their own two styles: ghost (transparent at rest, hover reveals a surface — the default) and subtle (a persistent faint fill with a border).
Sizes
Three sizes from sm to lg scale the font size, padding, and gap of every part. Pass size to the panel and to each part you render.
Collapsed
Set collapsed (or add the .-collapsed class to the panel) and the sidebar narrows to an icon rail: text labels, sub-menus, badges, and actions are hidden, and the menu-button icons center. The expanded and collapsed widths are driven by two component variables you can override:
| Variable | Default | Role |
|---|---|---|
@sidebar.width | 16rem | Expanded panel width |
@sidebar.width-icon | 3rem | Collapsed icon-rail width |
// Collapsed panel
sidebar({ color: "neutral", variant: "subtle", size: "md", collapsed: "true" })
collapsed from your own state (a button, a keyboard shortcut, a persisted cookie) — the recipe reacts to the class, nothing more.States
Active
The menu button and sub-button include an active boolean variant that fills the item with a per-color highlight to mark the current page. Pair it with aria-current="page".
sidebarMenuButton({ color: "neutral", variant: "ghost", size: "md", active: "true" })
Disabled
The menu button and sub-button include a disabled boolean variant (and a :disabled pseudo-class for native <button> elements) that dims the item to 0.5 opacity, sets cursor: not-allowed, and removes pointer events.
sidebarMenuButton({ color: "neutral", variant: "ghost", size: "md", disabled: "true" })
Accessibility
- Use semantic landmarks. Render the panel as
<aside>(or<nav>) and give it anaria-labelsuch as "Main navigation". Render menu items that navigate as<a>and items that trigger actions as<button>. - Mark the current page. Pass
active: "true"alongsidearia-current="page"so screen readers announce the current location (WCAG 2.4.8). - Keep labels reachable when collapsed. The collapsed rail hides label text visually with
display: none, which also hides it from assistive tech. Provide an accessible name on the button (e.g. anaria-label, or a tooltip) so icon-only items stay usable. - Focus visibility. The menu button includes a
:focus-visibleoutline (2px solid, primary color, 2px offset) that appears only during keyboard navigation (WCAG 2.4.7). - Label icon buttons. Give
menu-actionandgroup-actionbuttons anaria-label— their content is an icon with no text.
Customization
Overriding defaults
Each composable accepts an optional second argument that is deep-merged with the defaults. Override the panel widths, base styles, or default variants:
import { styleframe } from 'virtual:styleframe';
import { useSidebarRecipe } from '@styleframe/theme';
const s = styleframe();
const sidebar = useSidebarRecipe(s, {
base: {
// Wider expanded panel
width: '18rem',
},
defaultVariants: {
color: 'neutral',
variant: 'soft',
size: 'lg',
},
});
// Or override the component variables for the collapsed rail width:
s.variable('sidebar.width-icon', '3.5rem', { default: true });
export default s;
Filtering variants
Limit which values are generated to reduce output CSS:
import { styleframe } from 'virtual:styleframe';
import { useSidebarRecipe, useSidebarMenuButtonRecipe } from '@styleframe/theme';
const s = styleframe();
// Only the neutral, subtle panel
const sidebar = useSidebarRecipe(s, {
filter: { color: ['neutral'], variant: ['subtle'] },
});
// Only ghost menu buttons
const sidebarMenuButton = useSidebarMenuButtonRecipe(s, {
filter: { color: ['neutral'], variant: ['ghost'] },
});
export default s;
API Reference
useSidebarRecipe(s, options?)
Creates the panel surface recipe.
Variants:
| Variant | Options | Default |
|---|---|---|
color | light, dark, neutral | neutral |
variant | solid, soft, subtle | solid |
size | sm, md, lg | md |
collapsed | true, false | false |
Component variables: @sidebar.width (16rem), @sidebar.width-icon (3rem).
useSidebarMenuButtonRecipe(s, options?)
Creates the full-width interactive nav item. useSidebarMenuSubButtonRecipe shares the same surface.
Variants:
| Variant | Options | Default |
|---|---|---|
color | light, dark, neutral | neutral |
variant | ghost, subtle | ghost |
size | sm, md, lg | md |
active | true, false | false |
disabled | true, false | false |
Structural and accent recipes
useSidebarHeaderRecipe, useSidebarContentRecipe, useSidebarFooterRecipe, useSidebarGroupRecipe, useSidebarMenuRecipe, useSidebarMenuSubRecipe, useSidebarInsetRecipe, useSidebarMenuActionRecipe, useSidebarMenuBadgeRecipe, and useSidebarGroupActionRecipe each expose a single size axis (default md). useSidebarGroupLabelRecipe adds color (default neutral); useSidebarSeparatorRecipe exposes color only (default neutral).
All composables accept the same options shape — base, variants, defaultVariants, compoundVariants, and filter.
Best Practices
- Pass
sizeconsistently across parts. The panel, regions, and items each take their ownsize; mismatched sizes create visual inconsistency. - Use
neutralfor general-purpose sidebars. It adapts to light and dark mode automatically. - Pick the surface for your layout.
solid(the default) suits a panel on its own background; switch tosoftorsubtlewhen the panel shares the page background and needs a tint or border to stand apart. - Always mark the current page. Pair
active: "true"witharia-current="page". - Give icon-only items an accessible name. When the rail collapses, label text is hidden; an
aria-labelor tooltip keeps items usable. - Wire collapse to your own state. The recipe ships the CSS; you control when
collapsedflips.
FAQ
collapsed axis (the .-collapsed class). Toggling it — via a button, a keyboard shortcut, a mobile drawer, or persisted state — is your application's responsibility.collapsed: "true", the recipe narrows it to @sidebar.width-icon and, through descendant selectors, hides menu-button labels, group labels, sub-menus, badges, and actions while centering the menu-button icons. Wrap each button's text in <span class="sidebar-menu-button-label"> so it can be hidden.solid, which suits a sidebar that sits on its own background. If the panel shares the page background and would otherwise blend in, switch to soft (a faint tint) or subtle (a tint plus a border) so it reads as a distinct surface.useSidebarInsetRecipe() styles the main content area that sits beside the panel: a growing flex column on the page background. Place the panel and the inset in a flex row to build the full shell.Pagination
A navigation component for moving between pages of content. Supports horizontal and vertical orientations, three colors, six visual styles, three sizes, and active/disabled states through a three-part recipe system.
Tabs
An accessible tabbed interface for organizing content into selectable panels. Composed of a root wrapper, a tab list, individual triggers, and content panels — with line, pill, and soft styles, light/dark/neutral surfaces, three sizes, and horizontal or vertical orientation.