Skip to main content
Urbicon UI

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

Side
<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

The primary use case for the Sidebar primitive: a detail or property panel that slides in from an edge as an overlay on click.
<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

slotClasses paints the panel, header and footer slots for a dark branded shell. The active nav item is your own markup, since Sidebar styles the frame, not its contents.
<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 props
15 props
Prop
Type
Default
Description

06 Types

Local type definitions used by this component.

3 types
Name
Kind
Category
Used by
Description

07 Installation

Import

import { Sidebar } from '@urbicon-ui/blocks';