Skip to content
Ndop UI

Resizable

Panels that share space, resized by dragging or with the keyboard.

One
Two
Three
import { Component } from '@angular/core';
import {
  ResizableHandle,
  ResizablePanel,
  ResizablePanelGroup,
} from '@/ui/resizable/resizable';

/**
 * A sidebar beside two stacked panes. Each handle is a separator that the
 * arrow keys or a pointer move.
 */
@Component({
  selector: 'example-resizable-demo',
  imports: [ResizableHandle, ResizablePanel, ResizablePanelGroup],
  template: `
    <div
      ndResizablePanelGroup
      orientation="horizontal"
      class="max-w-md rounded-lg border md:min-w-[450px]"
    >
      <div ndResizablePanel [defaultSize]="50">
        <div class="flex h-[200px] items-center justify-center p-6">
          <span class="font-semibold">One</span>
        </div>
      </div>
      <div ndResizableHandle aria-label="Resize one"></div>
      <div ndResizablePanel [defaultSize]="50">
        <div ndResizablePanelGroup orientation="vertical">
          <div ndResizablePanel [defaultSize]="25">
            <div class="flex h-full items-center justify-center p-6">
              <span class="font-semibold">Two</span>
            </div>
          </div>
          <div ndResizableHandle aria-label="Resize two"></div>
          <div ndResizablePanel [defaultSize]="75">
            <div class="flex h-full items-center justify-center p-6">
              <span class="font-semibold">Three</span>
            </div>
          </div>
        </div>
      </div>
    </div>
  `,
})
export class ResizableDemoExample {}

Resizable panels share a row or column, with a handle between each pair that a pointer drags or the arrow keys move: a window splitter, such as a sidebar next to an editor. Each panel can have a default, minimum and maximum size, in percent of the group, and [(sizes)] reads or restores the layout.

Installation

npx @ndop/cli add resizable

Usage

import {
  ResizableHandle,
  ResizablePanel,
  ResizablePanelGroup,
} from '@/ui/resizable/resizable';
<div
  ndResizablePanelGroup
  orientation="horizontal"
  class="max-w-md rounded-lg border md:min-w-[450px]"
>
  <div ndResizablePanel [defaultSize]="50">
    <div class="flex h-[200px] items-center justify-center p-6">
      <span class="font-semibold">One</span>
    </div>
  </div>
  <div ndResizableHandle aria-label="Resize one"></div>
  <div ndResizablePanel [defaultSize]="50">
    <div ndResizablePanelGroup orientation="vertical">
      <div ndResizablePanel [defaultSize]="25">
        <div class="flex h-full items-center justify-center p-6">
          <span class="font-semibold">Two</span>
        </div>
      </div>
      <div ndResizableHandle aria-label="Resize two"></div>
      <div ndResizablePanel [defaultSize]="75">
        <div class="flex h-full items-center justify-center p-6">
          <span class="font-semibold">Three</span>
        </div>
      </div>
    </div>
  </div>
</div>

Examples

Resizable handle

A handle with a grip, showing it can be dragged.

Sidebar
Content
import { Component } from '@angular/core';
import { NdIcon } from '@ndop/core';
import {
  ResizableHandle,
  ResizableHandleGrip,
  ResizablePanel,
  ResizablePanelGroup,
} from '@/ui/resizable/resizable';

/** A handle with a grip, showing it can be dragged. */
@Component({
  selector: 'example-resizable-handle',
  imports: [
    NdIcon,
    ResizableHandle,
    ResizableHandleGrip,
    ResizablePanel,
    ResizablePanelGroup,
  ],
  template: `
    <div
      ndResizablePanelGroup
      orientation="horizontal"
      class="min-h-[200px] max-w-md rounded-lg border md:min-w-[450px]"
    >
      <div ndResizablePanel [defaultSize]="25" [minSize]="15">
        <div class="flex h-full items-center justify-center p-6">
          <span class="font-semibold">Sidebar</span>
        </div>
      </div>
      <div ndResizableHandle aria-label="Resize sidebar">
        <div ndResizableHandleGrip><svg ndIcon="grip-vertical"></svg></div>
      </div>
      <div ndResizablePanel [defaultSize]="75">
        <div class="flex h-full items-center justify-center p-6">
          <span class="font-semibold">Content</span>
        </div>
      </div>
    </div>
  `,
})
export class ResizableHandleExample {}

Resizable vertical

