Resizable
Accessible, unstyled single-element resize wrapper with edge and corner handles.
|
|
| Package |
@vizejs/ui/resizable |
| Maturity |
stable |
| Own the source |
vize lib pull resizable |
| Requires |
context, controllable-state, id, move |
| Aliases |
resizable, resize handle, resizable box, element resizer |
| Covers |
WAI-ARIA window splitter (separator) pattern, CSS resize property, re-resizable |
Minimal setup
Use a Vue 3.5+ project with Vue SFC compilation configured (see Vite+ integration).
vp install @vizejs/ui
Add the public import below to <script setup lang="ts">. Components provide behavior without styles; the preview uses optional Paper styles.
import { Resizable, ResizableRoot, ResizableHandle } from "@vizejs/ui/resizable";
Or copy the source into your project with vize lib pull resizable (see Source Distribution).
Try it
This is the basic example maintained with the component source, using the public package imports. The preview adds the optional Paper styles.
Open the interactive example · Styling these components
Copy the example
<!-- Notes panel with a controlled size that can be resized from its right edge and bottom-right corner. -->
<script setup lang="ts">
import { ref } from "vue";
import { Resizable, ResizableHandle } from "@vizejs/ui/resizable";
import type { ResizableSize } from "@vizejs/ui/resizable";
const size = ref<ResizableSize>({ width: 320, height: 200 });
</script>
<template>
<div>
<Resizable
v-model:size="size"
:min-width="200"
:max-width="560"
:min-height="120"
:max-height="400"
:step="10"
>
<p>Meeting notes: drag a handle or focus it and use the arrow keys.</p>
<ResizableHandle edge="e" aria-label="Resize notes width" />
<ResizableHandle edge="se" aria-label="Resize notes" />
</Resizable>
<output>{{ size.width }} × {{ size.height }} px</output>
</div>
</template>
API
Resizable
Source: src/families/layout/resizable/resizable-root.vue
Props
| Prop |
Type |
Default |
Description |
id |
string | null |
undefined |
Consumer-owned root id. null and undefined select a deterministic fallback. |
size |
ResizableSize |
undefined |
Controlled size in CSS pixels. undefined selects uncontrolled behavior. |
defaultSize |
ResizableSize |
{ width: 320, height: 240 } |
Initial size for uncontrolled use. |
minWidth |
number |
0 |
Minimum width in CSS pixels. |
maxWidth |
number |
Infinity |
Maximum width in CSS pixels. |
minHeight |
number |
0 |
Minimum height in CSS pixels. |
maxHeight |
number |
Infinity |
Maximum height in CSS pixels. |
lockAspectRatio |
boolean | number |
false |
Preserve the aspect ratio: true keeps the ratio from the start of each interaction, a number fixes width / height. |
step |
number |
10 |
Arrow-key step in CSS pixels. |
largeStep |
number |
50 |
Shift+Arrow step in CSS pixels. |
disabled |
boolean |
false |
Ignore pointer and keyboard resizing. |
dir |
ResizableDirection |
undefined |
Reading direction used to resolve start and end handles. undefined inherits DirectionProvider/LocaleProvider, then "ltr". |
Events
| Event |
Payload |
Description |
update:size |
[value: ResizableSize] |
Fired when the size requests a controlled value. |
resize-start |
[event: ResizableResizeEvent] |
Fired when a pointer or keyboard resize begins. |
resize |
[event: ResizableResizeEvent] |
Fired for every distinct size produced by an interaction. |
resize-end |
[event: ResizableResizeEvent] |
Fired when a pointer or keyboard resize ends. |
Slots
| Slot |
Slot props |
Description |
default |
ResizableSlotState |
Resizable contents and ResizableHandle parts. Receives the current size. |
Exposed
| Member |
Type |
Description |
disabled |
— |
|
element |
Readonly<ShallowRef<HTMLElement | null>> |
Template ref to the rendered root element. |
setSize |
— |
|
size |
— |
|
state |
— |
|
ResizableRoot
Source: src/families/layout/resizable/resizable-root.vue
Props
| Prop |
Type |
Default |
Description |
id |
string | null |
undefined |
Consumer-owned root id. null and undefined select a deterministic fallback. |
size |
ResizableSize |
undefined |
Controlled size in CSS pixels. undefined selects uncontrolled behavior. |
defaultSize |
ResizableSize |
{ width: 320, height: 240 } |
Initial size for uncontrolled use. |
minWidth |
number |
0 |
Minimum width in CSS pixels. |
maxWidth |
number |
Infinity |
Maximum width in CSS pixels. |
minHeight |
number |
0 |
Minimum height in CSS pixels. |
maxHeight |
number |
Infinity |
Maximum height in CSS pixels. |
lockAspectRatio |
boolean | number |
false |
Preserve the aspect ratio: true keeps the ratio from the start of each interaction, a number fixes width / height. |
step |
number |
10 |
Arrow-key step in CSS pixels. |
largeStep |
number |
50 |
Shift+Arrow step in CSS pixels. |
disabled |
boolean |
false |
Ignore pointer and keyboard resizing. |
dir |
ResizableDirection |
undefined |
Reading direction used to resolve start and end handles. undefined inherits DirectionProvider/LocaleProvider, then "ltr". |
Events
| Event |
Payload |
Description |
update:size |
[value: ResizableSize] |
Fired when the size requests a controlled value. |
resize-start |
[event: ResizableResizeEvent] |
Fired when a pointer or keyboard resize begins. |
resize |
[event: ResizableResizeEvent] |
Fired for every distinct size produced by an interaction. |
resize-end |
[event: ResizableResizeEvent] |
Fired when a pointer or keyboard resize ends. |
Slots
| Slot |
Slot props |
Description |
default |
ResizableSlotState |
Resizable contents and ResizableHandle parts. Receives the current size. |
Exposed
| Member |
Type |
Description |
size |
ResizableSize |
Current size. |
state |
ResizableState |
Interaction state. |
disabled |
boolean |
Whether resizing is disabled. |
element |
HTMLDivElement | null |
Rendered root element. |
setSize |
(size: ResizableSize) => boolean |
Request a size (clamped to the constraints) and report whether it changed. |
ResizableHandle
Source: src/families/layout/resizable/resizable-handle.vue
Props
| Prop |
Type |
Default |
Description |
edge |
ResizableEdge |
"se" |
Edge or corner this handle resizes from. start/end follow the root dir. |
ariaLabel |
string |
undefined |
Accessible name. undefined describes the resolved edge, e.g. "Resize from right edge". |
Slots
| Slot |
Slot props |
Description |
default |
ResizableSlotState |
Optional grip contents. Receives the current Resizable state. |
Exposed
| Member |
Type |
Description |
element |
HTMLDivElement | null |
Rendered separator element. |
physicalEdge |
ResizablePhysicalEdge |
Physical edge resolved from edge and the root dir. |
focus |
(options?: FocusOptions) => void |
Move focus to the handle. |
Behavior
Normative behavior for the @vizejs/ui/resizable single-element resize wrapper. It sizes
one element from edge and corner handles; use @vizejs/ui/splitter to divide space between
sibling panels. Every row is proven by the named mounted-DOM, SSR, or compile-time test.
| State x input |
Observable outcome |
Proven by |
| any render |
Root applies width/height and --vize-resizable-width/-height; handles are focusable role="separator" with aria-controls, per-axis orientation, aria-valuenow/min/max (infinite maxima omitted), and corner handles add aria-valuetext for both dimensions. |
renders a sized root and focusable separator handles with value semantics |
| Arrow keys on a handle |
The outward arrow grows and the inward arrow shrinks by step, Shift uses largeStep, off-axis arrows are not consumed, Home/End jump to the minimum/maximum; each key emits resize-start, resize, resize-end with source: "keyboard" and update:size. |
arrow keys resize by step, Shift by the large step, and Home/End jump to limits |
| pointer drag on a handle |
The handle captures the pointer, the size follows the accumulated movement from the pressed edge or corner, clamps to min/max, and data-state is resizing until release. |
pointer drag resizes from the pressed edge with capture and clamps to constraints |
lockAspectRatio |
true keeps the ratio from the start of the interaction; a number fixes width / height and ratio-derived dimensions still respect limits. |
aspect-ratio lock keeps width and height proportional |
start/end edges with dir |
Logical edges resolve to physical data-edge values through the root dir (end is the left edge in RTL). |
logical start and end handles follow the reading direction |
disabled |
Handles leave the tab order, report aria-disabled, and ignore keyboard and pointer input without events. |
disabled roots ignore keyboard and pointer resizing |
controlled size |
Requests emit update:size while the rendered size follows the prop. |
controlled size emits requests and follows the parent |
| root expose |
size, state, disabled, element, and a clamped setSize are available. |
root exposes size, state, and a clamped setSize |
| geometry helpers |
resolveResizableEdge, resizeByDelta, and constrainResizableSize are pure and ratio-aware. |
geometry helpers resolve edges, deltas, and ratio-aware clamping |
| handle outside a root |
Mounting throws VIZE_UI_CONTEXT_MISSING. |
handles require a Resizable root |
| SSR |
Isolated requests render byte-identical sized markup with separator values. |
renders byte-identical sized markup across isolated SSR requests |
| hydration |
Hydration reuses server handles with zero warnings and keyboard resizing works afterwards. |
hydrates handles without mismatches |
| public types |
Edges, sizes, sources, and lockAspectRatio are closed, typed contracts. |
src/families/layout/resizable/resizable.types.test-d.ts |
| DOM/SSR/Vapor |
Root and handle compile in each renderer lane. |
scripts/check-renderers.ts |
Components
| Component |
Contract |
resizable-root.vue |
Owns the controlled/uncontrolled size, constraints, aspect-ratio lock, and resize lifecycle. |
resizable-handle.vue |
Separator for one edge or corner; pointer drag through move and keyboard resizing. |
Resizable ships no stylesheet. Consumers position handles with data-edge and style
data-state.