Resizable
Lay a row or column of panels out and let the user move the boundary between two of them, by dragging or by arrowing the handle.
ResizablePanelGroup lays a row (or column) of panels out and lets the user move the boundary between two of
them, by dragging the handle or by arrowing it. Three components work together: ResizablePanelGroup (the
flex container and single owner of the sizes), ResizablePanel (one region, sized as a share of the group)
and ResizableHandle (the divider, focusable and draggable).
Concept — percentages, not pixels
Sizes are percentages of the group, never pixels — that is what makes a layout survive a window resize. The
group normalizes whatever defaultSizes you give so they sum to 100.
Minimal example
The group takes the full size of its parent (size-full), so it needs a parent with a height — one in a
plain <div> with no height collapses.
import { Component } from "@angular/core";
import { ResizableHandleComponent, ResizablePanelComponent, ResizablePanelGroupComponent } from "@sinequa/galactik";
@Component({
selector: "sample-component",
imports: [ResizablePanelGroupComponent, ResizablePanelComponent, ResizableHandleComponent],
template: `
<div class="h-96">
<resizable-panel-group direction="horizontal">
<resizable-panel [defaultSize]="30" [minSize]="20">Filters</resizable-panel>
<resizable-handle [withHandle]="true" aria-label="Resize the filters panel" />
<resizable-panel [defaultSize]="70">Results</resizable-panel>
</resizable-panel-group>
</div>
`,
})
export class SampleComponent {}How it works
minSize/maxSize per panel are honoured by both the pointer and the keyboard — what one panel refuses to
take, its neighbour keeps, so the total always stays at 100%. The handle implements the ARIA
window-splitter pattern: role="separator",
aria-orientation (the orientation of the line, not of the group — a separator between side-by-side panels
is vertical), and aria-valuenow/valuemin/valuemax bounded by the panel before it. Arrow keys move the
focused handle by 10% of the group, along the axis the group's direction implies — a resize is therefore
fully reachable without a pointer (WCAG 2.1.1). Dragging uses pointer events with pointer capture, so the drag
follows the pointer far outside the handle with no window-level listener, and a full-page overlay during the
drag keeps an <iframe> in a panel from swallowing the pointer.
Recipes
Remembering the layout
Give the group an autoSaveId and the layout is remembered in localStorage, under the key
resizable-panel:<autoSaveId> — restored on the next visit, but only if the group still has the same number
of panels; otherwise the panels' own defaultSizes are the better answer.
<resizable-panel-group direction="horizontal" autoSaveId="search-results">…</resizable-panel-group>Driving the layout from the outside
import { Component, signal } from "@angular/core";
import { ResizableHandleComponent, ResizablePanelComponent, ResizablePanelGroupComponent } from "@sinequa/galactik";
@Component({
selector: "sample-component",
imports: [ResizablePanelGroupComponent, ResizablePanelComponent, ResizableHandleComponent],
template: `
<resizable-panel-group #group="resizablePanelGroup" direction="horizontal" (layout)="sizes.set($event)">
<resizable-panel [defaultSize]="50">Left</resizable-panel>
<resizable-handle [withHandle]="true" aria-label="Resize panels" />
<resizable-panel [defaultSize]="50">Right</resizable-panel>
</resizable-panel-group>
<button type="button" (click)="group.setLayout([50, 50])">Reset</button>
`,
})
export class SampleComponent {
readonly sizes = signal<number[]>([]);
}setLayout() is ignored unless the array covers exactly the panels present — a partial layout would leave the
group summing to something other than 100%.
Options
Prop
Type
ResizablePanel
Prop
Type
ResizableHandle
Prop
Type