Skip to content
edmi-ui

Resizable

Panels the user can resize by dragging a handle.

StackBlitz ↗StackBlitz ↗StackBlitz ↗
Chart
Order book
Sidebar
Editor
Agent log
Chart
Order book
Sidebar
Editor
Agent log
Chart
Order book
Sidebar
Editor
Agent log
Terminal window
npx shadcn@latest add @edmi-ui/resizable
Terminal window
pnpm dlx shadcn@latest add @edmi-ui/resizable
Terminal window
yarn dlx shadcn@latest add @edmi-ui/resizable
Terminal window
bunx shadcn@latest add @edmi-ui/resizable

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-vue@latest add @edmi-ui/resizable
Terminal window
pnpm dlx shadcn-vue@latest add @edmi-ui/resizable
Terminal window
yarn dlx shadcn-vue@latest add @edmi-ui/resizable
Terminal window
bunx shadcn-vue@latest add @edmi-ui/resizable

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/resizable.json
Terminal window
pnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/resizable.json
Terminal window
yarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/resizable.json
Terminal window
bunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/resizable.json

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

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>
registry/ui/resizable.tsx
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 };
registry/ui/resizable/ResizableHandle.vue
<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>
registry/ui/resizable/ResizablePanel.vue
<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>
registry/ui/resizable/ResizablePanelGroup.vue
<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>
registry/ui/resizable/index.ts
export { default as ResizableHandle } from "./ResizableHandle.vue";
export { default as ResizablePanel } from "./ResizablePanel.vue";
export { default as ResizablePanelGroup } from "./ResizablePanelGroup.vue";
src/lib/registry/ui/resizable/resizable-handle.svelte
<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>
src/lib/registry/ui/resizable/resizable-pane-group.svelte
<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}
/>
src/lib/registry/ui/resizable/index.ts
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).

  • Handle grip is the Edmi 12x22 chip (--card fill, 1px --border).
  • Vue: ResizablePanelGroup, ResizablePanel, ResizableHandle (Reka Splitter). Takes direction and a numeric default-size; the panel group needs an id for SSR and persisted layouts.
  • Svelte: Resizable.PaneGroup, Resizable.Pane, Resizable.Handle (paneforge). Takes direction and a numeric defaultSize.