Shell
The app frame, with panes that open, close and resizeShell is the frame of an application. It places a header, navigation, the main content, an inspector and a bottom panel on a CSS grid. Each pane holds its own open state, follows the window when nobody has touched it, and becomes an overlay where there is no room for it.
Installation
Usage
On a window 768px or wider the sidebar sits in the frame and is open. Below 768px it is an overlay and is closed until a Shell.Trigger opens it.
Anatomy
Every part is optional except Shell.Content. The Root renders every child in the order you write it, and each part takes its own place on the Root's grid. A part must therefore be a direct child of Shell.Root, or sit in a wrapper with display: contents; inside any other wrapper the wrapper takes the grid cell. Any other child is a grid item too: give it a place (grid-area or position), or it takes the first free cell.
A Panel with no presentation of its own takes the Rail's. In the server HTML it does so only when it comes after the Rail in the same parent; otherwise it follows the Rail once the page is hydrated.
Props
Root
Header
Sidebar
The sidebar also takes the resize props.
Rail
Panel
The panel also takes the resize props.
Content
Inspector
The inspector also takes the resize props.
Bottom
The bottom panel also takes the resize props.
Trigger
Shell.Trigger is a <button>. Wrap it with IconButton or Button through asChild to style it.
The trigger renders type="button", aria-controls (the pane's id) and aria-expanded. Your onClick runs first. Call event.preventDefault() in it to cancel the trigger's action.
Resize
Panel, Sidebar, Inspector and Bottom take these props.
Presentation
presentation says how a pane sits in the frame. It takes one value or a responsive object.
Fixed
The pane takes its own column or row, and the content is narrower by its size.
Overlay
The pane covers the frame. A scrim covers everything else, the rest of the frame is inert, and focus moves into the pane. Escape or a press on the scrim closes it, and focus goes back where it was. An overlay that nobody has opened stays closed.
How far an overlay reaches depends on the Root's height:
'full'(the default): the overlay covers the window, and the page does not scroll while it is open.'auto': the frame grows with its content and the page scrolls, so the overlay and the scrim are placed on the window.- A number or a length: the Shell sits inside a page, and the overlay and the scrim cover only the Shell's box. The page still scrolls.
Stacked
The pane sits over the content without a scrim and without taking space from it.
States
Untouched
A pane with no open, defaultOpen, state or defaultState is untouched. It renders data-state="auto", and the stylesheet shows or hides it at each breakpoint:
- Rail and Sidebar are open where they are in the frame and closed where they are an overlay.
- Panel, Inspector and Bottom are closed.
A responsive defaultOpen or defaultState also leaves the pane untouched, with your object as the answer at each breakpoint:
The server HTML already carries the answer for every breakpoint, so the first paint is right before any script runs.
Touched
When a user opens or closes a pane, it keeps that state at every width. A single-value defaultOpen or defaultState starts the pane in that state in the same way.
Controlled
Pass open (or state on the Sidebar) with its change handler. The pane shows what you pass. Do not pass both the controlled prop and its default.
Callbacks
onOpenChange and onStateChange are called only when a user acts. reason is:
'toggle'for a Trigger, a double-click on the handle, or a drag pastcollapseThreshold.'dismiss'for Escape or a press on the scrim.
They are not called on mount, and not called when the window crosses a breakpoint. The deprecated onExpand and onCollapse follow the same rule, and also fire when a controlled open or state changes whether the pane is shown.
Sidebar thin
thin shows the sidebar at thinSize. With toggleModes="both" a trigger goes collapsed, thin, expanded. With toggleModes="single" it goes between collapsed and the sidebar's open state (thin if defaultState is "thin", else expanded).
Examples
Header and Sidebar
Rail and Panel
The rail and the panel do not drive each other. To close the panel when the rail closes, write it:
Resizable Inspector
Keep the size in your own state to save it between visits.
Controlled Sidebar
Inset
inset lifts a pane off the frame. The pane gets a margin, rounded corners and a shadow, and the frame paints a gray backdrop behind the middle row. Two inset panes side by side share one gap.
Change the gap on the Root:
Responsive
presentation, defaultOpen and defaultState take responsive objects. A breakpoint takes the value of the largest key at or below it.
The values are written to the pane as data-presentation-<bp> and data-auto-<bp>, and the stylesheet reads them at these widths.
Accessibility
Shell.Headerrenders<header>andShell.Contentrenders<main>.Shell.Triggersetsaria-controlsandaria-expanded.- An open overlay makes the rest of the frame
inert, moves focus into the pane, and returns focus when the pane closes. - Escape closes open overlays. It is handled as the key bubbles up to the Root, after the panes' own handlers: a component inside a pane that answers Escape itself, and calls
preventDefault()orstopPropagation(), keeps the pane open. A dialog or menu opened from a pane closes first. - The resize handle is a focusable
role="separator"witharia-controls,aria-valuenow,aria-valueminandaria-valuemax. The arrow keys move it by 8px (32px with Shift), and Home and End go to the limits. It shows a focus line on:focus-visible.
Changelog
0.8.0
- The Shell is rebuilt on kookie-ui-v2's model. See the package changelog for the full list.
- Untouched panes follow the window through the stylesheet. Callbacks fire only when a user acts.
- Sidebar and Inspector are overlays below 768px.
- The Rail and the Panel do not drive each other.
- An overlay is the same element as the pane, not a portalled sheet.
- The resize handle is
role="separator". - No motion.
- Deprecated:
Shell.Left,target="left",peekOnHover,snapPoints,snapTolerance,collapseThreshold,paneId,persistence, a responsive controlledopenorstate,useShell()anduseResponsivePresentation().
0.7.1
- Bug fixes: Root typing, Panel
onExpand/onCollapse, Triggertypeandaria-controls, Bottom arrow keys, the100dvhheight, the handle's focus line, and theheightprop on panes.
© 2026 Kushagra Dhawan. Licensed under MIT. GitHub.