Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .storybook/components/Roadmap/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -362,4 +362,10 @@ export const rows: Rows = [
stage: '🔵 experimental',
planned: 'Q2 2026',
},
{
component: 'Sidebar',
status: '✅ Done',
stage: '🔵 experimental',
planned: 'Q3 2026',
},
];
98 changes: 98 additions & 0 deletions packages/components/src/components/Sidebar/Sidebar.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import {
Meta,
Story,
Props,
Status,
} from '../../../../../.storybook/components';

import * as Stories from './Sidebar.stories';

<Meta of={Stories} />

# Sidebar

<Status variant="experimental" />

Sidebar is a low-level layout box for a side area that toggles between an open and a closed state,
animating its inline size between the two.

It is not an overlay and does not position itself. Sidebar only controls its inline size and
open/closed state.

## Import

```tsx
import { Sidebar } from '@koobiq/react-components';
```

## Usage

Pass a function as `children` to render content based on the current state.

```tsx
<Sidebar>
{({ isOpen, toggle }) => <Content isOpen={isOpen} onToggle={toggle} />}
</Sidebar>
```

<Story of={Stories.Base} />

## Props

<Props of={Stories.Base} />

## Open state

The Sidebar is closed by default. Use `defaultOpen` for an uncontrolled sidebar, or pass `isOpen` and
`onOpenChange` to control it.

<Story of={Stories.Controlled} />

## Sizing

The `size` and `closedSize` props accept numbers in pixels or CSS inline-size values.

```tsx
<Sidebar size={320} closedSize={56} />
<Sidebar size="50%" closedSize="25%" />
<Sidebar size="clamp(240px, 50%, 320px)" closedSize={32} />
```

<Story of={Stories.Size} />

## Placement

The `placement` anchors the content to its edge while the inline size animates, so a right-hand sidebar
collapses toward the right. It does not position the box.

<Story of={Stories.Placement} />

## Keyboard

Press `[` to toggle a `start` sidebar or `]` to toggle an `end` sidebar. Set
`keyboardShortcut` to use another key combination, or pass `null` to disable it.

```tsx
<Sidebar keyboardShortcut={{ code: 'KeyB' }} />
<Sidebar keyboardShortcut={{ code: 'KeyO', metaKey: true }} />
<Sidebar keyboardShortcut={null} />
```

<Story of={Stories.Keyboard} />

## Accessibility

A Sidebar has no ARIA role by default:

- Use `as="nav"` or `as="aside"` and add an accessible name.
- Mark the toggle control you render with `aria-expanded` and `aria-controls` pointing at the
sidebar's `id`.

## CSS variables

| Variable | Default | Purpose |
| ------------------------------ | ------- | ---------------------------------------------------------- |
| `--kbq-sidebar-size` | `240px` | Inline size while open (also settable via `size`). |
| `--kbq-sidebar-closed-size` | `32px` | Inline size while closed (also settable via `closedSize`). |
| `--kbq-sidebar-open-duration` | `200ms` | Duration of the expand animation. |
| `--kbq-sidebar-close-duration` | `100ms` | Duration of the collapse animation. |
33 changes: 33 additions & 0 deletions packages/components/src/components/Sidebar/Sidebar.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
.base {
--kbq-sidebar-size: 240px;
--kbq-sidebar-closed-size: 32px;
--kbq-sidebar-open-duration: 200ms;
--kbq-sidebar-close-duration: 100ms;

display: flex;
overflow: hidden;
box-sizing: border-box;
block-size: 100%;
interpolate-size: allow-keywords; /* Kept for future intrinsic-size transition support. */
transition: inline-size var(--kbq-transition-slow);
transition-duration: var(--kbq-sidebar-close-duration);
}

.base[data-placement='end'] {
justify-content: flex-end;
}

/* animation */
.base[data-transition='entering'] {
transition-duration: var(--kbq-sidebar-open-duration);
}

.base[data-transition='entering'],
.base[data-transition='entered'] {
inline-size: var(--kbq-sidebar-size);
}

.base[data-transition='exiting'],
.base[data-transition='exited'] {
inline-size: var(--kbq-sidebar-closed-size);
}
Loading
Loading