Resizable

Accessible resizable panel groups and layouts with keyboard support.

Examples

Basic

PropDefaultTypeDescription
autoSaveIdnullstring, nullUnique id used to auto-save group arrangement via localStorage.
direction*-vertical, horizontalThe group orientation of resizable; required prop
id-string, nullGroup id; falls back to useId when not provided.
keyboardResizeBy10number, nullStep size when arrow key was pressed.
storagedefaultStoragePanelGroupStorageCustom storage API; defaults to localStorage
Preview
Code
Sidebar
Header
Content

Handle

PropDefaultTypeDescription
iconi-lucide-grip-verticalboolean, stringAdd an icon to the resizable handle, falls back to i-lucide-grip-vertical when true.
resizableHandlesolid-gray{variant}-{color}Custom handle color of resizable handle. Note that this does not apply to the icon.
disabled-booleanDisable drag handle
id-stringResize handle id (unique within group); falls back to useId when not provided.
VariantDescription
solidUses border to create a solid handle.
outlineUses ring to create an outline handle.
~The unstyle or base variant
Preview
Code
1
2
3
4

Panel

PropDefaultTypeDescription
collapsedSize-numberThe size of panel when it is collapsed.
collapsible-booleanShould panel collapse when resized beyond its minSize. When true, it will be collapsed to collapsedSize.
id-string, nullPanel id (unique within group); falls back to useId when not provided.
defaultSize-numberInitial size of panel (numeric value between 1-100)
maxSize-numberThe maximum allowable size of panel (numeric value between 1-100); defaults to 100
minSize-numberThe minimum allowable size of panel (numeric value between 1-100); defaults to 10
order-numberThe order of panel within group; required for groups with conditionally rendered panels
Preview
Code
Main

Presets

shortcuts/resizable.ts
type ResizablePrefix = 'resizable'

export const staticResizable: Record<`${ResizablePrefix}-${string}`, string> = {
  // config
  'resizable-handle-icon-name': 'i-lucide-grip-vertical',

  // base
  'resizable-panel-group': 'flex h-full w-full data-[orientation=vertical]:flex-col',
  'resizable-panel': '',
  'resizable-handle': 'relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:ring-offset-base focus-visible:outline-none data-[orientation=vertical]:h-px data-[orientation=vertical]:w-full data-[orientation=vertical]:after:left-0 data-[orientation=vertical]:after:h-1 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',
  'resizable-handle-icon-wrapper': 'bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border',
  'resizable-handle-icon': 'square-2.5',

  // static variants
  'resizable-handle-solid-gray': 'bg-border focus-visible:ring-foreground/58',
  'resizable-handle-solid-black': 'bg-inverted focus-visible:ring-foreground/58',
  'resizable-handle-outline-gray': 'ring-1 ring-base focus-visible:ring-foreground/58',
}

export const dynamicResizable: [RegExp, (params: RegExpExecArray) => string][] = [
  [/^resizable-handle-solid(-(\S+))?$/, ([, , c = 'gray']) => `bg-${c}-200 dark:bg-${c}-700/58 focus-visible:ring-${c}-200 dark:focus-visible:ring-${c}-700/58`],
  [/^resizable-handle-outline(-(\S+))?$/, ([, , c = 'gray']) => `ring-1 ring-${c}-200 dark:ring-${c}-700/58 focus-visible:ring-${c}-200 dark:focus-visible:ring-${c}-700/58`],
]

export const resizable = [
  ...dynamicResizable,
  staticResizable,
]

Props

types/resizable.ts
import type { SplitterGroupProps, SplitterPanelProps, SplitterResizeHandleProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'

interface BaseExtension {
  /** CSS class for the component */
  class?: HTMLAttributes['class']
}

export interface NResizablePanelGroupProps extends SplitterGroupProps, BaseExtension {
  /** Additional properties for the una component */
  una?: Pick<NResizableUnaProps, 'resizablePanelGroup'>
}

export interface NResizablePanelProps extends SplitterPanelProps, BaseExtension {
  /** Additional properties for the una component */
  una?: Pick<NResizableUnaProps, 'resizablePanel'>
}

export interface NResizableHandleProps extends SplitterResizeHandleProps, BaseExtension {
  /**
   * Allows you to add `UnaUI` resizable handle preset properties,
   * Think of it as a shortcut for adding options or variants to the preset if available.
   *
   * @see https://github.com/una-ui/una-ui/blob/main/packages/preset/src/_shortcuts/resizable.ts
   * @example
   * resizable-handle="outline-yellow"
   */
  resizableHandle?: string
  /**
   * Custom handle icon
   * @example
   * icon="i-lucide-grip-vertical"
   */
  icon?: boolean | HTMLAttributes['class']
  /** Additional properties for the una component */
  una?: Pick<NResizableUnaProps, 'resizableHandle' | 'resizableHandleIconWrapper' | 'resizableHandleIcon'>
}

interface NResizableUnaProps {
  /** CSS class for the resizable panel */
  resizablePanel?: HTMLAttributes['class']
  /** CSS class for the resizable panel group */
  resizablePanelGroup?: HTMLAttributes['class']
  /** CSS class for the resizable handle */
  resizableHandle?: HTMLAttributes['class']
  /** CSS class for the resizable handle icon wrapper */
  resizableHandleIconWrapper?: HTMLAttributes['class']
  /** CSS class for the resizable handle icon */
  resizableHandleIcon?: HTMLAttributes['class']
}

Components

ResizablePanelGroup.vue
ResizableHandle.vue
ResizablePanel.vue
<script setup lang="ts">
import type { SplitterGroupEmits } from 'reka-ui'
import type { NResizablePanelGroupProps } from '../../types'
import { reactiveOmit } from '@vueuse/core'
import { SplitterGroup, useForwardPropsEmits } from 'reka-ui'
import { cn } from '../../utils'

const props = defineProps<NResizablePanelGroupProps>()
const emits = defineEmits<SplitterGroupEmits>()

const delegatedProps = reactiveOmit(props, ['class', 'una'])

const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>

<template>
  <SplitterGroup
    v-slot="{ ...slotProps }"
    data-slot="resizable-panel-group"
    v-bind="forwarded"
    :class="cn(
      'resizable-panel-group',
      props.una?.resizablePanelGroup,
      props.class,
    )"
  >
    <slot v-bind="slotProps" />
  </SplitterGroup>
</template>