Styleframe Logo
Navigation

Sidebar

A persistent navigation surface for application shells. Composed of fifteen coordinated recipes — panel, header, content, footer, groups, menus, interactive menu buttons, sub-menus, separators, actions, and badges — with three colors, three surface styles, three sizes, and a collapsible icon rail.

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.

Styling, not behavior. These recipes ship the visual structure and the CSS for the collapsed icon rail. State and behavior — the open/close toggle, keyboard shortcut, mobile drawer, and persistence — are left to your application. Toggling the 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 size axis and the same light / dark / neutral color system, so the whole surface stays coherent.
  • Get a collapsible rail for free: Set collapsed and 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:

src/components/sidebar.styleframe.ts
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:

src/components/Sidebar.tsx
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.

PartRecipeAxesRole
PaneluseSidebarRecipe()color, variant, size, collapsedFixed-width, full-height surface (<aside>)
HeaderuseSidebarHeaderRecipe()sizeSticky top region for branding
ContentuseSidebarContentRecipe()sizeScrollable region between header and footer
FooteruseSidebarFooterRecipe()sizeBottom region pinned with margin-top: auto
GroupuseSidebarGroupRecipe()sizeSection wrapper for a label + menu
Group labeluseSidebarGroupLabelRecipe()color, sizeMuted heading for a group
Group actionuseSidebarGroupActionRecipe()sizeIcon button on a group header
MenuuseSidebarMenuRecipe()sizeReset <ul> of menu items
Menu buttonuseSidebarMenuButtonRecipe()color, variant, size, active, disabledFull-width interactive nav item
Menu actionuseSidebarMenuActionRecipe()sizeIcon button on a menu item (hover affordance)
Menu badgeuseSidebarMenuBadgeRecipe()sizeCount/status chip on a menu item
Sub-menuuseSidebarMenuSubRecipe()sizeNested <ul> with an indented guide line
Sub-buttonuseSidebarMenuSubButtonRecipe()color, variant, size, active, disabledNested nav item
SeparatoruseSidebarSeparatorRecipe()color1px divider between sections
InsetuseSidebarInsetRecipe()sizeMain 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>
Pro tip: The interactive parts (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.
Positioned affordances: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.

ColorBehaviorUse Case
lightStays light across themesA sidebar that must remain light in dark mode
darkStays dark across themesA dark sidebar against a light app
neutralAdaptive (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.

VariantAppearance
solidFilled surface matching the page elevation — the default
softFaintly tinted surface, no border
subtleTinted surface with a border
Choosing a surface: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:

VariableDefaultRole
@sidebar.width16remExpanded panel width
@sidebar.width-icon3remCollapsed icon-rail width
// Collapsed panel
sidebar({ color: "neutral", variant: "subtle", size: "md", collapsed: "true" })
Behavior is yours to wire. The recipe only ships the collapsed CSS. Toggle 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 an aria-label such as "Main navigation". Render menu items that navigate as <a> and items that trigger actions as <button>.
  • Mark the current page. Pass active: "true" alongside aria-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. an aria-label, or a tooltip) so icon-only items stay usable.
  • Focus visibility. The menu button includes a :focus-visible outline (2px solid, primary color, 2px offset) that appears only during keyboard navigation (WCAG 2.4.7).
  • Label icon buttons. Give menu-action and group-action buttons an aria-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:

src/components/sidebar.styleframe.ts
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:

src/components/sidebar.styleframe.ts
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;
Good to know: Filtering also removes compound variants and adjusts default variants that reference filtered-out values, so your recipe stays consistent.

API Reference

useSidebarRecipe(s, options?)

Creates the panel surface recipe.

Variants:

VariantOptionsDefault
colorlight, dark, neutralneutral
variantsolid, soft, subtlesolid
sizesm, md, lgmd
collapsedtrue, falsefalse

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:

VariantOptionsDefault
colorlight, dark, neutralneutral
variantghost, subtleghost
sizesm, md, lgmd
activetrue, falsefalse
disabledtrue, falsefalse

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.

Learn more about recipes →

Best Practices

  • Pass size consistently across parts. The panel, regions, and items each take their own size; mismatched sizes create visual inconsistency.
  • Use neutral for 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 to soft or subtle when the panel shares the page background and needs a tint or border to stand apart.
  • Always mark the current page. Pair active: "true" with aria-current="page".
  • Give icon-only items an accessible name. When the rail collapses, label text is hidden; an aria-label or tooltip keeps items usable.
  • Wire collapse to your own state. The recipe ships the CSS; you control when collapsed flips.

FAQ