Navigation
Sidebar
Cursor-following hover pill & gliding active indicator
detent-sidebar
Port status
- Catalog status
- implemented
- Origin
- upstream
- Phase
- 2
- Wave
- phase-2-navigation
- Group
- Navigation
7.0 KB gzip for the class module
Variants
The preview above is the expanded (default) panel; this one is icon-only (collapsed). Both have nested
sections, badges and a ⌘B hint. The active entry carries aria-current="page", a parent with
children carries aria-expanded, and in icon-only mode each entry keeps an aria-label plus a
tooltip. Below 768px the same panel becomes a modal drawer (role="dialog", aria-modal) that
Escape and the backdrop close and that can be dragged away.
Recorded states
hoverrecorded state; no setterclosedrecorded state; no setterexpandedrecorded state; no settercollapsedrecorded state; no setterdraggingrecorded state; no setteractiverecorded state; no setter
Usage
Import
import '/components/sidebar/define.js';
// or register it yourself
import { DetentSidebar } from '/components/sidebar/sidebar.js';
Attributes
active-idcollapseddefault-collapsedmobile-open
Properties
sectionsactiveIdcollapseddefaultCollapsedmobileOpenopenMobilecloseMobileonActiveChangeonCollapsedChangeonMobileOpenChange
Slots
footerheader
Events
detent-active-change— { id: string }detent-collapsed-change— { collapsed: boolean }detent-mobile-open-change— { open: boolean }
Example
Runnable example: site/examples/sidebar.ts. The snippet below is generated from the catalogued attributes.
<detent-sidebar
active-id
collapsed
default-collapsed
mobile-open
></detent-sidebar>
Property-only values
const element = document.querySelector('detent-sidebar');
if (element !== null) {
// sections: SidebarSection[]
element.sections = [];
// onActiveChange: (id: string) => void
element.onActiveChange = () => {};
// onCollapsedChange: (collapsed: boolean) => void
element.onCollapsedChange = () => {};
// onMobileOpenChange: (open: boolean) => void
element.onMobileOpenChange = () => {};
}
Source
The pinned upstream source for this row:
src/components/ui/sidebar.tsxsha256 647633cb886b…
Native files
The native module and its artifacts:
src/components/sidebar/sidebar.tssrc/components/sidebar/styles.csssrc/components/sidebar/define.tsdocs/components/sidebar.mdsite/examples/sidebar.tstests/browser/sidebar.spec.tsevidence/components/sidebar.md
API
Boolean attributes follow HTML presence semantics: present means true, absent means false, and disabled="false" still means disabled.
Attributes
| Attribute | Property | Type | Default | Notes |
|---|---|---|---|---|
active-id | activeId | string | undefined | Kebab-case attribute; camelCase property. |
collapsed | collapsed | boolean | undefined | Presence semantics: present means true, absent means false. |
default-collapsed | defaultCollapsed | boolean | false | Presence semantics: present means true, absent means false. |
mobile-open | mobileOpen | boolean | undefined | Presence semantics: present means true, absent means false. |
Properties
| Property | Type | Default | Notes |
|---|---|---|---|
sections | SidebarSection[] | undefined | Property-only: assign from script; not accepted as an attribute. |
activeId | string | undefined | Property-only: assign from script; not accepted as an attribute. |
collapsed | boolean | undefined | Property-only: assign from script; not accepted as an attribute. |
defaultCollapsed | boolean | false | Property-only: assign from script; not accepted as an attribute. |
mobileOpen | boolean | undefined | Property-only: assign from script; not accepted as an attribute. |
openMobile | — | — | Recorded in the catalog without a top-level type; pinned from source when the row is ported. |
closeMobile | — | — | Recorded in the catalog without a top-level type; pinned from source when the row is ported. |
onActiveChange | (id: string) => void | undefined | Property-only: assign from script; not accepted as an attribute. |
onCollapsedChange | (collapsed: boolean) => void | undefined | Property-only: assign from script; not accepted as an attribute. |
onMobileOpenChange | (open: boolean) => void | undefined | Property-only: assign from script; not accepted as an attribute. |
Events
| Event | Upstream prop | Payload type | Notes |
|---|---|---|---|
detent-active-change | onActiveChange | (id: string) => void | Composed, bubbles; fires only for a user action. |
detent-collapsed-change | onCollapsedChange | (collapsed: boolean) => void | Composed, bubbles; fires only for a user action. |
detent-mobile-open-change | onMobileOpenChange | (open: boolean) => void | Composed, bubbles; fires only for a user action. |
Slots
| Slot | Notes |
|---|---|
footer | Consumer content; rendered through a slot, never re-created. |
header | Consumer content; rendered through a slot, never re-created. |
CSS customisation
- React `children`/`ReactNode` props become DOM slots; caller content stays light DOM.
- `className` prop is replaced by documented CSS parts/custom properties; no Tailwind in consumers.
- React callback props become typed composed CustomEvents; programmatic state assignment emits none.
- Framer Motion springs/AnimatePresence ported to CSS transitions, WAAPI or the shared rAF spring helper.
- `lucide-react` icon components replaced with local SVG assets (ISC notice retained).
- React state/hooks replaced by element-owned state with pre-upgrade property preservation.
- One panel serves both layouts (static at 768px and above, off-canvas modal drawer below) instead of two React panels, so the `header`/`footer` slots exist exactly once.
- `useSidebar` becomes a live reader `useSidebar(element)` and `SidebarTrigger` becomes a factory returning a real button.
- `SidebarItem.icon` is a text glyph; icon-only mode uses `visibility: hidden` plus `aria-label` (upstream `opacity: 0` kept the label in the accessibility tree).
- `openMobile()`/`closeMobile()` are added as user-intent helpers so a trigger can emit; drag uses pointer events with an -80px threshold and a `dragging` attribute (no dragElastic, no 420/36/0.8 springs).
What it does
A collapsible application sidebar with a cursor-following hover pill, a gliding active pill, nested sections, an icon-only mode with tooltips, ⌘B/Ctrl+B toggle and a modal, draggable mobile drawer.
Upstream behaviour
- Hover pill tracks the cursor; active pill glides between items with a shared layout spring
- Nested items with animated expand/collapse and active dot indicator
- Icon-only collapse mode with floating tooltips, toggled via ⌘B / Ctrl+B
- Mobile drawer under md with drag-to-dismiss and backdrop blur
- Header and footer slots adapt to the collapsed state via useSidebar()
Differences from upstream
- React `children`/`ReactNode` props become DOM slots; caller content stays light DOM.
- `className` prop is replaced by documented CSS parts/custom properties; no Tailwind in consumers.
- React callback props become typed composed CustomEvents; programmatic state assignment emits none.
- Framer Motion springs/AnimatePresence ported to CSS transitions, WAAPI or the shared rAF spring helper.
- `lucide-react` icon components replaced with local SVG assets (ISC notice retained).
- React state/hooks replaced by element-owned state with pre-upgrade property preservation.
- One panel serves both layouts (static at 768px and above, off-canvas modal drawer below) instead of two React panels, so the `header`/`footer` slots exist exactly once.
- `useSidebar` becomes a live reader `useSidebar(element)` and `SidebarTrigger` becomes a factory returning a real button.
- `SidebarItem.icon` is a text glyph; icon-only mode uses `visibility: hidden` plus `aria-label` (upstream `opacity: 0` kept the label in the accessibility tree).
- `openMobile()`/`closeMobile()` are added as user-intent helpers so a trigger can emit; drag uses pointer events with an -80px threshold and a `dragging` attribute (no dragElastic, no 420/36/0.8 springs).