Resizable
Panels the user can resize by dragging a handle.
import { ResizableHandle, ResizablePanel, ResizablePanelGroup,} from "@/components/ui/resizable";
const pane = "flex h-full items-center justify-center text-sm text-muted-foreground";
export default function Demo() { return ( <div className="flex w-full shrink-0 flex-col gap-6"> <ResizablePanelGroup orientation="horizontal" className="min-h-[180px] w-full max-w-md shrink-0 rounded-xl border border-border bg-card" > <ResizablePanel defaultSize="60%"> <div className={pane}>Chart</div> </ResizablePanel> <ResizableHandle withHandle /> <ResizablePanel defaultSize="40%"> <div className={pane}>Order book</div> </ResizablePanel> </ResizablePanelGroup> <ResizablePanelGroup orientation="horizontal" className="min-h-[220px] w-full max-w-md shrink-0 rounded-xl border border-border bg-card" > <ResizablePanel defaultSize="30%"> <div className={pane}>Sidebar</div> </ResizablePanel> <ResizableHandle withHandle /> <ResizablePanel defaultSize="70%"> <ResizablePanelGroup orientation="vertical" className="h-full"> <ResizablePanel defaultSize="60%"> <div className={pane}>Editor</div> </ResizablePanel> <ResizableHandle withHandle /> <ResizablePanel defaultSize="40%"> <div className={pane}>Agent log</div> </ResizablePanel> </ResizablePanelGroup> </ResizablePanel> </ResizablePanelGroup> </div> );}<script setup lang="ts">import { ResizableHandle, ResizablePanel, ResizablePanelGroup,} from "@/components/ui/resizable";
const pane = "flex h-full items-center justify-center text-sm text-muted-foreground";</script>
<template> <div class="flex w-full shrink-0 flex-col gap-6"> <div class="h-[180px] w-full max-w-md"> <ResizablePanelGroup id="edmi-resizable-a" direction="horizontal" class="rounded-xl border border-border bg-card" > <ResizablePanel :default-size="60"> <div :class="pane">Chart</div> </ResizablePanel> <ResizableHandle with-handle /> <ResizablePanel :default-size="40"> <div :class="pane">Order book</div> </ResizablePanel> </ResizablePanelGroup> </div> <div class="h-[220px] w-full max-w-md"> <ResizablePanelGroup id="edmi-resizable-b" direction="horizontal" class="rounded-xl border border-border bg-card" > <ResizablePanel :default-size="30"> <div :class="pane">Sidebar</div> </ResizablePanel> <ResizableHandle with-handle /> <ResizablePanel :default-size="70"> <ResizablePanelGroup id="edmi-resizable-b-inner" direction="vertical" class="h-full"> <ResizablePanel :default-size="60"> <div :class="pane">Editor</div> </ResizablePanel> <ResizableHandle with-handle /> <ResizablePanel :default-size="40"> <div :class="pane">Agent log</div> </ResizablePanel> </ResizablePanelGroup> </ResizablePanel> </ResizablePanelGroup> </div> </div></template><script lang="ts"> import * as Resizable from "$lib/components/ui/resizable";
const pane = "flex h-full items-center justify-center text-sm text-muted-foreground";</script>
<div class="flex w-full shrink-0 flex-col gap-6"> <div class="h-[180px] w-full max-w-md"> <Resizable.PaneGroup direction="horizontal" class="rounded-xl border border-border bg-card" > <Resizable.Pane defaultSize={60}> <div class={pane}>Chart</div> </Resizable.Pane> <Resizable.Handle withHandle /> <Resizable.Pane defaultSize={40}> <div class={pane}>Order book</div> </Resizable.Pane> </Resizable.PaneGroup> </div> <div class="h-[220px] w-full max-w-md"> <Resizable.PaneGroup direction="horizontal" class="rounded-xl border border-border bg-card" > <Resizable.Pane defaultSize={30}> <div class={pane}>Sidebar</div> </Resizable.Pane> <Resizable.Handle withHandle /> <Resizable.Pane defaultSize={70}> <Resizable.PaneGroup direction="vertical" class="h-full"> <Resizable.Pane defaultSize={60}> <div class={pane}>Editor</div> </Resizable.Pane> <Resizable.Handle withHandle /> <Resizable.Pane defaultSize={40}> <div class={pane}>Agent log</div> </Resizable.Pane> </Resizable.PaneGroup> </Resizable.Pane> </Resizable.PaneGroup> </div></div>Installation
Section titled “Installation”npx shadcn@latest add @edmi-ui/resizablepnpm dlx shadcn@latest add @edmi-ui/resizableyarn dlx shadcn@latest add @edmi-ui/resizablebunx shadcn@latest add @edmi-ui/resizablePick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Install the dependencies:
Terminal window npm install react-resizable-panelsTerminal window pnpm add react-resizable-panelsTerminal window yarn add react-resizable-panelsTerminal window bun add react-resizable-panelsCopy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-vue@latest add @edmi-ui/resizablepnpm dlx shadcn-vue@latest add @edmi-ui/resizableyarn dlx shadcn-vue@latest add @edmi-ui/resizablebunx shadcn-vue@latest add @edmi-ui/resizablePick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Install the dependencies:
Terminal window npm install reka-ui @vueuse/coreTerminal window pnpm add reka-ui @vueuse/coreTerminal window yarn add reka-ui @vueuse/coreTerminal window bun add reka-ui @vueuse/coreCopy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/resizable.jsonpnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/resizable.jsonyarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/resizable.jsonbunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/resizable.jsonPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Copy the code from Component source into your project and update the import paths to match your aliases.
import { ResizableHandle, ResizablePanel, ResizablePanelGroup,} from "@/components/ui/resizable";
<div className="flex w-full shrink-0 flex-col gap-6"> <ResizablePanelGroup orientation="horizontal" className="min-h-[180px] w-full max-w-md shrink-0 rounded-xl border border-border bg-card" > <ResizablePanel defaultSize="60%"> <div>Chart</div> </ResizablePanel> <ResizableHandle withHandle /> <ResizablePanel defaultSize="40%"> <div>Order book</div> </ResizablePanel> </ResizablePanelGroup> {/* … */}</div><script setup lang="ts">import { ResizableHandle, ResizablePanel, ResizablePanelGroup,} from "@/components/ui/resizable";</script>
<template> <div class="flex w-full shrink-0 flex-col gap-6"> <div class="h-[180px] w-full max-w-md"> <ResizablePanelGroup id="edmi-resizable-a" direction="horizontal" class="rounded-xl border border-border bg-card" > <ResizablePanel :default-size="60"> <div>Chart</div> </ResizablePanel> <ResizableHandle with-handle /> <ResizablePanel :default-size="40"> <div>Order book</div> </ResizablePanel> </ResizablePanelGroup> </div> <div class="h-[220px] w-full max-w-md"> <!-- … --> </div></template><script lang="ts"> import * as Resizable from "$lib/components/ui/resizable";</script>
<div class="flex w-full shrink-0 flex-col gap-6"> <div class="h-[180px] w-full max-w-md"> <Resizable.PaneGroup direction="horizontal" class="rounded-xl border border-border bg-card" > <Resizable.Pane defaultSize={60}> <div>Chart</div> </Resizable.Pane> <Resizable.Handle withHandle /> <Resizable.Pane defaultSize={40}> <div>Order book</div> </Resizable.Pane> </Resizable.PaneGroup> </div> <div class="h-[220px] w-full max-w-md"> <!-- … --></div>Component source
Section titled “Component source”import { cn } from "cn";import * as ResizablePrimitive from "react-resizable-panels";import { DotsSixVerticalIcon } from "@phosphor-icons/react";
function ResizablePanelGroup({ className, ...props}: ResizablePrimitive.GroupProps) { return ( <ResizablePrimitive.Group data-slot="resizable-panel-group" className={cn( "flex h-full w-full aria-[orientation=vertical]:flex-col", className, )} {...props} /> );}
function ResizablePanel({ ...props }: ResizablePrimitive.PanelProps) { return <ResizablePrimitive.Panel data-slot="resizable-panel" {...props} />;}
// 1px --border line; `withHandle` adds the 12x22 grip chip (kit.css .grip).function ResizableHandle({ withHandle, className, ...props}: ResizablePrimitive.SeparatorProps & { withHandle?: boolean;}) { return ( <ResizablePrimitive.Separator data-slot="resizable-handle" className={cn( "relative flex w-px items-center justify-center bg-border outline-none after:absolute after:inset-y-0 after:left-1/2 after:w-2 after:-translate-x-1/2 focus-visible:bg-ring aria-[orientation=horizontal]:h-px aria-[orientation=horizontal]:w-full aria-[orientation=horizontal]:after:left-0 aria-[orientation=horizontal]:after:h-2 aria-[orientation=horizontal]:after:w-full aria-[orientation=horizontal]:after:translate-x-0 aria-[orientation=horizontal]:after:-translate-y-1/2 [&[aria-orientation=horizontal]>div]:rotate-90", className, )} {...props} > {withHandle && ( <div className="z-10 flex h-[22px] w-3 shrink-0 items-center justify-center rounded-[4px] border border-border bg-card text-muted-foreground"> <DotsSixVerticalIcon className="size-2.5" /> </div> )} </ResizablePrimitive.Separator> );}
export { ResizableHandle, ResizablePanel, ResizablePanelGroup };<script setup lang="ts">import type { SplitterResizeHandleEmits, SplitterResizeHandleProps } from 'reka-ui'import type { HTMLAttributes } from 'vue'import { GripVertical } from '@lucide/vue'import { reactiveOmit } from '@vueuse/core'import { SplitterResizeHandle, useForwardPropsEmits } from 'reka-ui'import { cn } from '@/registry/edmi/lib/utils'
const props = defineProps<SplitterResizeHandleProps & { class?: HTMLAttributes['class'], withHandle?: boolean }>()const emits = defineEmits<SplitterResizeHandleEmits>()
const delegatedProps = reactiveOmit(props, 'class', 'withHandle')const forwarded = useForwardPropsEmits(delegatedProps, emits)</script>
<template> <!-- 1px --border line; `withHandle` adds the 12x22 grip chip (kit.css .grip). Reka's data-orientation is the group direction. --> <SplitterResizeHandle data-slot="resizable-handle" v-bind="forwarded" :class="cn('relative flex w-px items-center justify-center bg-border outline-none after:absolute after:inset-y-0 after:left-1/2 after:w-2 after:-translate-x-1/2 focus-visible:bg-ring data-[orientation=vertical]:h-px data-[orientation=vertical]:w-full data-[orientation=vertical]:after:left-0 data-[orientation=vertical]:after:h-2 data-[orientation=vertical]:after:w-full data-[orientation=vertical]:after:-translate-y-1/2 data-[orientation=vertical]:after:translate-x-0 [&[data-orientation=vertical]>div]:rotate-90', props.class)" > <div v-if="props.withHandle" class="z-10 flex h-[22px] w-3 shrink-0 items-center justify-center rounded-[4px] border border-border bg-card text-muted-foreground" > <slot> <GripVertical class="size-2.5" /> </slot> </div> </SplitterResizeHandle></template><script setup lang="ts">import type { SplitterPanelEmits, SplitterPanelProps } from 'reka-ui'import { SplitterPanel, useForwardExpose, useForwardPropsEmits } from 'reka-ui'
const props = defineProps<SplitterPanelProps>()const emits = defineEmits<SplitterPanelEmits>()
const forwarded = useForwardPropsEmits(props, emits)const { forwardRef } = useForwardExpose()</script>
<template> <SplitterPanel :ref="forwardRef" v-slot="slotProps" data-slot="resizable-panel" v-bind="forwarded" > <slot v-bind="slotProps" /> </SplitterPanel></template><script setup lang="ts">import type { SplitterGroupEmits, SplitterGroupProps } from 'reka-ui'import type { HTMLAttributes } from 'vue'import { reactiveOmit } from '@vueuse/core'import { SplitterGroup, useForwardPropsEmits } from 'reka-ui'import { cn } from '@/registry/edmi/lib/utils'
const props = defineProps<SplitterGroupProps & { class?: HTMLAttributes['class'] }>()const emits = defineEmits<SplitterGroupEmits>()
const delegatedProps = reactiveOmit(props, 'class')
const forwarded = useForwardPropsEmits(delegatedProps, emits)</script>
<template> <SplitterGroup v-slot="slotProps" data-slot="resizable-panel-group" v-bind="forwarded" :class="cn('flex h-full w-full data-[orientation=vertical]:flex-col', props.class)" > <slot v-bind="slotProps" /> </SplitterGroup></template>export { default as ResizableHandle } from "./ResizableHandle.vue";export { default as ResizablePanel } from "./ResizablePanel.vue";export { default as ResizablePanelGroup } from "./ResizablePanelGroup.vue";<script lang="ts"> import * as ResizablePrimitive from "paneforge"; import DotsSixVerticalIcon from "phosphor-svelte/lib/DotsSixVertical"; import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
let { ref = $bindable(null), class: className, withHandle = false, ...restProps }: WithoutChildrenOrChild<ResizablePrimitive.PaneResizerProps> & { withHandle?: boolean; } = $props();</script>
<!-- 1px --border line; `withHandle` adds the 12x22 grip chip (kit.css .grip). paneforge's data-direction is the group direction. --><ResizablePrimitive.PaneResizer bind:ref data-slot="resizable-handle" class={cn( "relative flex w-px items-center justify-center bg-border outline-none after:absolute after:inset-y-0 after:left-1/2 after:w-2 after:-translate-x-1/2 focus-visible:bg-ring data-[direction=vertical]:h-px data-[direction=vertical]:w-full data-[direction=vertical]:after:left-0 data-[direction=vertical]:after:h-2 data-[direction=vertical]:after:w-full data-[direction=vertical]:after:-translate-y-1/2 data-[direction=vertical]:after:translate-x-0 [&[data-direction=vertical]>div]:rotate-90", className )} {...restProps}> {#if withHandle} <div class="z-10 flex h-[22px] w-3 shrink-0 items-center justify-center rounded-[4px] border border-border bg-card text-muted-foreground" > <DotsSixVerticalIcon class="size-2.5" /> </div> {/if}</ResizablePrimitive.PaneResizer><script lang="ts"> import * as ResizablePrimitive from "paneforge"; import { cn } from "$lib/utils.js";
let { ref = $bindable(null), this: paneGroup = $bindable(), class: className, ...restProps }: ResizablePrimitive.PaneGroupProps & { this?: ResizablePrimitive.PaneGroup; } = $props();</script>
<ResizablePrimitive.PaneGroup bind:ref bind:this={paneGroup} data-slot="resizable-pane-group" class={cn("flex h-full w-full data-[direction=vertical]:flex-col", className)} {...restProps}/>import { Pane } from "paneforge";import Handle from "./resizable-handle.svelte";import PaneGroup from "./resizable-pane-group.svelte";
export { Handle, Handle as ResizableHandle, Pane, Pane as ResizablePane, PaneGroup, // PaneGroup as ResizablePaneGroup,};| Prop | Type | Default |
|---|---|---|
ResizablePanelGroup orientation |
"horizontal" | "vertical" |
"horizontal" |
ResizablePanel defaultSize |
number (px) or string (e.g. "60%") |
— |
ResizableHandle withHandle |
boolean |
false |
Built on react-resizable-panels v4 (orientation, not direction).
✦ Edmi additions
Section titled “✦ Edmi additions”- Handle grip is the Edmi 12x22 chip (
--cardfill, 1px--border).
Vue / Svelte differences
Section titled “Vue / Svelte differences”- Vue:
ResizablePanelGroup,ResizablePanel,ResizableHandle(Reka Splitter). Takesdirectionand a numericdefault-size; the panel group needs anidfor SSR and persisted layouts. - Svelte:
Resizable.PaneGroup,Resizable.Pane,Resizable.Handle(paneforge). Takesdirectionand a numericdefaultSize.