Resizable

Storybook

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

<dt-resizable>
  <dt-resizable-panel id="sidebar" initial-size="25p">
    Sidebar
  </dt-resizable-panel>
  <dt-resizable-handle />
  <dt-resizable-panel id="content">
    Main content
  </dt-resizable-panel>
</dt-resizable>

Three panels

<dt-resizable>
  <dt-resizable-panel id="sidebar" initial-size="20p">
    Sidebar
  </dt-resizable-panel>
  <dt-resizable-handle />
  <dt-resizable-panel id="content">
    Content
  </dt-resizable-panel>
  <dt-resizable-handle />
  <dt-resizable-panel id="details" initial-size="25p">
    Details
  </dt-resizable-panel>
</dt-resizable>

Vertical

<dt-resizable direction="column">
  <dt-resizable-panel id="top" initial-size="40p">
    Top
  </dt-resizable-panel>
  <dt-resizable-handle />
  <dt-resizable-panel id="bottom">
    Bottom
  </dt-resizable-panel>
</dt-resizable>

Nested layouts

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

<dt-resizable>
  <dt-resizable-panel id="sidebar" initial-size="25p">
    Sidebar
  </dt-resizable-panel>
  <dt-resizable-handle />
  <dt-resizable-panel id="content">
    <dt-resizable direction="column">
      <dt-resizable-panel id="editor" initial-size="60p">
        Editor
      </dt-resizable-panel>
      <dt-resizable-handle />
      <dt-resizable-panel id="terminal">
        Terminal
      </dt-resizable-panel>
    </dt-resizable>
  </dt-resizable-panel>
</dt-resizable>

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 "825" (164px) 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 size tokens ("925") 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 size tokens (e.g., "925" which resolves to 332px). Raw pixel values are not accepted — the component resolves token values from Dialtone's CSS custom properties at runtime.

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.

<dt-resizable-panel
  id="sidebar"
  initial-size="30p"
  user-min-size="20p"
  user-max-size="50p"
>
  Sidebar (min 20%, max 50%)
</dt-resizable-panel>

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.

<dt-resizable>
  <dt-resizable-panel id="nav" initial-size="700" :resizable="false">
    Navigation (64px, fixed)
  </dt-resizable-panel>
  <dt-resizable-panel id="content">
    Content (fills remaining space)
  </dt-resizable-panel>
</dt-resizable>

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.

<dt-resizable @panel-collapse="onPanelCollapse">
  <dt-resizable-panel
    id="sidebar"
    initial-size="25p"
    user-min-size="825"
    collapsible
    :collapsed="isSidebarCollapsed"
  >
    Sidebar
  </dt-resizable-panel>
  <dt-resizable-handle />
  <dt-resizable-panel id="content">
    <header>
      <button @click="isSidebarCollapsed = !isSidebarCollapsed">
        Toggle sidebar
      </button>
    </header>
    Content
  </dt-resizable-panel>
</dt-resizable>

<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:

<dt-resizable-panel
  id="list"
  :initial-size="isDetailOpen ? '30p' : '100p'"
>
  List
</dt-resizable-panel>
<dt-resizable-handle />
<dt-resizable-panel
  id="detail"
  :initial-size="isDetailOpen ? '70p' : '0p'"
  collapsible
  :collapsed="!isDetailOpen"
>
  Detail
</dt-resizable-panel>

Auto-collapse rules

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

<dt-resizable
  :collapse-rules="[
    { panelId: 'details', priority: 1 },
    { panelId: 'sidebar', priority: 2 },
  ]"
>
  ...
</dt-resizable>

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.

<dt-resizable storage-key="my-layout">
  ...
</dt-resizable>

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(); },
};
<dt-resizable :storage="piniaAdapter">
  ...
</dt-resizable>

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.

<dt-resizable offset-element="#toolbar">
  <div id="toolbar" style="position: absolute; top: 0; left: 0; right: 0; height: 48px; z-index: 10;">
    Toolbar
  </div>
  <dt-resizable-panel id="left" initial-size="50p">
    Left
  </dt-resizable-panel>
  <dt-resizable-handle />
  <dt-resizable-panel id="right" initial-size="50p">
    Right
  </dt-resizable-panel>
</dt-resizable>

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

Slots

Name
Description
default

Container for panels and handles.

Props

Name
Description
Default
class

Additional CSS classes applied to the container element.

Type: string|object|array
''
collapseRules

Rules defining which panels collapse first when space is constrained

Type: array
[]
direction

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

Type: string
Values: 'row''column'
'row'
messages

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

Type: object
{}
offsetAmount

Explicit pixel offset. Overrides offsetElement measurement when both provided.

Type: number
null
offsetDirection

Which edge(s) the offset applies to.

Type: string
Values: 'start''end''both'
'start'
offsetElement

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

Type: string
null
panels

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

Type: array
[]
spaceAllocationStrategy

Strategy for redistributing space when panels open/close.

Type: string
Values: 'proportional''preserve-manual'
'proportional'
storage

Custom storage adapter. Overrides storageKey when both are provided.

Type: object
null
storageKey

localStorage key for persisting panel sizes across page loads.

Type: string
null

Events

Name
Description
panel-collapse
panel-resize
resize-end
resize-start

DtResizablePanel

Slots

Name
Description
default

Panel content. Provides panel state and collapsed/resizing flags.

Props

Name
Description
Default
id
required

Unique panel identifier. Must be unique within its DtResizable parent.

Type: string
class

Additional CSS classes applied to the panel element.

Type: string|object|array
''
collapseSize

Container width threshold that triggers auto-collapse.

Type: string
undefined
collapsed

Initial collapsed state.

Type: boolean
false
collapsible

Whether this panel can be collapsed to zero width.

Type: boolean
false
initialSize

Initial size as a percentage token (e.g., '25p' for 25%) or Dialtone size token.

Type: string
undefined
resizable

Whether this panel can be resized by dragging.

Type: boolean
true
systemMaxSize

Maximum size for system viewport scaling. Falls back to userMaxSize.

Type: string
undefined
systemMinSize

Minimum size for system viewport scaling. Falls back to userMinSize.

Type: string
undefined
userMaxSize

Maximum size for user drag interactions (hard ceiling).

Type: string
undefined
userMinSize

Minimum size for user drag interactions (hard floor).

Type: string
undefined

DtResizableHandle

Props

Name
Description
Default
afterPanelId

ID of the panel after this handle. Auto-detected from layout order if not set.

Type: string
null
ariaLabel

Override the default aria-label for i18n.

Type: string
null
beforePanelId

ID of the panel before this handle. Auto-detected from layout order if not set.

Type: string
null
class

Additional CSS classes applied to the handle element.

Type: string|object|array
''
disableResetOnDoubleClick

Disable the double-click reset behavior.

Type: boolean
false
disabled

Disable resize interaction for this handle.

Type: boolean
false
resetBehavior

Which panels to reset on double-click.

Type: string
Values: 'both''before''after''all'
'both'
Resizable documentation last updated Thursday, June 11, 2026