<script setup>
import { Resizable } from "@dicehub/kappa/components/resizable";
</script>
<template>
<Resizable.Root
class="layout"
:panels="[
{ id: 'navigation', minSize: 22 },
{ id: 'inspector', minSize: 32 },
]"
:default-size="[36, 64]"
aria-label="Workspace layout"
>
<Resizable.Panel id="navigation">Navigation</Resizable.Panel>
<Resizable.Handle id="navigation:inspector" aria-label="Resize navigation and inspector" />
<Resizable.Panel id="inspector">Inspector</Resizable.Panel>
</Resizable.Root>
</template>
<style>
.layout { inline-size: 100%; block-size: 16rem; }
.layout [data-slot="resizable-panel"] { display: grid; place-items: center; padding: 1rem; font-weight: 650; }
</style>Installation
Resizable is part of the main Kappa package. Ark UI owns panel measurements, pointer capture, keyboard behavior, and splitter semantics; Kappa provides the visual divider, grip, and semantic token treatment.
Barrel
import { Resizable } from "@dicehub/kappa";Granular
import {
Resizable,
ResizableHandle,
ResizablePanel,
ResizableResizeTrigger,
ResizableResizeTriggerIndicator,
} from "@dicehub/kappa/components/resizable";Usage
Declare every panel in panels, render a Panel for each id, and place a Handle between adjacent panels. Give Root a size through its parent or a block-size class so the divider has a measurable area. Kappa panels are scroll containers, so long content can scroll without changing the splitter’s flex sizing.
<script setup>
import { Resizable } from "@dicehub/kappa/components/resizable";
</script>
<template>
<Resizable.Root
class="workspace"
:panels="[
{ id: 'files', minSize: 25 },
{ id: 'preview', minSize: 30 },
]"
:default-size="[32, 68]"
aria-label="File preview layout"
>
<Resizable.Panel id="files">Files</Resizable.Panel>
<Resizable.Handle id="files:preview" aria-label="Resize files and preview" />
<Resizable.Panel id="preview">Preview</Resizable.Panel>
</Resizable.Root>
</template>
<style>
.workspace { inline-size: 100%; block-size: 16rem; }
.workspace [data-slot="resizable-panel"] { display: grid; place-items: center; padding: 1rem; font-weight: 650; }
</style>Use the full id form before:after for predictable composition. A Handle is a Kappa alias for ResizeTrigger and includes ResizeTriggerIndicator when it has no custom slot.
Composition
Resizable.Root
├── Resizable.Panel
├── Resizable.Handle / Resizable.ResizeTrigger
│ └── Resizable.ResizeTriggerIndicator
├── Resizable.Panel
└── Resizable.Context (optional)Ark UI's splitter machine remains the behavior layer. Kappa wraps the current Ark Vue 5.39.1 API and adds named parts, styling, a default grip for Handle, and the same Root / RootProvider / Context composition model.
Use RootProvider with useSplitter when the machine belongs in setup code. Use Context to call setSizes, collapsePanel, expandPanel, or other API methods from a descendant.
<script setup>
import { useSplitter } from "@dicehub/kappa/components/resizable";
import { Resizable } from "@dicehub/kappa/components/resizable";
const api = useSplitter({
panels: [
{ id: "left", minSize: 25 },
{ id: "right", minSize: 25 },
],
defaultSize: [40, 60],
});
</script>
<template>
<Resizable.RootProvider :value="api">
<!-- Compose Panel, Handle, and ResizeTriggerIndicator parts. -->
</Resizable.RootProvider>
</template>Examples
Vertical
Set orientation to vertical when the divider should separate stacked panels.
<script setup>
import { Resizable } from "@dicehub/kappa/components/resizable";
</script>
<template>
<Resizable.Root
class="layout"
orientation="vertical"
:panels="[
{ id: 'chart', minSize: 30 },
{ id: 'events', minSize: 22 },
]"
:default-size="[62, 38]"
aria-label="Chart and events layout"
>
<Resizable.Panel id="chart">Chart</Resizable.Panel>
<Resizable.Handle id="chart:events" aria-label="Resize chart and events" />
<Resizable.Panel id="events">Events</Resizable.Panel>
</Resizable.Root>
</template>
<style>
.layout { inline-size: 100%; block-size: 16rem; }
.layout [data-slot="resizable-panel"] { display: grid; place-items: center; padding: 1rem; font-weight: 650; }
</style>Handle and indicator
Handle includes a quiet grip by default. Use the Ark-aligned ResizeTrigger and indicator parts for custom content.
<script setup>
import { Resizable } from "@dicehub/kappa/components/resizable";
</script>
<template>
<Resizable.Root
class="layout"
:panels="[
{ id: 'editor', minSize: 28 },
{ id: 'output', minSize: 28 },
]"
:default-size="[58, 42]"
aria-label="Editor and output layout"
>
<Resizable.Panel id="editor">Editor</Resizable.Panel>
<Resizable.ResizeTrigger id="editor:output" aria-label="Resize editor and output">
<Resizable.ResizeTriggerIndicator />
</Resizable.ResizeTrigger>
<Resizable.Panel id="output">Output</Resizable.Panel>
</Resizable.Root>
</template>
<style>
.layout { inline-size: 100%; block-size: 16rem; }
.layout [data-slot="resizable-panel"] { display: grid; place-items: center; padding: 1rem; font-weight: 650; }
</style>Collapsible panels
Mark a panel collapsible, then use Enter on its handle or Context methods to collapse and expand it.
<script setup>
import { Resizable } from "@dicehub/kappa/components/resizable";
</script>
<template>
<Resizable.Root
class="layout"
:panels="[
{ id: 'details', minSize: 20, collapsible: true, collapsedSize: 8 },
{ id: 'summary', minSize: 28 },
]"
:default-size="[34, 66]"
aria-label="Summary and details layout"
>
<Resizable.Panel id="details">Details</Resizable.Panel>
<Resizable.Handle id="details:summary" aria-label="Resize details and summary" />
<Resizable.Panel id="summary">
<Resizable.Context v-slot="api">
<div class="summary">
<span>Summary</span>
<div class="actions">
<button type="button" @click="api.expandPanel('details')">Expand details</button>
<button type="button" @click="api.collapsePanel('details')">Collapse details</button>
</div>
</div>
</Resizable.Context>
</Resizable.Panel>
</Resizable.Root>
</template>
<style>
.layout { inline-size: 100%; block-size: 16rem; }
.layout [data-slot="resizable-panel"] { display: grid; place-items: center; padding: 1rem; font-weight: 650; }
.summary { display: grid; justify-items: center; gap: 0.75rem; }
.actions { display: flex; gap: 0.5rem; }
</style>Nested splitters
Compose a second Root inside a panel when a workbench needs independent horizontal and vertical boundaries.
<script setup>
import { Resizable } from "@dicehub/kappa/components/resizable";
</script>
<template>
<Resizable.Root
class="layout"
:panels="[
{ id: 'outline', minSize: 22 },
{ id: 'workbench', minSize: 38 },
]"
:default-size="[28, 72]"
aria-label="Project outline and workbench"
>
<Resizable.Panel id="outline">Outline</Resizable.Panel>
<Resizable.Handle id="outline:workbench" aria-label="Resize outline and workbench" />
<Resizable.Panel id="workbench" class="nested-host">
<Resizable.Root
class="nested-layout"
orientation="vertical"
:panels="[
{ id: 'code', minSize: 30 },
{ id: 'terminal', minSize: 24 },
]"
:default-size="[64, 36]"
aria-label="Code and terminal layout"
>
<Resizable.Panel id="code">Code</Resizable.Panel>
<Resizable.Handle id="code:terminal" aria-label="Resize code and terminal" />
<Resizable.Panel id="terminal">Terminal</Resizable.Panel>
</Resizable.Root>
</Resizable.Panel>
</Resizable.Root>
</template>
<style>
.layout { inline-size: 100%; block-size: 18rem; }
.layout [data-slot="resizable-panel"] { display: grid; place-items: center; padding: 1rem; font-weight: 650; }
.nested-host { place-items: stretch; }
.nested-layout { inline-size: 100%; block-size: auto; align-self: stretch; }
</style>Controlled layout
Use v-model:size when application state or another control owns the current layout.
<script setup>
import { ref } from "vue";
import { Resizable } from "@dicehub/kappa/components/resizable";
const size = ref([42, 58]);
</script>
<template>
<Resizable.Root
v-model:size="size"
class="layout"
:panels="[
{ id: 'canvas', minSize: 25 },
{ id: 'metrics', minSize: 25 },
]"
aria-label="Controlled canvas and metrics layout"
>
<Resizable.Panel id="canvas">Canvas · {{ size[0] }}%</Resizable.Panel>
<Resizable.Handle id="canvas:metrics" aria-label="Resize canvas and metrics" />
<Resizable.Panel id="metrics">Metrics · {{ size[1] }}%</Resizable.Panel>
</Resizable.Root>
<div class="actions">
<button type="button" @click="size = [28, 72]">28 / 72</button>
<button type="button" @click="size = [60, 40]">60 / 40</button>
</div>
</template>
<style>
.layout { inline-size: 100%; block-size: 16rem; }
.layout [data-slot="resizable-panel"] { display: grid; place-items: center; padding: 1rem; font-weight: 650; }
.actions { display: flex; justify-content: end; gap: 0.5rem; margin-block-start: 0.75rem; }
</style>Accessibility
- Give every Handle or ResizeTrigger a concise accessible label. Ark UI exposes a separator role, orientation, current value, limits, and controlled panel ids.
- Keep panel ids unique within each Root and keep the panels array synchronized with rendered Panel parts.
- Use visible focus styles. Pointer dragging and keyboard resizing remain available in light, dark, forced-colors, and reduced-motion modes.
- Use
disabledon a divider when that boundary must not move. Use panel constraints for minimum and maximum sizes. - When a panel is collapsible, explain the behavior in nearby text if the affordance is not obvious from the layout.
Keyboard Support
| Key | Behavior |
|---|---|
ArrowLeft / ArrowRight | Resize a horizontal divider by one step. |
ArrowUp / ArrowDown | Resize a vertical divider by one step. |
Shift + Arrow | Resize by 10 px for faster keyboard adjustments. |
Home / End | Move the adjacent panel to its minimum or maximum boundary. |
Enter | Collapse or expand the adjacent panel when it is collapsible. |
F6 / Shift + F6 | Move focus to the next or previous divider. |
API Reference
Resizable.Root
| Prop | Type | Default | Description |
|---|---|---|---|
panels | ResizablePanelData[] | required | Panel ids and optional order, minimum/maximum, and collapse constraints. |
defaultSize / size | number[] | equal / — | Initial or controlled panel percentages. |
orientation | "horizontal" | "vertical" | "horizontal" | Sets the divider axis and keyboard direction. |
keyboardResizeBy | number | 1 px | Pixels moved by an arrow key. Shift + Arrow uses 10 px. |
id / ids | string / Partial<ElementIds> | generated | Stable machine and part identifiers for composition. |
registry | SplitterRegistry | — | Enables coordinated multi-drag interactions between splitters. |
nonce | string | — | Nonce for the global cursor style injected during pointer resize. |
asChild | boolean | false | Merges root behavior into its direct child. |
Parts
| Part | Element | Description |
|---|---|---|
Resizable.Root | div | Owns panel sizes, constraints, orientation, and resize state. |
Resizable.RootProvider | div | Provides an externally created useSplitter machine. |
Resizable.Panel | div | Measured, constrained, and scrollable panel. Its id must exist in Root panels. |
Resizable.Handle | button | Kappa alias for ResizeTrigger with a visible grip indicator by default. |
Resizable.ResizeTrigger | button | Ark-aligned keyboard and pointer divider control. Use id='before:after'. |
Resizable.ResizeTriggerIndicator | div | Optional Ark indicator for a custom ResizeTrigger grip. |
Resizable.Context | renderless | Exposes the splitter API for collapse, expand, and programmatic resizing. |
Events
| Event | Payload | Description |
|---|---|---|
update:size | number[] | Updates v-model after a size change. |
resize | ResizableResizeDetails | Ark invokes this callback for controlled size changes; use update:size for the current model and resizeEnd for interaction completion. |
resizeEnd | ResizableResizeEndDetails | Reports the final sizes after a resize interaction. |
resizeStart | void | Reports the start of a pointer or keyboard resize. |
collapse / expand | ResizableExpandCollapseDetails | Reports a collapsible panel changing state. |
Slots
| Slot | Description |
|---|---|
Root.default / RootProvider.default | Panel and divider parts in their layout order. |
Panel.default | Panel content. Give the panel an id present in Root panels. |
Handle.default | Replaces the default grip indicator when custom handle content is needed. |
ResizeTrigger.default | Custom content for an Ark-aligned divider control. |
ResizeTriggerIndicator.default | Custom indicator content inside a ResizeTrigger. |
Context.default | Receives the renderless ResizableApi value. |
Data Attributes
| Attribute | Value | Description |
|---|---|---|
data-slot | "resizable" and part names | Identifies Kappa roots and parts. |
data-orientation | "horizontal" | "vertical" | Exposes the active axis on roots, panels, handles, and indicators. |
data-id / data-index | panel or trigger identity | Exposes Ark panel and divider identity for styling and testing. |
data-dragging | present while active | Exposes active pointer or keyboard resize state. |
data-focus / data-disabled | present when applicable | Exposes divider focus and disabled states. |
role / aria-valuenow / aria-controls | on ResizeTrigger | Ark UI separator semantics and current layout values. |
Exports
| Export | Description |
|---|---|
Resizable | Compound Ark-backed splitter component. |
ResizableRoot / ResizableRootProvider / ResizablePanel | Named root, provider, and panel components. |
ResizableHandle | Friendly divider alias with a default grip indicator. |
ResizableResizeTrigger / ResizableResizeTriggerIndicator | Ark-aligned divider and optional indicator components. |
ResizableContext | Renderless access to the active splitter API. |
ResizableProps / Resizable*Props / Resizable*Slots / ResizableEmits | Public root, part, slot, and event contracts. |
ResizablePanelData / ResizableApi / ResizableOrientation | Panel constraints, machine API, and orientation types. |
useSplitter / useSplitterContext / splitterAnatomy | Ark UI composition functions and anatomy. |