@edux-design/chrome
Responsive sidebar primitives (Sidebar, SidebarHeader, SidebarSection, SidebarItem, SidebarFooter) used to build the application chrome in Edux products. The components manage expand/collapse state, tooltips for the collapsed view, and consistent spacing/focus styles.
Installation
pnpm add @edux-design/chrome @edux-design/utils @edux-design/tooltips @edux-design/icons
# or
npm install @edux-design/chrome @edux-design/utils @edux-design/tooltips @edux-design/icons
Peer deps: react@^19.1.0, react-dom@^19.1.0.
Token contract
Sidebar navigation and profile rows use the declared gap-12 spacing token;
the header and footer use declared py-12; and the collapsed rail uses the
declared w-72 token. The expanded rail width (240px), header height
(60px), and logo wrapper width (40px) remain explicit component geometry
because the current theme has no matching declared dimensions. The footer
shadow pseudo-element keeps its empty content and 2px translate as local
implementation details.
No new semantic tokens are introduced. Consumers should import
@edux-design/tokens/theme so the shared utilities resolve consistently.
Usage
import {
Sidebar,
SidebarHeader,
SidebarSection,
SidebarItem,
SidebarFooter,
} from "@edux-design/chrome";
import { Users, Settings } from "@edux-design/icons";
export function AppChrome() {
return (
<Sidebar defaultExpanded>
<SidebarHeader logo={<span className="text-lg font-bold">Edux</span>} />
<SidebarSection title="Workspace">
<SidebarItem icon={<Users />} label="People" active />
<SidebarItem icon={<Settings />} label="Settings" />
</SidebarSection>
<SidebarFooter>
<button className="text-sm">Logout</button>
</SidebarFooter>
</Sidebar>
);
}
Features
- Internal context tracks
isExpandedand exposes a toggle button inSidebarHeader. - When collapsed, items show tooltips (via
@edux-design/tooltips) so labels remain discoverable. - Sections automatically hide their titles when collapsed to keep the column skinny.
- Footer content sticks to the bottom for account/profile controls.
Development
pnpm --filter @edux-design/chrome lint
pnpm --filter @edux-design/chrome check-types
pnpm --filter @edux-design/chrome build
Stories live at src/demos/Sidebar.stories.jsx and cover both expanded/collapsed states.
Notes
SidebarItemrenders a focusable list item (TooltipTrigger). PassonClickor wrap it with routing logic (e.g.,next/linkviaasChildsupport if needed).- The TODOs inside
Sidebar.jsx(avatar presets, padding fixes) are good starter issues if you need enhancements. - Because the layout relies on Tailwind tokens, keep overrides minimal to avoid drift between apps.
Docked panels
For non-modal inspector, search and editing docks, use the eight DockedPanel* primitives. They are separate from the navigation Sidebar* API. See the API and integration guide for imports, class/ref targets, scroll ownership, theming and the Payload-local export mapping. Storybook demos live under Chrome / DockedPanel.
Additional components
Semantic sidebar footer profiles.
Scroll-edge affordances
useScrollShadows, observeScrollShadows and
HORIZONTAL_SCROLL_SHADOW_SIDES observe an existing viewport without creating
viewport wrappers. They support mutable ref replacement, four physical edges, thresholds,
content/viewport resize, RTL and cleanup. The optional inset shadow uses the
existing --color-border-subtle semantic colour; host overlays remain decorative
and must pass pointers through. See the API, host migration inventory and
verification guide.
Controlled and embedded Sidebar
Sidebar preserves its existing expanded app defaults (240px expanded, existing
w-72 collapsed class and viewport height). Use expanded with
onExpandedChange(next: boolean) for host-controlled expansion, or
defaultExpanded for an initial uncontrolled value. Each enabled, uncancelled
toggle activation requests one change. Controlled updates and changes to default
props do not emit callbacks. The host may reject a controlled request.
The root forwards native aside attributes/styles/events and an HTMLElement
ref. Its default accessible name is still “Sidebar navigation”; supply your own
aria-label/aria-labelledby. Public data-sidebar, data-expanded,
data-layout and data-scroll-mode markers describe component state.
Use layout="embedded" to fill a bounded host without fixed app width, viewport
height, border or shadow. Embedded mode renders a named native nav;
app mode keeps the existing aside/complementary semantics. Native role and
accessible naming props may override these defaults to fit the host landmark.
scrollMode="internal" (default) gives the supported
data-sidebar-scroll region min-height-safe flex scrolling. scrollProps and
scrollRef expose that actual div's attributes, class/style, events and ref.
scrollMode="external" leaves overflow visible and lets an ancestor own scrolling.
The root/scroll region are stable across expansion and scroll-mode changes.
Changing layout switches the semantic root element and updates root/scroll refs;
choose a stable layout for long-lived interactive content.
Chrome's existing overflow observer supplies internal edge shadows; external
scroll mode attaches no internal overflow observer.
SidebarHeader accepts translated expandLabel/collapseLabel, native header
props/ref, and toggleProps/toggleRef for its button. Toggle type, expanded state
and control association remain component-owned. toggleProps.onClick or root
capture handlers can cancel the expansion request with preventDefault.
Collapsed items retain their accessible names. Collapse preserves button focus;
if it removes a focused logo/control, focus recovers to the toggle (or root).
onCollapseAutoFocus(event) may prevent that recovery to choose a host fallback.
Outside focus is preserved. The root's default tabIndex=-1 adds no tab stop.
Native hidden and inert root props support a host that keeps navigation
mounted while closed. hidden suppresses the root's flex display; the host
still owns moving focus to a visible control before hiding the navigation.
<Sidebar
expanded={expanded}
onExpandedChange={setExpanded}
layout="embedded"
aria-label="Book navigation"
ref={sidebarRef}
scrollRef={scrollRef}
scrollProps={{ role: 'region', 'aria-label': 'Book sections', tabIndex: 0 }}
>
<SidebarHeader expandLabel="Expand navigation" collapseLabel="Collapse navigation" />
<SidebarSection title="Sections">{/* existing SidebarItems */}</SidebarSection>
</Sidebar>
Routes, permissions, breakpoints, outer mobile visibility and focus return after host closure remain host policy. The mobile demo uses expansion requests and public refs to close/reopen its outer host, without English selectors, width inspection or synthetic clicks. Storybook's Sidebar contracts cover controlled, embedded internal/external scroll, mobile, translated labels and light/dark modes.