Sidebar
Sidebar primitive — fixed-position panel, permanent on desktop and overlay on mobile. Use directly for detail panels and custom shells. For a standard application chrome (left rail + mobile hamburger), use SidebarLayout.
Overview
Looking for an app shell?
For the common pattern of a permanent left sidebar with a mobile hamburger and an offset main content area, prefer SidebarLayout. It wraps this primitive and resolves the CSS-variable scoping that otherwise leaves your main content underneath the sidebar.
Use the Sidebar primitive directly for right-side detail
panels, custom shells, or any sidebar that opens as an overlay on click.
Playground
<script lang="ts">
import { Sidebar, Button } from '@urbicon-ui/blocks';
let open = $state(false);
</script>
<Sidebar
bind:open
>
{#snippet header()}
<div class="flex items-center justify-between py-3">
<span class="text-text-primary font-semibold">Navigation</span>
<Button
variant="ghost"
intent="neutral"
size="xs"
onclick={() => (open = false)}
aria-label="Close sidebar"
>
<CloseIcon class="h-4 w-4" />
</Button>
</div>
{/snippet}
<nav class="space-y-1 p-4">
{#each ['Dashboard', 'Projects', 'Team', 'Settings'] as item (item)}
<button
class="text-text-secondary hover:bg-surface-hover hover:text-text-primary w-full rounded-lg px-3 py-2 text-left text-sm transition-colors"
onclick={() => (open = false)}
>
{item}
</button>
{/each}
</nav>
</Sidebar>01 When to use
Sidebar is an <aside> landmark for a side panel that is part of the page layout. Use it for persistent app navigation that slides in as an
overlay on mobile (mode="responsive") or width-collapses at all viewports (mode="collapsible"). On desktop there is no backdrop — the panel sits alongside the main content.
Pick a different overlay if you need:
- A ready-made app shell with mobile hamburger, header slot, and centered content column → SidebarLayout.
- A transient detail panel that pulls focus (backdrop + focus-trap, opens on click, closes after action) → Drawer.
- A floating panel anchored to a specific element (date picker, action menu, autocomplete) → Popover.
02 Examples
Right-Side Detail Panel
<Sidebar bind:open side="right" width="20rem">
{#snippet header()}
<span class="font-semibold">Item Details</span>
{/snippet}
<div class="space-y-4 p-4">
<div>
<dt class="text-xs text-text-tertiary">Name</dt>
<dd class="text-sm text-text-primary">Project Alpha</dd>
</div>
<div>
<dt class="text-xs text-text-tertiary">Status</dt>
<dd><Badge intent="success" size="sm">Active</Badge></dd>
</div>
</div>
</Sidebar>Collapsible App Navigation
responsive mode (the default) open only drives the mobile overlay — on a desktop width the panel is always there. In collapsible mode open drives it at every width: the panel stays mounted and animates its width to 0 instead of sliding away, which is why the button below toggles rather than opens. Keep it mounted — wrapping it in a conditional block gets you a disappearing panel in either mode and no animation in this one.<!-- The panel stays mounted; open animates its width, it does not unmount. -->
<Button variant="outlined" onclick={() => (navOpen = !navOpen)}>
{navOpen ? 'Collapse' : 'Expand'} navigation
</Button>
<Sidebar bind:open={navOpen} mode="collapsible" width="16rem">
{#snippet header()}
<span class="font-semibold">Acme</span>
{/snippet}
<nav class="space-y-1 p-3">
{#each ['Overview', 'Analytics', 'Settings'] as item (item)}
<a href="/{item.toLowerCase()}" class="block rounded-lg px-3 py-2 text-sm">{item}</a>
{/each}
</nav>
</Sidebar>03 Customization
Branded Sidebar
<Sidebar
bind:open
slotClasses={{
panel: 'bg-neutral-900 border-neutral-800',
header: 'border-neutral-800',
footer: 'border-neutral-800'
}}
>
{#snippet header()}
<span class="text-white font-semibold">Brand</span>
{/snippet}
<nav class="p-3">
<button class="text-neutral-400 hover:bg-neutral-800">Overview</button>
</nav>
</Sidebar>Sidebar also supports unstyled for a fully hand-rolled
shell. The branded dark panel above is natural preset material: register it under presets.Sidebar on BlocksProvider and apply it via preset — see Customization.
04 Accessibility
Semantic Landmark
Renders as an <aside> element, which screen
readers announce as a complementary landmark. When the sidebar is closed (mobile overlay
dismissed, or collapsible mode closed), it receives both aria-hidden="true" and inert — the panel keeps its children mounted, so removing
it from the accessibility tree without also removing it from the tab order would let a keyboard
user walk into a region their screen reader skips. Focus that was inside the panel returns to
whatever held it before the panel opened.
Keyboard
Escape closes the mobile overlay (configurable via closeOnEscape). Backdrop click dismiss is
configurable via closeOnBackdropClick.
Responsive Behavior
In responsive mode (default): on desktop (≥1024px)
the sidebar is always visible as a fixed panel — the open prop only controls the mobile overlay. Below 1024px
it slides in as an overlay with backdrop and body scroll lock.
In collapsible mode: the open prop controls the sidebar at all viewports. On
desktop it animates its width (no backdrop or scroll lock). On mobile it behaves as an
overlay like responsive mode. Use --sidebar-effective-width (0px when closed, full width
when open) to transition your main content offset.
Reduced Motion
Slide and width transitions use CSS custom property durations that respect prefers-reduced-motion via the design token system.
05 API Reference
Prop | Type | Default | Description | |
|---|---|---|---|---|
children | Snippet | — | Main scrollable content of the sidebar. | |
class | string | — | Additional CSS classes applied to the sidebar panel. | |
closeOnBackdropClick | boolean | true | Close the mobile overlay when clicking the backdrop. | |
closeOnEscape | boolean | true | Close the mobile overlay when pressing Escape. | |
footer | Snippet | — | Content rendered in the sidebar footer (below the scrollable area). | |
header | Snippet | — | Content rendered in the sidebar header (above the scrollable area). | |
mode | SidebarVariants['mode'] | 'responsive' | Controls sidebar behavior across viewports.
- responsive (default): permanently visible on desktop (≥1024px), slide-in overlay on mobile.
- collapsible: toggleable via open at all viewports — width animation on desktop, overlay on mobile. | |
onOpenChange | (open: boolean) => void | — | Fires when the open state changes (mobile overlay dismissed, or collapsible toggled). | |
open | boolean | — | Controls sidebar visibility. In responsive mode (default) this only affects the mobile overlay — on desktop the sidebar is always visible. In collapsible mode this controls visibility at all viewports. Supports bind:open.
While the panel is hidden it carries both aria-hidden and inert, so its
children leave the accessibility tree AND the tab order together — they stay
mounted, so one without the other would strand a keyboard user in an
invisible region. inert also suppresses pointer events, applied the moment
open flips rather than at the end of the transition. If focus was inside
the panel, it returns to whatever held it when the panel opened. | |
preset | string | — | Apply a named preset registered via <BlocksProvider presets={{ Sidebar: {...} }}>.
Prefer this over class overrides when the requested look falls outside the
semantic intent palette — presets keep hover/active/dark-mode logic coherent
and make the custom look reusable across the project. | |
side | SidebarVariants['side'] | 'left' | Which edge the sidebar attaches to. | |
slotClasses | Partial<Record<SidebarSlots, string>> | — | Per-slot class overrides merged with variant styles. | |
unstyled | boolean | — | Strip all default styles. Combine with slotClasses for custom appearance. | |
width | string | '16rem' | CSS width of the sidebar panel. Also exposed as --sidebar-width
(constant) and --sidebar-effective-width (0px when collapsed) CSS
variables on the <aside>. These inherit only inside the sidebar's
own subtree — for the main content offset use <SidebarLayout>. | |
...HTMLAttributes<HTMLElement> inherited | HTMLAttributes | — | HTML attributes (excluding: 'children') |
06 Types
Local type definitions used by this component.
Name | Kind | Category | Used by | Description | |
|---|---|---|---|---|---|
SidebarProps | interface | props | 0 | — | |
SidebarVariants | type | variant | 0 | — | |
SidebarSlots | type | variant | 0 | Slot names derived from the tv() config above — single source of truth for slotClasses. |
07 Installation
Import
import { Sidebar } from '@urbicon-ui/blocks';