Drag-to-resize container with keyboard nudge controls and breakpoint awareness.
Current breakpoint:
px
Focus the drag handle and use arrow keys to resize by keyboard.
Alpine.js Resizable Container
huxResizable controls a container width with pointer and keyboard interactions, tracks the currently matched breakpoint, and can clamp width to min/max or parent bounds.
API
huxResizable(config)
Returns an Alpine data object with:
currentWidth: numberminWidth: numbermaxWidth: number | nullpreventOverflow: booleanstopAtLastBreakpoint: booleaniframeSrc: string | nullbreakpointItems: Array<{ label: string, minWidth: number }>activeBreakpoint: string | nullstartDrag(pointerEvent): voidnudgeWidth(xDelta: number): voidjumpToLabel(breakpointLabel: string): voidjumpToBreakpoint(xDirection: 'next' | 'prev' | 'first' | 'last'): voiddestroy(): void
Internal helper methods are private implementation details and are not part of the supported API contract.
Options
breakpointItems: Array<{ label: string, minWidth: number }>(default: xs/sm/md/lg/xl)minWidth: number(default:375)maxWidth: number | null(default:null)preventOverflow: boolean(default:true)stopAtLastBreakpoint: boolean(default:false)iframeSrc: string | null(default:null)
Quick Start
<div x-data="huxResizable({ iframeSrc: '/examples/preview.html', stopAtLastBreakpoint: true })">
<div
x-ref="resizableContainer"
x-bind:style="`width: ${currentWidth}px; min-width: ${minWidth}px;`"
>
<iframe x-ref="iframeEl" x-bind:src="iframeSrc" title="Preview"></iframe>
<button
type="button"
aria-label="Drag to resize"
x-on:mousedown="startDrag($event)"
x-on:touchstart.prevent="startDrag($event)"
x-on:keydown.right.prevent="nudgeWidth(8)"
x-on:keydown.left.prevent="nudgeWidth(-8)"
x-on:keydown.up.prevent="jumpToBreakpoint('next')"
x-on:keydown.down.prevent="jumpToBreakpoint('prev')"
></button>
</div>
</div>Common Usage Patterns
Clamp to Parent Width
huxResizable({
preventOverflow: true,
})Cap at Largest Breakpoint
huxResizable({
stopAtLastBreakpoint: true,
})Custom Breakpoints
huxResizable({
breakpointItems: [
{ label: 'mobile', minWidth: 360 },
{ label: 'tablet', minWidth: 768 },
{ label: 'desktop', minWidth: 1200 },
],
})Dynamic iframe Source
Pass iframeSrc to load a specific URL into the preview iframe. This allows multiple resizable containers on the same page to each load different content without duplicating markup.
huxResizable({
iframeSrc: '/preview/my-component',
})When iframeSrc is null (the default), the iframe renders without a src attribute and displays a blank page.
Behavior Contract
maxWidthlower thanminWidthis normalized up tominWidthduring init.breakpointItemsare filtered to valid shape and sorted byminWidthascending.currentWidthinitializes fromresizableContainer.offsetWidthwhen available.nudgeWidth(),jumpToLabel(), andjumpToBreakpoint()always apply clamp logic before updating width.jumpToBreakpoint()no-ops when direction is invalid or no breakpoints exist.destroy()removes pointer listeners and disconnects resize observer.
Error Handling
- Invalid breakpoint entries are ignored.
- Missing container refs fall back safely to width defaults; keyboard methods still no-op safely where needed.
- Unknown breakpoint labels in
jumpToLabel()no-op.
Accessibility Notes
- Keep the drag handle as
button type="button"with an explicitaria-label. - Support keyboard resizing (
ArrowLeft/ArrowRight) and breakpoint jumps (ArrowUp/ArrowDown,Home,End). - Expose current breakpoint changes with
aria-live="polite"status text. - Ensure the active resize affordance has visible focus styles.
Notes
destroy()is part of the supported API here because it performs listener and observer cleanup for long-lived contexts.