Two panes stacked, resized up and down.

Header
Content
import { Component, signal } from '@angular/core';
import {
  ResizableHandle,
  ResizablePanel,
  ResizablePanelGroup,
} from '@/ui/resizable/resizable';

/**
 * Two panes stacked, resized up and down; the sizes are bound, to save
 * and restore the layout.
 */
@Component({
  selector: 'example-resizable-vertical',
  imports: [ResizableHandle, ResizablePanel, ResizablePanelGroup],
  template: `
    <div
      ndResizablePanelGroup
      orientation="vertical"
      [(sizes)]="sizes"
      class="min-h-[200px] max-w-md rounded-lg border md:min-w-[450px]"
    >
      <div ndResizablePanel [defaultSize]="25" [minSize]="10">
        <div class="flex h-full items-center justify-center p-6">
          <span class="font-semibold">Header</span>
        </div>
      </div>
      <div ndResizableHandle aria-label="Resize header"></div>
      <div ndResizablePanel [defaultSize]="75">
        <div class="flex h-full items-center justify-center p-6">
          <span class="font-semibold">Content</span>
        </div>
      </div>
    </div>
  `,
})
export class ResizableVerticalExample {
  protected readonly sizes = signal<readonly number[]>([]);
}

Accessibility

Implements the Window Splitter pattern of the WAI-ARIA Authoring Practices.

Keyboard

Keys the component handles.

Key Action
TabMoves focus to the next handle, in document order.
ArrowLeftorArrowRightIn a horizontal group, moves the focused handle toward that side by keyboardStep.
ArrowUporArrowDownIn a vertical group, moves the focused handle up or down by keyboardStep.
HomeShrinks the panel before the handle to its minSize, as far as the panel after it allows.
EndGrows the panel before the handle to its maxSize, as far as the panel after it allows.

Focus

  • Handles are in the tab order unless disabled; panels aren't.
  • Dragging a handle with the pointer doesn't move focus.

Right-to-left

In a horizontal group in a right-to-left layout, the panels run from right to left, so ArrowLeft grows the panel before the handle and ArrowRight shrinks it, and dragging follows the pointer. Direction comes from the nearest dir attribute.

API reference

ResizablePanelGroup

[ndResizablePanelGroup]

A group of resizable panels (@ndop/primitives/resizable), side by side or stacked, with a handle between each pair.

ResizablePanelGroup binding Type Default
[class]
Extra classes, merged last so they win.
string''
[orientation]
Whether the panels sit side by side or stacked.
'horizontal' | 'vertical''horizontal'
[keyboardStep]
How far an arrow key moves a handle, in percent.
number10
[(sizes)]
The panels' sizes in percent, in document order. Two-way bindable with [(sizes)]; when unset, each panel's defaultSize applies.
readonly number[][]
Data attribute Values
data-slotresizable-panel-group
data-orientation —

ResizablePanel

[ndResizablePanel]

One panel; its size is in percent of the group.

Manages id.

ResizablePanel binding Type Default
[defaultSize]
Initial size in percent; without one, it shares the space left.
number | undefinedundefined
[minSize]
Smallest size in percent.
number0
[maxSize]
Largest size in percent.
number100
Data attribute Values
data-slotresizable-panel
data-size —

ResizableHandle

[ndResizableHandle]role separator

The line between two panels, which arrow keys and dragging move. Put a ResizableHandleGrip in it to show it can be dragged. Name it with aria-label.

Manages aria-orientation, aria-valuenow, aria-valuemin, aria-valuemax, aria-controls, tabindex.

ResizableHandle binding Type Default
[class]
Extra classes, merged last so they win.
string''
[disabled]
Stops it moving and takes it out of the tab order.
booleanfalse
Data attribute Values
data-slotresizable-handle
data-dragging —
data-disabled —

ResizableHandleGrip

[ndResizableHandleGrip]

A grip in a handle, showing it can be dragged. It's decorative.

ResizableHandleGrip binding Type Default
[class]
Extra classes, merged last so they win.
string''
Data attribute Values
data-slotresizable-handle-grip

Agent notes

Do

  • Name each handle with aria-label, such as "Resize sidebar".
  • Set a minSize that keeps each panel usable.
  • Show a grip with ResizableHandleGrip when the handle is hard to see.

Don't

  • Don't nest so many groups that the handles are hard to tell apart.
  • Don't put the only way to reach content in a panel that can shrink to nothing.