DocsComponentsResizable
Resizable
Panels you resize by dragging the handle between them, or with the arrow keys.
Alpine.js3 files
<pb-ui.resizable-group direction="horizontal" class="max-w-lg rounded-lg border" style="height: 260px">
<pb-ui.resizable-panel :size="50"><div class="flex h-full items-center justify-center p-6 font-semibold">One</div></pb-ui.resizable-panel>
<pb-ui.resizable-handle with_handle />
<pb-ui.resizable-panel :size="50">
<pb-ui.resizable-group direction="vertical">
<pb-ui.resizable-panel :size="25"><div class="flex h-full items-center justify-center p-6 font-semibold">Two</div></pb-ui.resizable-panel>
<pb-ui.resizable-handle />
<pb-ui.resizable-panel :size="75"><div class="flex h-full items-center justify-center p-6 font-semibold">Three</div></pb-ui.resizable-panel>
</pb-ui.resizable-group>
</pb-ui.resizable-panel>
</pb-ui.resizable-group>Installation
pyblade-ui add resizable- Install the dependencies
Alpine.js, loaded in your
<head>. - Copy these files into your projectcomponents/ui/resizable_group.html
@props({'direction': 'horizontal'}) <div data-slot="resizable-group" data-direction="{{ direction }}" x-data="{ direction: '{{ direction }}' }" {{ attributes.cn( "flex h-full w-full", {'horizontal': 'flex-row', 'vertical': 'flex-col'}[direction], ) }}>{{ slot }}</div>components/ui/resizable_panel.html@props({'size': 50, 'min': 10}) <div data-slot="resizable-panel" data-min="{{ min }}" style="flex: {{ size }} 1 0px" {{ attributes.cn("min-h-0 min-w-0 overflow-hidden") }}>{{ slot }}</div>components/ui/resizable_handle.html@props({'with_handle': False}) <div role="separator" tabindex="0" data-slot="resizable-handle" data-orientation="horizontal" x-bind:data-orientation="direction" x-bind:aria-orientation="direction === 'horizontal' ? 'vertical' : 'horizontal'" x-data="{ from: null, grow(el) { return parseFloat(el.style.flexGrow) || 1 }, resize(delta) { const a = $el.previousElementSibling, b = $el.nextElementSibling, total = this.grow(a) + this.grow(b); const min = Math.max(parseFloat(a.dataset.min) || 0, 0) / 100 * total, minB = (parseFloat(b.dataset.min) || 0) / 100 * total; const next = Math.min(Math.max(this.grow(a) + delta * total, min), total - minB); a.style.flexGrow = next; b.style.flexGrow = total - next; }, down(e) { this.from = direction === 'horizontal' ? e.clientX : e.clientY; $el.setPointerCapture(e.pointerId) }, move(e) { if (this.from === null) return; const a = $el.previousElementSibling, b = $el.nextElementSibling, pos = direction === 'horizontal' ? e.clientX : e.clientY; const span = direction === 'horizontal' ? a.offsetWidth + b.offsetWidth : a.offsetHeight + b.offsetHeight; this.resize((pos - this.from) / span); this.from = pos; }, }" x-on:pointerdown="down($event)" x-on:pointermove="move($event)" x-on:pointerup="from = null" x-on:pointercancel="from = null" x-on:keydown.left.prevent="direction === 'horizontal' && resize(-0.05)" x-on:keydown.right.prevent="direction === 'horizontal' && resize(0.05)" x-on:keydown.up.prevent="direction === 'vertical' && resize(-0.05)" x-on:keydown.down.prevent="direction === 'vertical' && resize(0.05)" {{ attributes.cn( "relative flex shrink-0 touch-none items-center justify-center bg-border outline-none select-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1", "w-px cursor-col-resize after:absolute after:inset-y-0 after:left-1/2 after:w-2 after:-translate-x-1/2 data-[orientation=vertical]:h-px data-[orientation=vertical]:w-full data-[orientation=vertical]:cursor-row-resize data-[orientation=vertical]:after:inset-x-0 data-[orientation=vertical]:after:inset-y-auto data-[orientation=vertical]:after:left-0 data-[orientation=vertical]:after:h-2 data-[orientation=vertical]:after:w-full data-[orientation=vertical]:after:translate-x-0 data-[orientation=vertical]:after:-translate-y-1/2", ) }}> @if(with_handle) <div class="z-10 flex h-4 w-3 items-center justify-center rounded-xs border bg-border" x-bind:class="{ 'rotate-90': direction === 'vertical' }"> <svg class="size-2.5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="9" cy="12" r="1"/><circle cx="9" cy="5" r="1"/><circle cx="9" cy="19" r="1"/><circle cx="15" cy="12" r="1"/><circle cx="15" cy="5" r="1"/><circle cx="15" cy="19" r="1"/></svg> </div> @endif </div> - Update the path to fit your project
The
componentsfolder is the one set inpyblade.toml.
Usage
The tags <pb-ui.resizable-group> <pb-ui.resizable-panel> <pb-ui.resizable-handle> are available in all your templates.
<pb-ui.resizable-group direction="horizontal" class="min-h-48 rounded-lg border">
<pb-ui.resizable-panel :size="50"><div class="p-6">One</div></pb-ui.resizable-panel>
<pb-ui.resizable-handle with_handle />
<pb-ui.resizable-panel :size="50"><div class="p-6">Two</div></pb-ui.resizable-panel>
</pb-ui.resizable-group>Props
<pb-ui.resizable-group>
| Prop | Default |
|---|---|
| direction | 'horizontal' |
<pb-ui.resizable-panel>
| Prop | Default |
|---|---|
| size | 50 |
| min | 10 |
<pb-ui.resizable-handle>
| Prop | Default |
|---|---|
| with_handle | False |
Any other attribute (class, id, x-on:click, hx-post…) is passed to the root element; your Tailwind classes win over the component's.