Skip to main content
Urbicon UI

SplitPaneexperimental

Resizable two-pane layout with a draggable divider.

Playground

Start pane
End pane
Orientation
<SplitPane>
  {#snippet start()}
    <div class="bg-surface-elevated flex h-full items-center justify-center p-4">
      <span class="text-text-secondary text-sm font-medium">Start pane</span>
    </div>
  {/snippet}
  {#snippet end()}
    <div class="flex h-full items-center justify-center p-4">
      <span class="text-text-secondary text-sm font-medium">End pane</span>
    </div>
  {/snippet}
</SplitPane>

01 Examples

SplitPane fills its container, so give that container a height. The first pane's share is a 0 to 1 number: start it with defaultRatio, or take control with bind:ratio to read and persist the layout (to localStorage, say), while defaultRatio stays the double-click reset target. min and max bound the pane as a px number or a percentage string, and onRatioChange fires after each drag or keyboard resize.

IDE layout: file tree and editor

A two-pane workspace: a file tree bound with bind:ratio next to the editor. Drag the divider or focus it and use the arrow keys. Double-click resets to defaultRatio.

// main.ts

import App from './App.svelte';

import './styles.css';

export default new App(options);

<div class="border-border-subtle h-80 w-full overflow-hidden rounded-xl border">
  <SplitPane bind:ratio={ideRatio} defaultRatio={0.28} min="18%" max="55%">
    {#snippet start()}
      <nav aria-label="Demo sidebar" class="bg-surface-elevated h-full overflow-auto p-3">
        <p class="text-text-tertiary mb-2 px-1 text-xs font-semibold tracking-wide uppercase">
          Explorer
        </p>
        <ul class="space-y-0.5 text-sm">
          <li class="text-text-secondary flex items-center gap-2 px-1 py-1">
            <FolderOpenIcon class="size-4" /> src
          </li>
          <li class="text-text-secondary flex items-center gap-2 px-1 py-1 pl-5">
            <FileIcon class="size-4" /> App.svelte
          </li>
          <li
            class="text-text-primary bg-primary-subtle flex items-center gap-2 rounded px-1 py-1 pl-5 font-medium"
          >
            <FileIcon class="size-4" /> main.ts
          </li>
          <li class="text-text-secondary flex items-center gap-2 px-1 py-1 pl-5">
            <FileIcon class="size-4" /> styles.css
          </li>
          <li class="text-text-secondary flex items-center gap-2 px-1 py-1">
            <FileIcon class="size-4" /> README.md
          </li>
        </ul>
      </nav>
    {/snippet}
    {#snippet end()}
      <main class="h-full overflow-auto p-4 font-mono text-sm">
        <p class="text-text-tertiary">// main.ts</p>
        <p class="text-text-secondary">import App from './App.svelte';</p>
        <p class="text-text-secondary">import './styles.css';</p>
        <p class="text-text-secondary mt-2">export default new App(options);</p>
      </main>
    {/snippet}
  </SplitPane>
</div>

Vertical split: output over log

Set orientation='vertical' to stack the panes with a horizontal divider. Ideal for a preview or output region above a scrolling console. The arrow-key axis follows the layout: Up/Down resize a vertical split.

Preview

Rendered output goes here. Resize the divider below to give the log more room when you need to read a long stack trace.

✓ build succeeded in 412ms

→ 24 modules transformed

→ dist/index.js 12.4 kB

! 1 unused export in utils.ts

→ watching for changes…

<div class="border-border-subtle h-96 w-full overflow-hidden rounded-xl border">
  <SplitPane orientation="vertical" defaultRatio={0.55} min="20%" max="85%">
    {#snippet start()}
      <section aria-label="Preview" class="bg-surface-elevated h-full overflow-auto p-4">
        <p class="text-text-primary mb-1 text-sm font-semibold">Preview</p>
        <p class="text-text-secondary text-sm leading-relaxed">
          Rendered output goes here. Resize the divider below to give the log more room when
          you need to read a long stack trace.
        </p>
      </section>
    {/snippet}
    {#snippet end()}
      <section aria-label="Build log" class="h-full overflow-auto p-4 font-mono text-xs">
        <p class="text-success">✓ build succeeded in 412ms</p>
        <p class="text-text-tertiary">→ 24 modules transformed</p>
        <p class="text-text-tertiary">→ dist/index.js 12.4 kB</p>
        <p class="text-text-secondary">! 1 unused export in utils.ts</p>
        <p class="text-text-tertiary">→ watching for changes…</p>
      </section>
    {/snippet}
  </SplitPane>
</div>

Collapsible sidebar with onCollapsedChange

With collapsible, dragging the first pane below collapseThreshold (or pressing Enter on the focused divider) snaps it fully shut, and onCollapsedChange fires on the transition. Track that flag to swap a 'Show panel' affordance in and out.
sidebar visible Drag the divider left, or focus it and press Enter.

Main content. Collapse the sidebar to reclaim its width. Re-expand it by pressing Enter on the divider, and the previous ratio is restored.

<div class="w-full space-y-3">
  <div class="flex items-center gap-3">
    <Badge intent={sidebarCollapsed ? 'neutral' : 'success'} variant="soft" size="sm">
      {sidebarCollapsed ? 'sidebar collapsed' : 'sidebar visible'}
    </Badge>
    <span class="text-text-tertiary text-xs"
      >Drag the divider left, or focus it and press Enter.</span
    >
  </div>
  <div class="border-border-subtle h-72 w-full overflow-hidden rounded-xl border">
    <SplitPane
      collapsible
      defaultRatio={0.3}
      min="15%"
      max="60%"
      onCollapsedChange={(c) => (sidebarCollapsed = c)}
    >
      {#snippet start()}
        <nav aria-label="Demo sidebar" class="bg-surface-elevated h-full overflow-auto p-3">
          <p class="text-text-primary mb-2 text-sm font-semibold">Filters</p>
          <ul class="text-text-secondary space-y-1 text-sm">
            <li>Status</li>
            <li>Assignee</li>
            <li>Priority</li>
            <li>Label</li>
          </ul>
        </nav>
      {/snippet}
      {#snippet end()}
        <main class="h-full overflow-auto p-4">
          <p class="text-text-secondary text-sm leading-relaxed">
            Main content. Collapse the sidebar to reclaim its width. Re-expand it by pressing
            Enter on the divider, and the previous ratio is restored.
          </p>
        </main>
      {/snippet}
    </SplitPane>
  </div>
</div>

Chat beside a live artifact

An AI pattern: the conversation on the left, the generated artifact on the right. Compose SplitPane with the Chat surfaces. See the Chat component for the message list and PromptInput composer that fill the left pane.
Build me a pricing table.
Done. The component is rendering in the panel on the right.
Ask a follow-up…

Artifact preview

Starter

per month

Pro

per month

Team

per month

<div class="border-border-subtle h-96 w-full overflow-hidden rounded-xl border">
  <SplitPane defaultRatio={0.42} min="25%" max="65%">
    {#snippet start()}
      <div class="flex h-full flex-col">
        <div class="flex-1 space-y-3 overflow-auto p-4">
          <div class="bg-surface-elevated ml-auto max-w-[80%] rounded-2xl px-3 py-2 text-sm">
            Build me a pricing table.
          </div>
          <div class="text-text-secondary flex max-w-[85%] items-start gap-2 text-sm">
            <SparklesIcon class="text-primary mt-0.5 size-4 shrink-0" />
            <span>Done. The component is rendering in the panel on the right.</span>
          </div>
        </div>
        <div class="border-border-subtle border-t p-3">
          <div
            class="border-border-default text-text-tertiary rounded-xl border px-3 py-2 text-sm"
          >
            Ask a follow-up…
          </div>
        </div>
      </div>
    {/snippet}
    {#snippet end()}
      <section aria-label="Preview" class="bg-surface-elevated h-full overflow-auto p-6">
        <p class="text-text-primary mb-4 text-sm font-semibold">Artifact preview</p>
        <div class="border-border-subtle grid grid-cols-3 gap-3">
          {#each ['Starter', 'Pro', 'Team'] as tier (tier)}
            <div class="border-border-subtle rounded-xl border p-3 text-center">
              <p class="text-text-primary text-sm font-semibold">{tier}</p>
              <p class="text-text-tertiary text-xs">per month</p>
            </div>
          {/each}
        </div>
      </section>
    {/snippet}
  </SplitPane>
</div>

For a permanent navigation rail rather than two rebalanceable content regions, reach for SidebarLayout or Sidebar instead. SplitPane is for when both panes are primary content.

02 Accessibility

The "window splitter" pattern

The divider follows the WAI-ARIA window splitter pattern. It renders as role="separator" made focusable (tabindex="0"), carries aria-controls pointing at the first pane, and exposes its position as aria-valuenow / aria-valuemin / aria-valuemax (percentages, clamped into the configured min/max). Its aria-orientation reflects the divider bar itself, across the drag axis: vertical for a horizontal layout, horizontal for a vertical one. Give it a meaningful name via handleLabel (default "Resize panes"). When disabled, the divider drops out of the tab order and reports aria-disabled.

Keyboard

Tab moves focus to the divider. The arrow keys follow the layout axis:

  • / (horizontal) or / (vertical) resize by ±2%.
  • Hold Shift with an arrow for a ±10% step.
  • Home / End jump to the min / max limit.
  • Enter toggles collapse when collapsible is set. Otherwise it resets to defaultRatio, the keyboard equivalent of the double-click reset.

Pointer & touch target

Drag uses setPointerCapture, so a resize keeps tracking even if the pointer leaves the divider. The divider is a full-length hit area along the split axis rather than a hairline, keeping it reachable for touch and coarse pointers. The data-dragging and data-collapsed attributes on the root expose the live state for CSS-only styling.

03 API Reference

21 props
21 props 2 required
Prop
Type
Default
Description

04 Types

Local type definitions used by this component.

10 types
Name
Kind
Category
Used by
Description

05 Installation

Import

import { SplitPane } from '@urbicon-ui/blocks';
import type { SplitPaneProps, SplitPaneLimit, SplitPaneOrientation } from '@urbicon-ui/blocks';