A headless React component library for building resizable, collapsible side panel layouts, which adjusts itself based on available space.
Headless/Unstyled - Full control over styling, state is exposed via data attributes (
data-expanded,data-closed-style, etc.), CSS custom properties (--sidepane-width), and components use render propsSmart and responsive - Guarantees a minimum width for your main content, auto-closes the opposite pane when space is tight, and gracefully handles viewport resizing. Panes can still be hover-viewed even when there's no room to pin them.
Manually resizable panels - Drag to resize with configurable min/max widths
Hidden or compact when closed - Closed panes can be fully hidden or display a slim compact bar (useful for icon-based navigation that's always accessible)
Persistent state - Pluggable persistence adapters (localStorage, cookies, or custom). Sensible defaults work out of the box, but everything is configurable or replaceable.
TypeScript - Full type definitions included
Zero dependencies - Only React as a peer dependency
npm install @didmar/react-sidepanes
# or
pnpm add @didmar/react-sidepanes
# or
yarn add @didmar/react-sidepanesimport{SidepanesProvider,Sidepane,SidepaneToggle,CentralPane,EdgeHoverSensor}from'@didmar/react-sidepanes'functionApp(){return(<SidepanesProvider><divclassName="app-layout">{/* Toggle buttons */}<SidepaneToggleanchor="left">{({ isOpen, onClick, ariaLabel })=>(<buttononClick={onClick}aria-label={ariaLabel}>{isOpen ? '←' : '→'}</button>)}</SidepaneToggle>{/* Edge hover sensors */}<EdgeHoverSensoranchor="left"/><EdgeHoverSensoranchor="right"/>{/* Left sidepane */}<Sidepaneanchor="left"closedStyle="compact"><nav>Navigation content</nav></Sidepane>{/* Central content */}<CentralPane>
Main content
</CentralPane>{/* Right sidepane */}<Sidepaneanchor="right"closedStyle="hidden"><aside>Details panel</aside></Sidepane></div></SidepanesProvider>)}Wraps your application and provides sidepane state management.
<SidepanesProviderconfig={{persistence: localStorageAdapter,// or cookieAdapter, noopAdapter, or customdefaultWidth: 320,// Default width for both panes (px)minWidth: 200,// Minimum sidepane width (px)centralPaneMinWidth: 400,// Minimum central pane width (px)centralPaneMaxWidth: 800,// Maximum central pane width (px)compactWidth: 40,// Width when closed in compact mode (px)animationDuration: 200,// Animation duration (ms)defaultLeftPane: {openState: 'pinned',width: 280,closedStyle: 'compact'},defaultRightPane: {openState: 'closed',width: 320,closedStyle: 'hidden'}}}>{children}</SidepanesProvider>Config options:
persistence- Persistence adapter for saving pane state (default:localStorageAdapter)defaultWidth- Default width for both panes in pixels (default: 320)minWidth- Minimum sidepane width in pixels (default: 200)centralPaneMinWidth- Minimum central pane width in pixels (default: 400)centralPaneMaxWidth- Maximum central pane width in pixels (default: 800)compactWidth- Width when closed in compact mode in pixels (default: 40)animationDuration- Animation duration in milliseconds (default: 200)defaultLeftPane- Initial state for left pane:{ openState?: 'closed' | 'pinned', width?: number, closedStyle?: 'hidden' | 'compact' }defaultRightPane- Initial state for right pane:{ openState?: 'closed' | 'pinned', width?: number, closedStyle?: 'hidden' | 'compact' }
Note: openState can be 'closed', 'pinned', or 'hovered', but only 'closed' and 'pinned' are persisted. The 'hovered' state is always temporary.
The main side panel component. Renders an <aside> element with data attributes.
<Sidepaneanchor="left"// 'left' | 'right'closedStyle="compact"// 'compact' | 'hidden'resizable// Enable resize handleheader={<h3>Title</h3>}// Optional header contentclassName="my-sidepane">
Panel content
</Sidepane>Render props pattern:
Both children and header props can be render functions that receive pane state:
<Sidepaneanchor="left"closedStyle="compact"header={({ isCompact, isOpen, isPinned })=>(<div><h3style={{opacity: isCompact ? 0 : 1}}>
Navigation
</h3></div>)}>{({ isOpen, isTemporary, isPinned, isCompact, width, toggle, open, close })=>(<div><p>Pane is {isOpen ? 'open' : 'closed'}</p><p>Width: {width}px</p><buttononClick={toggle}>Toggle</button></div>)}</Sidepane>Data attributes for styling:
data-sidepane- Always presentdata-anchor="left|right"- Panel positiondata-expanded="true|false"- Whether the pane is expandeddata-temporary="true"- When opened via hoverdata-closed-style="compact|hidden"- The closedStyle configuration (always present)data-overlay="true"- When displayed as overlay (temporary or compact hovering)
CSS custom properties:
--sidepane-width- Current width in pixels--sidepane-animation-duration- Animation duration in milliseconds
Wrapper elements:
When using the header prop, the Sidepane component creates wrapper elements:
data-sidepane-header- Wraps the header contentdata-sidepane-content- Wraps the main contentdata-sidepane-placeholder- Placeholder element to maintain layout when compact pane is displayed as overlay
Renders a toggle button using render props pattern.
<SidepaneToggleanchor="left">{({ isOpen, isTemporary, isPinDisabled, onClick, ariaLabel })=>(<buttononClick={onClick}aria-label={ariaLabel}disabled={isPinDisabled}>{isTemporary ? <PinIcon/> : isOpen ? <CloseIcon/> : <OpenIcon/>}</button>)}</SidepaneToggle>Drag handle for resizing panels.
<Sidepaneanchor="left"resizable><nav>Content</nav><SidepaneResizeHandleanchor="left"/></Sidepane>Invisible sensor area that triggers hover-to-open behavior.
<EdgeHoverSensoranchor="right"disabled={false}zIndex={1202}getWidth={({ viewportWidth, centralPaneRect })=>{// Custom width calculationreturnMath.min(200,viewportWidth*0.15)}}>{({ width, isActive, anchor })=>(// Optional: Custom sensor visualization<div>Sensor width: {width}px</div>)}</EdgeHoverSensor>Props:
anchor- 'left' | 'right' (required)disabled- Disable the sensor (default: false)getWidth- Custom width calculator function receiving{ viewportWidth, centralPaneRect }children- Optional render function for custom renderingzIndex- z-index value (default: 1202)className- Additional CSS classstyle- Additional inline styles
Data attributes:
data-edge-hover-sensor- Always presentdata-anchor="left|right"- Sensor positiondata-active="true"- When sensor is active (pane closed)
CSS custom properties:
--edge-sensor-width- Current width in pixels
The main content area that adjusts based on open sidepanes.
<CentralPaneclassName="main-content">
Your main content
</CentralPane>The library uses data attributes for styling, allowing you to target different states with CSS:
/* Basic sidepane styling */
[data-sidepane] {
position: fixed;
top:0;
bottom:0;
width:var(--sidepane-width);
background: white;
transition: transform 0.3s ease;
}
/* Left sidepane positioning */
[data-sidepane][data-anchor="left"] {
left:0;
}
/* Right sidepane positioning */
[data-sidepane][data-anchor="right"] {
right:0;
}
/* Closed state */
[data-sidepane][data-expanded="false"][data-anchor="left"] {
transform:translateX(-100%);
}
/* Compact mode (slim bar when closed) */
[data-sidepane][data-closed-style="compact"][data-expanded="false"] {
width:48px;
transform: none;
}
/* Temporary/hover state overlay */
[data-sidepane][data-temporary="true"] {
box-shadow:0020pxrgba(0,0,0,0.2);
z-index:100;
}The library includes built-in persistence adapters:
import{localStorageAdapter,// Uses localStoragecookieAdapter,// Uses document.cookienoopAdapter// No persistence}from'@didmar/react-sidepanes'// Use localStorage (default)<SidepanesProviderconfig={{persistence: localStorageAdapter}}>
// No persistence
<SidepanesProviderconfig={{persistence: noopAdapter}}>
// Custom adapter
const myAdapter = {get: (key: string)=>myStorage.get(`sidepanes:${key}`),set: (key: string,value: string)=>myStorage.set(`sidepanes:${key}`,value)}<SidepanesProviderconfig={{persistence: myAdapter}}>Full TypeScript support with exported types:
importtype{SidepaneAnchor,PaneState,SidepanesConfig,PersistenceAdapter,ToggleRenderProps}from'@didmar/react-sidepanes'# Install dependencies
pnpm install
# Start demo app
pnpm dev
# Run tests
pnpm test# Run E2E tests
pnpm test:e2e
# Build library
pnpm build
# Lint
pnpm lintcd packages/react-sidepanes
npm pack --dry-run # preview first
npm publishMIT