React 19 · 8 kB gzipped · no dependencies
The chassis
under your tool.
Icon rails on the edges, resizable zones around a centre that is yours, and a layout that survives a reload. Headless underneath, repaintable on top.
Live. Drag the gutters between the surfaces, click a rail icon or move it into another half, resize the window.
Four ways in
Each one runs in your browser and its whole source is on screen. Start from the closest.
Minimal
The smallest chassis that works. Two columns, a band, a centre — and a header of your own driving it.
import { Panels, Panel } from '@pasquelin/panels'
import '@pasquelin/panels/styles.css'
<Panels <PanelId >>
<Panel <PanelId > id="files" zone="left" title="Files" >
<FileTree />
</Panel >
<Panel <PanelId > id="notes" zone="right" opens={380}>
<Notes />
</Panel >
<Panels.Center >
<YourApp />
</Panels.Center >
</Panels >
Tip. Panels sharing a zone and a slot take turns; the rail switches between them. Give a panel the secondary slot to stack it under the first instead. Here the rail icons can also be dragged from one half to another.
Open Minimal →
function Layout() {
return (
<Panels <PanelId >>
<Panel <PanelId > id="sites" zone="left" title="Sites" >
<SiteList />
</Panel >
<Panels.Center >
<Outlet />
</Panels.Center >
</Panels >
)
}
Tip. Declare the panels in the layout route, above the outlet. Declared per page, they would unmount on every navigation and lose whatever they held.
Open React Router →
import { DockviewCenter } from '@pasquelin/panels/dockview'
<Panels.Center >
<DockviewCenter
documents={{ editor: Editor }}
empty={NothingOpen}
onReady={setApi}
/>
</Panels.Center >
Tip. Import it from the dockview entry point so its weight only lands on the projects that want tabs. Tool panels never enter the centre: a document has a name, a panel has an icon.
Open Document tabs →
Repainted
The same chassis under four palettes. Colours, radius, rail width, header height — all custom properties.
.pnl-root {
--pnl-chassis : #2e2b26;
--pnl-panel : #1b1916;
--pnl-accent : #d99a2b;
--pnl-radius : 12px;
--pnl-rail : 60px;
}
Tip. Set the accent token on any ancestor and the chassis inherits your brand instead of imposing one. Need to go further? Every piece is exported and replaceable on its own.
Open Repainted →
The whole surface
There is not much of it, and that is the point. Six things to know.
Declare
A panel is a descriptor. Where it hangs, what it is called, what it draws.
<Panel id="files" zone="left" title="Files" icon={<Icon />}>
<FileTree />
</Panel >
Drive
One hook for every header, shortcut or menu that needs to act on the panels.
const { reveal, close, toggle, isShown } = usePanels<PanelId >()
Rearrange
One prop, and a reader drags a rail icon into another half. Where they put it is remembered with the rest of the layout.
<Panels <PanelId > draggablePanels>
Drive from outside React
Build the store yourself and a socket, a native menu or a worker can open a panel.
const store = createPanelsStore<PanelId >()
socket.on('alert' , () => store.getState().show('alerts' ))
Repaint
Every value is a custom property. Set the accent and the rails follow your brand.
.pnl-root {
--pnl-panel : #101418;
--pnl-accent : #47965c;
--pnl-radius : 10px;
}
Or take the logic only
The components are built on hooks that render nothing. Draw your own frame on them.
const zone = useZone('left' )
useContainerFit(ref)