Split-pane layout groups with draggable, keyboard-operable dividers.
This is the layout primitive behind docs sites with an adjustable sidebar, IDE-style workspaces and editor/preview splits. Three components compose it:
resizable_group/1- the flex container that owns thePetalResizablehookresizable_panel/1- a sized paneresizable_handle/1- the separator between two panes
Panels are sized as percentages of the group and rendered as
flex: <pct> 1 0px, so a window resize keeps the split proportional. The
hook reads only its DIRECT children, which is what makes nested groups work:
each group owns its own hook instance and never touches an inner group's
panels.
The library stores nothing. On drag release and on keyboard commit the group
dispatches a bubbling petal:resizable-resize DOM event carrying
detail.sizes (percentages in panel order) and, when on_resize is set,
pushes the same payload to your LiveView. Persist it wherever you like -
session, URL params, localStorage.
Keyboard
With a handle focused (it is in the tab order):
ArrowLeft/ArrowRight- resize a vertical separator by 2 pointsArrowUp/ArrowDown- resize a horizontal separator by 2 points- hold
Shiftfor a 10-point step Home- shrink the preceding panel to itsmin_size(collapse it if collapsible)End- grow the preceding panel to itsmax_sizeEnter- toggle collapse on a collapsible preceding panel
Arrows perpendicular to the separator are a no-op, per the WAI-ARIA window splitter pattern.
Examples
A docs layout: a collapsible sidebar and a content pane.
<.resizable_group id="docs" class="h-80">
<.resizable_panel id="docs-nav" default_size={25} min_size={15} collapsible>
Navigation
</.resizable_panel>
<.resizable_handle controls="docs-nav" with_handle />
<.resizable_panel default_size={75}>
Content
</.resizable_panel>
</.resizable_group>A vertical split that reports its sizes back to the LiveView.
<.resizable_group id="editor" orientation="vertical" on_resize="split_changed" class="h-96">
<.resizable_panel id="editor-code" default_size={70} min_size={30}>Code</.resizable_panel>
<.resizable_handle orientation="vertical" controls="editor-code" />
<.resizable_panel default_size={30} min_size={10}>Output</.resizable_panel>
</.resizable_group>Nested groups - a horizontal split inside a vertical one. Each group gets its own id and its own hook.
<.resizable_group id="ide" orientation="vertical" class="h-96">
<.resizable_panel default_size={75}>
<.resizable_group id="ide-top" class="h-full">
<.resizable_panel id="ide-files" default_size={20} min_size={10}>Files</.resizable_panel>
<.resizable_handle controls="ide-files" />
<.resizable_panel default_size={50}>Editor</.resizable_panel>
<.resizable_handle />
<.resizable_panel default_size={30}>Preview</.resizable_panel>
</.resizable_group>
</.resizable_panel>
<.resizable_handle orientation="vertical" />
<.resizable_panel default_size={25} min_size={10}>Terminal</.resizable_panel>
</.resizable_group>
Summary
Functions
The container. Renders a flex row (or column) and mounts the PetalResizable
hook, which owns dragging, keyboard resizing, clamping and the resize events.
The separator between two panels.
One pane of a group.
Functions
The container. Renders a flex row (or column) and mounts the PetalResizable
hook, which owns dragging, keyboard resizing, clamping and the resize events.
Put resizable_panel/1 and resizable_handle/1 children in it, alternating,
with a handle between every adjacent pair of panels. Give the group a height
(class="h-80", class="h-full", ...) - a flex container has none of its own.
Attributes
id(:string) - auto-generated when omitted; set it so the resize events can be told apart. Defaults tonil.orientation(:string) - horizontal = panels side by side (vertical dividers); vertical = panels stacked. Defaults to"horizontal". Must be one of"horizontal", or"vertical".on_resize(:string) - optional LiveView event name pushed on drag release and keyboard commit with %{"sizes" => [..]} percentages. Defaults tonil.class(:any) - extra classes on the group; this is where height goes. Defaults tonil.- Global attributes are accepted.
Slots
inner_block(required) - resizable_panel and resizable_handle children, in order.
The separator between two panels.
It is the accessible control: role="separator", in the tab order, and
carrying the live aria-valuenow for the panel before it. Note the
inversion - a handle in a horizontal group (panels side by side) is a
VERTICAL separator, so pass the group's orientation and the component flips
it for you.
The painted line is a hairline; the hit area around it is deliberately much
larger. with_handle adds the visible grip.
Server-rendered aria-valuenow/valuemin/valuemax are a starting point -
the hook restamps all three (plus aria-orientation and, when the preceding
panel has an id, aria-controls) from the live layout on mount and on every
resize.
Attributes
orientation(:string) - the ORIENTATION OF THE GROUP; the separator's own aria-orientation is the inverse. Defaults to"horizontal". Must be one of"horizontal", or"vertical".with_handle(:boolean) - renders the visible grip-dot affordance. Defaults tofalse.controls(:string) - id of the preceding panel, for aria-controls; the hook fills it in when the panel has one. Defaults tonil.value_now(:integer) - initial aria-valuenow (the preceding panel's size); the hook keeps it current. Defaults to50.value_min(:integer) - initial aria-valuemin for the preceding panel. Defaults to0.value_max(:integer) - initial aria-valuemax for the preceding panel. Defaults to100.label(:string) - accessible name for the separator; override per split when a page has several. Defaults to"Resize panels".class(:any) - extra classes on the separator. Defaults tonil.- Global attributes are accepted.
One pane of a group.
default_size is a percentage of the group. Panels without one grow to share
whatever is left (flex: 1 1 0px); when a group mixes sized and unsized
panels the hook normalises the shares on mount so the sized ones land on their
exact percentage.
Give the panel an id if a handle needs to point aria-controls at it, or if
you want to read the panel out of a petal:resizable-collapse event.
Attributes
id(:string) - needed for aria-controls and collapse events. Defaults tonil.default_size(:integer) - initial size as a percentage of the group; unsized panels share the remainder equally. Defaults tonil.min_size(:integer) - smallest percentage the panel can be dragged or keyed down to. Defaults to10.max_size(:integer) - largest percentage the panel can grow to. Defaults to100.collapsible(:boolean) - when true, dragging below roughly half the min_size snaps the panel to collapsed_size and fires petal:resizable-collapse. Defaults tofalse.collapsed_size(:integer) - the size the panel snaps to when collapsed. Defaults to0.class(:any) - extra classes on the panel. Defaults tonil.- Global attributes are accepted.
Slots
inner_block(required)