Resizable
A hand-drawn split view whose panels resize by dragging or arrow-keying the divider.
Overview
Resizable is a split view whose panels the user can resize — built on pointer events, no
third-party engine. Use it for adjustable layouts: a nav / editor / inspector three-pane, a
list-detail split, a preview pane. Lay out ResizablePanels separated by ResizableHandles inside
a ResizablePanelGroup; sizes are percentages of the group.
Anatomy
A flex ResizablePanelGroup laid out along its direction, holding ResizablePanels split by
ResizableHandles. Each handle is a hand-drawn divider (@ghds/sketch-core line) that is a
focusable role="separator" with aria-valuenow / aria-valuemin / aria-valuemax. Dragging a
handle — or focusing it and pressing the arrow keys — redistributes space between its two
neighbouring panels.
Variants & States
The group’s direction ('horizontal' or 'vertical') is the main axis; the handle can show a
grip via withHandle.
| State | Status | Notes |
|---|---|---|
| Dragging | Implemented | Pointer drag on a handle resizes the adjacent panels. |
| Focus | Implemented | The handle is a focusable separator; arrow keys resize in 5% steps. |
| With grip | Implemented | withHandle renders a visible grip in the middle of the handle. |
| Disabled | Not implemented | — |
| Loading | Not implemented | — |
Usage
Do
- Give the ResizablePanelGroup (or a wrapper) an explicit width AND height — it fills its container.
- Set minSize / maxSize on panels that must not collapse or dominate.
- Use withHandle on the divider so the drag affordance is visible, especially on touch.
Don't
- Don't rely on drag alone — the arrow-key path is what makes it keyboard accessible; keep the handle focusable.
- Don't nest deeply resizable groups without need; the resize model is per-pair of neighbours.
Accessibility
- Each handle is a
role="separator"withtabIndex={0},aria-valuenow(its leading panel’s percentage),aria-valuemin={0}, andaria-valuemax={100}, andaria-orientationmatching the split — so screen readers announce it as an operable splitter with a live value. - Keyboard resize is built in: with the handle focused, Left/Right (horizontal) or Up/Down
(vertical) move in 5% steps, and
aria-valuenowupdates. - Sizes are clamped to each panel’s
minSize/maxSize, so keyboard and pointer resizing can’t push a panel past its bounds. - See the Accessibility Guide for shared conventions.
Content
Put a scrollable region inside each panel — panels set overflow: auto, so content that exceeds a
shrunken panel scrolls rather than clipping. Keep the most important pane above its minSize.
Props API
React@ghds/react
| Prop | Type | Default | Description |
|---|---|---|---|
direction | 'horizontal' | 'vertical' | — | Axis panels are laid out and resized along (ResizablePanelGroup). Required. |
defaultSize | number | — | Initial panel size as a percentage of the group (ResizablePanel). Defaults to an equal split. |
minSize | number | 10 | Smallest percentage the panel may shrink to (ResizablePanel). |
maxSize | number | 90 | Largest percentage the panel may grow to (ResizablePanel). |
withHandle | boolean | false | Shows a visible grip in the middle of the handle (ResizableHandle). |
...rest | HTMLAttributes<HTMLDivElement> | — | Native <div> attributes pass through to each part. |
Web Components@ghds/web-components
| Prop | Type | Default | Description |
|---|---|---|---|
direction | 'horizontal' | 'vertical' | 'horizontal' | Reflected attribute; axis the panels are laid out and resized along (<gh-resizable-group>). |
default-size | number | — | Initial panel size as a percentage of the group (<gh-resizable-panel>). Defaults to an equal split. |
min-size | number | 10 | Smallest percentage the panel may shrink to (<gh-resizable-panel>). |
max-size | number | 90 | Largest percentage the panel may grow to (<gh-resizable-panel>). |
with-handle | boolean | false | Reflected attribute; shows a visible grip in the middle of the handle (<gh-resizable-handle>). |
React Native@ghds/react-native
| Prop | Type | Default | Description |
|---|---|---|---|
direction | 'horizontal' | 'vertical' | — | Axis the panels are laid out and resized along (ResizablePanelGroup). Required. |
defaultSize | number | — | Initial panel size as a percentage of the group (ResizablePanel). Defaults to an equal split. |
minSize | number | 10 | Smallest percentage the panel may shrink to (ResizablePanel). |
maxSize | number | 90 | Largest percentage the panel may grow to (ResizablePanel). |
withHandle | boolean | false | Shows a visible grip in the middle of the handle (ResizableHandle). |
style / testID | StyleProp<ViewStyle> / string | — | Group container style, and a test handle. Touch-drag only — there is no arrow-key resize on React Native. |
Code examples
// React — @ghds/react
import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from '@ghds/react/resizable';
<div style={{ width: 480, height: 240 }}>
<ResizablePanelGroup direction="horizontal">
<ResizablePanel defaultSize={50}>One</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel defaultSize={50}>Two</ResizablePanel>
</ResizablePanelGroup>
</div>;
<!-- Web Components — @ghds/web-components -->
<script type="module">
import '@ghds/web-components/resizable-group';
import '@ghds/web-components/resizable-panel';
import '@ghds/web-components/resizable-handle';
</script>
<gh-resizable-group direction="horizontal">
<gh-resizable-panel default-size="50">One</gh-resizable-panel>
<gh-resizable-handle with-handle></gh-resizable-handle>
<gh-resizable-panel default-size="50">Two</gh-resizable-panel>
</gh-resizable-group>
// React Native — @ghds/react-native
// Touch-drag only — there is no arrow-key resize on React Native.
import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from '@ghds/react-native/resizable';
<ResizablePanelGroup direction="horizontal" style={{ height: 240 }}>
<ResizablePanel defaultSize={50}>{/* one */}</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel defaultSize={50}>{/* two */}</ResizablePanel>
</ResizablePanelGroup>;
Live Demo
React and Web Components render live below, driven by the same tokens — drag either divider, or
focus it and press the arrow keys to resize, and toggle dark mode in the header to see both respond
identically. React Native can’t run in a browser without additional react-native-web wiring (not
set up in this site), so its usage is shown as a code sample instead. (On React Native the handle is
touch-drag only — there is no arrow-key resize.)