Sidebar
Fixed-position side panel, permanent on desktop and overlay on mobile.
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.
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. Reach for it directly for a right-side detail panel or a
custom shell whose layout you own. In mode="responsive" (the default) it is
permanent on desktop and slides in as a mobile overlay. In mode="collapsible" it toggles
at every width. On desktop there is no backdrop, so it never dims the page.
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
mode=collapsible is the second mode, and the one the Playground cannot reach. It drives open at every width: on desktop it animates the panel between full width and 0 (which is why the button toggles rather than opens), and on a phone it falls back to the same slide-in overlay as responsive. So one collapsible sidebar covers both the desktop collapse and the mobile overlay. Keep it mounted: wrapping it in a conditional block gets you a disappearing panel and no width animation.<!-- 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, and 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. The panel exposes --sidebar-effective-width (0px when closed, full
width when open), but it inherits only inside the sidebar, not to sibling content. To offset
the main content beside it, reach for SidebarLayout, which lifts the variable to the layout for you.
Reduced Motion
Slide and width transitions use CSS custom property durations that respect prefers-reduced-motion via the design token system.
05 API Reference
15 propsProp | 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';