# Sidebar

```tsx
import { Sidebar } from '@tangible/ui';
```

Interactive docs and examples: [Sidebar in Storybook](https://storybook-tangible-ui.pages.dev/?path=/docs/components-sidebar--docs).

## Sidebar props

| Prop | Type | Default | Description |
| - | - | - | - |
| `children` | `ReactNode` | | Sidebar content (Header, Nav, etc.). |
| `aria-label?` | `string` | | Accessible label for the sidebar landmark (static) or dialog (drawer). When a page has multiple sidebars, each must have a unique label so screen reader users can distinguish between landmarks (e.g. "Course navigation", "Lesson settings"). |
| `className?` | `string` | | Additional CSS class names. |
| `collapsed?` | `boolean` | | Collapsed state (controlled). Pair with `onCollapsedChange`. Leave undefined to let the sidebar own the state. |
| `collapsible?` | `boolean` | `false` | Opt into the collapsed rail: the sidebar can shrink to `--tui-sidebar-collapsed-width`, hiding Header, Nav and Footer while leaving any other child — a `Sidebar.Toggle`, an icon rail — visible. Deliberately absent from drawer mode: a drawer is already a show/hide overlay, and a rail inside one has nothing to sit beside. |
| `defaultCollapsed?` | `boolean` | `false` | Initial collapsed state when uncontrolled. |
| `drawer?` | `boolean` | `false` | Enable drawer mode (mobile overlay). |
| `onClose?` | `() => void` | | Callback to close the drawer. Required when `drawer={true}`. Called when ESC is pressed or backdrop is clicked. Omitting this in drawer mode means the user cannot dismiss the overlay, which is a WCAG 2.1.1 (Keyboard) failure. |
| `onCollapsedChange?` | `(collapsed: boolean) => void` | | Called with the next collapsed state whenever `Sidebar.Toggle` is pressed — in both controlled and uncontrolled mode. |
| `open?` | `boolean` | | Drawer open state. |
| `position?` | `"left" \| "right"` | `'left'` | Sidebar position. Affects border placement and drawer slide direction. |

### Sidebar.Header

| Prop | Type | Default | Description |
| - | - | - | - |
| `children` | `ReactNode` | | Header content (back button, title, progress, CTAs). |
| `className?` | `string` | | Additional CSS class names. |

### Sidebar.Footer

| Prop | Type | Default | Description |
| - | - | - | - |
| `children` | `ReactNode` | | Footer content (CTAs, user info, etc.). Sticks to the bottom of the sidebar. |
| `className?` | `string` | | Additional CSS class names. |

### Sidebar.Nav

| Prop | Type | Default | Description |
| - | - | - | - |
| `children` | `ReactNode` | | Nav content (typically Accordion with StepList). |
| `aria-label?` | `string` | | Accessible label for the nav landmark. Use this for the primary label since nav is the actionable region. |
| `aria-labelledby?` | `string` | | ID of a visible heading that labels the navigation. |
| `className?` | `string` | | Additional CSS class names. |

### Sidebar.Toggle

| Prop | Type | Default | Description |
| - | - | - | - |
| `className?` | `string` | | Additional CSS class names. |
| `disabled?` | `boolean` | | Disabled state. |
| `href?` | `never` | | |
| `labels?` | `SidebarToggleLabels` | | Accessible names for the two states. Defaults are English and are exported as `defaultSidebarToggleLabels`; a partial object overrides only the keys it names. |
| `loading?` | `boolean` | | Loading state: shows a spinner and blocks activation while keeping the button focusable (`aria-disabled`, not native `disabled`), so a keyboard user who triggered it keeps their place. Only `onClick` is guarded: put the action there, not in `onKeyDown`/`onPointerDown`. The start and end of loading aren't announced; confirm completion through your own status message (a Toast, say). |
| `loadingLabel?` | `string` | `` `${label}, loading` (English) `` | Accessible label override during loading state. For i18n support. |
| `shape?` | `"circle" \| "square"` | `'square'` | Button shape. Use `'circle'` for avatar-adjacent actions, FABs, or anywhere a round affordance is expected. |
| `showTooltip?` | `boolean` | `false` | Show the label as a tooltip on hover. Since icon-only buttons have no visible text, enabling this helps sighted users discover what the button does. Recommended for most standalone icon buttons. Can be omitted in toolbars where a shared Tooltip provider handles it, or where adjacent visible text already communicates the action. |
| `size?` | `"lg" \| "md" \| "sm" \| "xs"` | `'sm'` | Size of the button. `'xs'`: 24px — below WCAG 2.5.8 minimum target size (24×24 CSS px). Use only in dense, mouse-first UIs (toolbars, table rows) where touch is not expected. Consider pairing with adequate spacing or a larger touch target via padding/margin. `'sm'`: 32px (default); `'md'`: 40px; `'lg'`: 48px |
| `theme?` | `"danger" \| "info" \| "primary" \| "secondary" \| "success" \| "warning"` | `'secondary'` | Semantic color theme. Note: Defaults to 'secondary' (unlike Button which defaults to 'primary') because IconButtons are typically used for toolbar/utility actions. |
| `tooltipSide?` | `"bottom" \| "left" \| "right" \| "top"` | `'top'` | Tooltip placement side. |
| `variant?` | `"ghost" \| "outline" \| "solid"` | `'ghost'` | Visual style variant. `'solid'`: Filled background; `'outline'`: Border only, transparent background; `'ghost'`: No border or background, icon only — has no at-rest visual affordance. Best suited for toolbar groups, icon bars, or other contexts where surrounding UI makes the interactive nature obvious. |

Also accepts the standard attributes of `ButtonHTMLAttributes` (`data-*`, `aria-*`, event handlers), passed to the element it renders.

## Other exports

- `defaultSidebarToggleLabels`: `Required<SidebarToggleLabels>`

## Examples

- [Collapsible](https://storybook-tangible-ui.pages.dev/?path=/story/components-sidebar--collapsible): `collapsible` lets the sidebar shrink to a rail. Header, Nav and Footer go
  with it; anything else placed directly in the sidebar stays — which is why
  `Sidebar.Toggle` belongs there and not inside the Header.

- [Collapsed Rail](https://storybook-tangible-ui.pages.dev/?path=/story/components-sidebar--collapsed-rail): Starts as a rail. The toggle is the only thing left to bring it back.

- [Controlled Collapse](https://storybook-tangible-ui.pages.dev/?path=/story/components-sidebar--controlled-collapse): Controlled: the parent owns `collapsed` and the sidebar only reports intent
  through `onCollapsedChange`. Worth a story of its own because a consumer that
  forgets to write the value back gets a toggle that announces a state change
  and then does nothing — the sidebar deliberately will not move itself.

- [Collapsible (Right)](https://storybook-tangible-ui.pages.dev/?path=/story/components-sidebar--collapsible-right): On a right-hand sidebar the glyph mirrors: both directions collapse away from the content.

- [Drawer Open Interaction](https://storybook-tangible-ui.pages.dev/?path=/story/components-sidebar--drawer-open-interaction): Drawer opens with focus inside, role="dialog", and aria-modal.

- [Drawer Escape Interaction](https://storybook-tangible-ui.pages.dev/?path=/story/components-sidebar--drawer-escape-interaction): Escape closes the drawer and restores focus to the trigger.

- [Drawer Backdrop Interaction](https://storybook-tangible-ui.pages.dev/?path=/story/components-sidebar--drawer-backdrop-interaction): Clicking the backdrop closes the drawer.

- [Drawer Focus Trap Interaction](https://storybook-tangible-ui.pages.dev/?path=/story/components-sidebar--drawer-focus-trap-interaction): Focus trap cycles Tab within the drawer.

- [Drawer Scroll Lock Interaction](https://storybook-tangible-ui.pages.dev/?path=/story/components-sidebar--drawer-scroll-lock-interaction): Body scroll lock class is applied while the drawer is open.

- [Collapse Focus Recovery Interaction](https://storybook-tangible-ui.pages.dev/?path=/story/components-sidebar--collapse-focus-recovery-interaction): Regression cover for the focus hole in a *programmatic* collapse: when
  `collapsed` flips from outside while focus sits in a region the rail hides,
  the browser drops focus to `<body>` and the user's place in the page is
  gone silently. Clicking the toggle never hits this — focus is already on the
  one element that survives — so only an external control reproduces it.

  This has to live in a browser. jsdom has no layout, so `offsetParent` is
  null for every element and the guard that separates "hidden under the
  cursor" from "user clicked away" cannot be exercised there at all.

- [Collapse Interaction](https://storybook-tangible-ui.pages.dev/?path=/story/components-sidebar--collapse-interaction): The toggle survives its own collapse and keeps the sidebar reachable from
  the keyboard — the regions it hides leave the tab order with it.

Other stories: Static, With Header and Nav Slots, With Footer, Right Position, Drawer Mode, Drawer (Right Position), Drawer Keyboard Interaction, Course Viewer (Full Integration) ([interactive page](https://storybook-tangible-ui.pages.dev/?path=/docs/components-sidebar--docs)).

## Component API tokens

Read via fallback, never declared. Set one on an ancestor to restyle every instance under it.

- `--tui-sidebar-width`
- `--tui-sidebar-collapsed-width`
- `--tui-sidebar-bg`
- `--tui-sidebar-fg`
- `--tui-sidebar-border`
- `--tui-sidebar-divider`
- `--tui-sidebar-padding`
- `--tui-sidebar-z`

---

Interactive version: https://storybook-tangible-ui.pages.dev/?path=/docs/components-sidebar--docs
