Resizable

Beta

A layout component that lets users resize adjacent panels by dragging a handle between them.

Preview

Usage

The resizable component splits a container into adjustable panels separated by draggable handles. It works well for sidebar layouts, split-view editors, and any interface where users should control how space is distributed.

Examples

Two panels

Three panels

Vertical

Nested layouts

Resizable groups can be nested. For example, a horizontal sidebar + content layout where the content area is itself a vertical split:

Best practices

Do

  • Set initial-size on panels with a known width (sidebars, detail panes). Omit it on the main content panel so it fills the remaining space.
  • Set user-min-size on every panel. Without it, panels can shrink to nearly zero. A minimum of "300" (192px) keeps most content usable.
  • Use storage-key to persist layouts. Users expect their panel arrangement to survive a page refresh.
  • Always provide a way to restore collapsed panels — for example, a menu icon button in a sibling panel's header.

Don’t

  • Don't pass raw pixel values for sizes. Only percentage tokens ("25p") and Dialtone layout tokens ("500") are accepted.
  • Don't set initial-size on every panel. Leave one panel without it so it absorbs remaining space and the layout always fills the container.
  • Don't hide a panel without giving the user a way to bring it back. Provide an expand control in a visible sibling panel (e.g., a menu button in the content header).

Constraints

All size props accept two formats: percentage tokens (e.g., "25p" for 25% of the container) and Dialtone layout tokens (e.g., "500" which resolves to 320px). Raw pixel values are not accepted — the component resolves token values from Dialtone's --dt-layout-* CSS custom properties at runtime, matching DtBox and the d-w-* / d-h-* utility classes.

initial-size defines where a panel starts. For panels whose size should flex with the available space (like a main content area), omit initial-size and the panel will fill whatever space remains.

User constraints

user-min-size and user-max-size set hard limits on how small or large a user can drag a panel. These are enforced during drag interactions.

System constraints

system-min-size and system-max-size define the range the layout engine uses when redistributing space during viewport resizes. These default to the user constraints when not specified. System min must be >= user min, and system max must be <= user max.

Fixed panels

Set :resizable="false" to fix a panel at its initial-size. Fixed panels cannot be dragged, and no handle is rendered between a fixed panel and its neighbor. The layout engine subtracts fixed panel widths first, then distributes the remaining space among resizable panels.

Collapsing panels

Mark a panel as collapsible to let it collapse to zero width. Use the collapsed prop for the initial state, or call collapsePanel() from a template ref.

<script setup>
import { ref } from 'vue';

const isSidebarCollapsed = ref(false);

function onPanelCollapse (panelId, collapsed) {
  if (panelId === 'sidebar') {
    isSidebarCollapsed.value = collapsed;
  }
}
</script>

Listen to @panel-collapse to keep your local state in sync — the panel can also be collapsed by the system (auto-collapse rules, viewport resize). Always provide a visible control in a sibling panel to restore a collapsed panel.

Dynamic constraints on collapse

For layouts where a panel starts hidden (e.g., a detail pane that opens when an item is selected), bind initial-size to a computed value that changes based on collapsed state:

Auto-collapse rules

Use collapse-rules to define which panels collapse first when space gets tight. Lower priority numbers collapse first.

Persisting panel sizes

Add a storage-key to save panel sizes to localStorage automatically. Users resize once, and the layout restores on their next visit.

For state management integration (Pinia, Vuex, or an API), implement the ResizableStorageAdapter interface and pass it via :storage:

const piniaAdapter = {
  save(data) { layoutStore.setLayout(data); },
  load() { return layoutStore.layout; },
  clear() { layoutStore.clearLayout(); },
};

When both storage-key and :storage are provided, the custom adapter takes precedence.

Space allocation strategies

When a panel opens or closes, the remaining panels need to redistribute space. The space-allocation-strategy prop controls how:

  • proportional (default) — All non-collapsed panels give or take space proportionally based on their current size.
  • preserve-manual — Panels that the user has manually resized keep their exact size. Only panels the user hasn't touched give up space.

Offset from fixed elements

When a fixed or absolutely positioned element (like a toolbar or header) overlaps the resizable area, set offset-element on DtResizable to automatically offset all handles and panel content below it.

Alternatively, use offset-amount for an explicit pixel value without measuring an element. If both are provided, offset-amount takes precedence.

Accessibility

Keyboard navigation

Each resize handle has role="separator" with aria-orientation, aria-valuenow, aria-valuemin, aria-valuemax, aria-controls, and aria-valuetext reflecting the current layout. Handles are always focusable (tabindex="0") and follow the W3C ARIA separator pattern.

Key Action
Arrow keys Resize by 8px
Shift + Arrow Resize by 24px
Ctrl/Cmd + Arrow Resize by 1px
Enter Collapse or expand the adjacent panel (if collapsible)
Home Set panel to minimum size
End Set panel to maximum size
R Reset adjacent panels to initial sizes
Escape Remove focus from the handle

Size changes are announced to screen readers via an aria-live region. All announcement strings are configurable via the messages prop on DtResizable for i18n.

Double-clicking a handle resets the two adjacent panels to their initial size proportions.

Vue API

DtResizable

import { Resizable } from '@dialpad/dialtone-vue';

Slots

Name
Type
default

Container for panels and handles.

Props

Name
Default
Type
class
''
string|object|array

Additional CSS classes applied to the container element.

collapseRules
[]
array

Rules defining which panels collapse first when space is constrained

direction
'row'
"'row'" | "'column'"

Layout direction. 'row' for horizontal, 'column' for vertical.

messages
{}
object

i18n message overrides for screen reader announcements. Accepts keys from ResizableKeyboardMessages.

offsetAmount
null
number

Explicit pixel offset. Overrides offsetElement measurement when both provided.

offsetDirection
'start'
"'start'" | "'end'" | "'both'"

Which edge(s) the offset applies to.

offsetElement
null
string

CSS selector for a fixed element to offset handles and panel content from.

panels
[]
array

Panel configurations array. When provided, panels are initialized from this array instead of registering via child DtResizablePanel components.

spaceAllocationStrategy
'proportional'
"'proportional'" | "'preserve-manual'"

Strategy for redistributing space when panels open/close.

storage
null
object

Custom storage adapter. Overrides storageKey when both are provided.

storageKey
null
string

localStorage key for persisting panel sizes across page loads.

Events

Name
Type
panel-collapse
panel-resize
resize-end
resize-start

DtResizablePanel

import { DtResizable_panel } from '@dialpad/dialtone-vue';

DtResizableHandle

import { DtResizable_handle } from '@dialpad/dialtone-vue';

Resizable documentation last updated Thursday, June 18, 2026