# Splitter

Use `ui-splitter` for editor shells, sidebars, inspector layouts, dashboard regions, and other work surfaces where users benefit from allocating more space to one region without leaving the current page.

## Import
```ts
import { SplitterComponent, SplitterPanelDirective, type SplitterPanel, type SplitterResizeEvent } from 'ui';
```

## Basic workspace split
```ts
import { Component, signal } from '@angular/core';
import {
  SplitterComponent,
  SplitterPanelDirective,
  type SplitterPanel,
  type SplitterResizeEvent,
} from 'ui';

@Component({
  selector: 'app-splitter-basic-demo',
  standalone: true,
  imports: [SplitterComponent, SplitterPanelDirective],
  template: `
    <div style="display:flex;flex-direction:column;gap:1rem;width:100%;max-width:56rem">
      <div
        style="display:flex;flex-wrap:wrap;gap:1rem;align-items:center;padding:0.75rem 0.875rem;border:1px dashed var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background2-rest)"
      >
        <span style="font-size:0.8125rem;color:var(--color-neutral-foreground2-rest)"
          >Sidebar: <strong>{{ sizes()[0].toFixed(0) }}%</strong></span
        >
        <span style="font-size:0.8125rem;color:var(--color-neutral-foreground2-rest)"
          >Canvas: <strong>{{ sizes()[1].toFixed(0) }}%</strong></span
        >
        <span style="font-size:0.8125rem;color:var(--color-neutral-foreground2-rest)"
          >Inspector: <strong>{{ sizes()[2].toFixed(0) }}%</strong></span
        >
      </div>

      <div
        style="height:22rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:1rem;overflow:hidden;background:var(--color-neutral-background-rest)"
      >
        <ui-splitter
          [panels]="panels()"
          orientation="horizontal"
          [gutterSize]="8"
          (panelResize)="onResize($event)"
        >
          <ng-template uiSplitterPanel="sidebar">
            <div
              style="height:100%;padding:1rem;background:var(--color-neutral-background-rest);border-right:1px solid var(--color-neutral-stroke-rest)"
            >
              <div style="font-size:0.875rem;font-weight:600">Navigation</div>
              <div
                style="margin-top:0.5rem;font-size:0.8125rem;color:var(--color-neutral-foreground2-rest)"
              >
                Keep project or file navigation resizable, but not too narrow.
              </div>
            </div>
          </ng-template>

          <ng-template uiSplitterPanel="canvas">
            <div
              style="height:100%;padding:1rem;background:linear-gradient(180deg,var(--color-neutral-background-rest),var(--color-neutral-background2-rest))"
            >
              <div style="font-size:0.875rem;font-weight:600">Main workspace</div>
              <div
                style="margin-top:0.5rem;font-size:0.8125rem;color:var(--color-neutral-foreground2-rest)"
              >
                This center region usually gets the most flexible space.
              </div>
            </div>
          </ng-template>

          <ng-template uiSplitterPanel="inspector">
            <div
              style="height:100%;padding:1rem;background:var(--color-neutral-background-rest);border-left:1px solid var(--color-neutral-stroke-rest)"
            >
              <div style="font-size:0.875rem;font-weight:600">Inspector</div>
              <div
                style="margin-top:0.5rem;font-size:0.8125rem;color:var(--color-neutral-foreground2-rest)"
              >
                A secondary panel can stay resizable without taking over the layout.
              </div>
            </div>
          </ng-template>
        </ui-splitter>
      </div>
    </div>
  `,
})
export class SplitterBasicDemoComponent {
  protected readonly panels = signal<SplitterPanel[]>([
    { id: 'sidebar', size: 22, minSize: 180, maxSize: 320 },
    { id: 'canvas', size: 53, minSize: 320 },
    { id: 'inspector', size: 25, minSize: 220, maxSize: 420 },
  ]);

  protected readonly sizes = signal([22, 53, 25]);

  protected onResize(event: SplitterResizeEvent): void {
    const next = [...this.sizes()];
    next[event.panelIndex] = event.newSize;
    this.sizes.set(next);
  }
}
```

## Min and max constraints
```ts
import { Component, signal } from '@angular/core';
import {
  ButtonComponent,
  SplitterComponent,
  SplitterPanelDirective,
  type SplitterPanel,
  type SplitterResizeEvent,
} from 'ui';

@Component({
  selector: 'app-splitter-constraints-demo',
  standalone: true,
  imports: [ButtonComponent, SplitterComponent, SplitterPanelDirective],
  template: `
    <div style="display:flex;flex-direction:column;gap:1rem;width:100%;max-width:52rem">
      <div
        style="display:flex;flex-wrap:wrap;gap:0.75rem;align-items:center;padding:0.75rem 0.875rem;border:1px dashed var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background2-rest)"
      >
        <ui-button variant="secondary" appearance="outline" (click)="reset()">Reset</ui-button>
        <span style="font-size:0.8125rem;color:var(--color-neutral-foreground2-rest)"
          >Left: <strong>{{ sizes()[0].toFixed(0) }}%</strong></span
        >
        <span style="font-size:0.8125rem;color:var(--color-neutral-foreground2-rest)"
          >Right: <strong>{{ sizes()[1].toFixed(0) }}%</strong></span
        >
      </div>

      <div
        style="height:18rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:1rem;overflow:hidden;background:var(--color-neutral-background-rest)"
      >
        <ui-splitter
          [panels]="panels()"
          orientation="horizontal"
          [gutterSize]="8"
          (panelResize)="onResize($event)"
        >
          <ng-template uiSplitterPanel="filters">
            <div style="height:100%;padding:1rem;background:var(--color-neutral-background-rest)">
              <div style="font-size:0.875rem;font-weight:600">Filters</div>
              <div
                style="margin-top:0.5rem;font-size:0.8125rem;color:var(--color-neutral-foreground2-rest)"
              >
                This panel has a minimum size so checkboxes and fields stay usable.
              </div>
            </div>
          </ng-template>

          <ng-template uiSplitterPanel="results">
            <div
              style="height:100%;padding:1rem;background:linear-gradient(180deg,var(--color-neutral-background-rest),var(--color-neutral-background2-rest))"
            >
              <div style="font-size:0.875rem;font-weight:600">Results</div>
              <div
                style="margin-top:0.5rem;font-size:0.8125rem;color:var(--color-neutral-foreground2-rest)"
              >
                The main area still keeps its own minimum size, so the content view does not
                collapse into noise.
              </div>
            </div>
          </ng-template>
        </ui-splitter>
      </div>
    </div>
  `,
})
export class SplitterConstraintsDemoComponent {
  protected readonly initialPanels: SplitterPanel[] = [
    { id: 'filters', size: 32, minSize: 220, maxSize: 360 },
    { id: 'results', size: 68, minSize: 360 },
  ];

  protected readonly panels = signal<SplitterPanel[]>(this.initialPanels);
  protected readonly sizes = signal([32, 68]);

  protected onResize(event: SplitterResizeEvent): void {
    const next = [...this.sizes()];
    next[event.panelIndex] = event.newSize;
    this.sizes.set(next);
  }

  protected reset(): void {
    this.panels.set([...this.initialPanels]);
    this.sizes.set([32, 68]);
  }
}
```

## Vertical regions
```ts
import { Component, signal } from '@angular/core';
import {
  SplitterComponent,
  SplitterPanelDirective,
  type SplitterPanel,
  type SplitterResizeEvent,
} from 'ui';

@Component({
  selector: 'app-splitter-vertical-demo',
  standalone: true,
  imports: [SplitterComponent, SplitterPanelDirective],
  template: `
    <div style="display:flex;flex-direction:column;gap:1rem;width:100%;max-width:42rem">
      <div
        style="display:flex;flex-wrap:wrap;gap:1rem;align-items:center;padding:0.75rem 0.875rem;border:1px dashed var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background2-rest)"
      >
        <span style="font-size:0.8125rem;color:var(--color-neutral-foreground2-rest)"
          >Summary: <strong>{{ sizes()[0].toFixed(0) }}%</strong></span
        >
        <span style="font-size:0.8125rem;color:var(--color-neutral-foreground2-rest)"
          >Feed: <strong>{{ sizes()[1].toFixed(0) }}%</strong></span
        >
      </div>

      <div
        style="height:24rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:1rem;overflow:hidden;background:var(--color-neutral-background-rest)"
      >
        <ui-splitter
          [panels]="panels()"
          orientation="vertical"
          [gutterSize]="8"
          (panelResize)="onResize($event)"
        >
          <ng-template uiSplitterPanel="summary">
            <div style="height:100%;padding:1rem;background:var(--color-neutral-background-rest)">
              <div style="font-size:0.875rem;font-weight:600">Summary</div>
              <div
                style="margin-top:0.5rem;font-size:0.8125rem;color:var(--color-neutral-foreground2-rest)"
              >
                Vertical splitters fit dashboards with a compact header region above a denser
                content area.
              </div>
            </div>
          </ng-template>

          <ng-template uiSplitterPanel="feed">
            <div
              style="height:100%;padding:1rem;background:linear-gradient(180deg,var(--color-neutral-background-rest),var(--color-neutral-background2-rest))"
            >
              <div style="font-size:0.875rem;font-weight:600">Activity feed</div>
              <div
                style="margin-top:0.5rem;font-size:0.8125rem;color:var(--color-neutral-foreground2-rest)"
              >
                The lower region can grow when users need more scrolling room.
              </div>
            </div>
          </ng-template>
        </ui-splitter>
      </div>
    </div>
  `,
})
export class SplitterVerticalDemoComponent {
  protected readonly panels = signal<SplitterPanel[]>([
    { id: 'summary', size: 34, minSize: 120, maxSize: 220 },
    { id: 'feed', size: 66, minSize: 220 },
  ]);

  protected readonly sizes = signal([34, 66]);

  protected onResize(event: SplitterResizeEvent): void {
    const next = [...this.sizes()];
    next[event.panelIndex] = event.newSize;
    this.sizes.set(next);
  }
}
```

## Fixed utility panel
```ts
import { Component } from '@angular/core';
import { SplitterComponent, SplitterPanelDirective, type SplitterPanel } from 'ui';

@Component({
  selector: 'app-splitter-fixed-panel-demo',
  standalone: true,
  imports: [SplitterComponent, SplitterPanelDirective],
  template: `
    <div style="display:flex;flex-direction:column;gap:1rem;width:100%;max-width:52rem">
      <div style="font-size:0.875rem;line-height:1.5;color:var(--color-neutral-foreground2-rest)">
        Sometimes one panel should stay fixed while the neighboring region absorbs resizing. This is
        useful for a locked utility rail or a compact metrics strip.
      </div>

      <div
        style="height:18rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:1rem;overflow:hidden;background:var(--color-neutral-background-rest)"
      >
        <ui-splitter [panels]="panels" orientation="horizontal" [gutterSize]="8">
          <ng-template uiSplitterPanel="rail">
            <div
              style="height:100%;padding:1rem;background:var(--color-neutral-background-rest);border-right:1px solid var(--color-neutral-stroke-rest)"
            >
              <div style="font-size:0.875rem;font-weight:600">Utility rail</div>
              <div
                style="margin-top:0.5rem;font-size:0.8125rem;color:var(--color-neutral-foreground2-rest)"
              >
                This panel is marked non-resizable.
              </div>
            </div>
          </ng-template>

          <ng-template uiSplitterPanel="content">
            <div
              style="height:100%;padding:1rem;background:linear-gradient(180deg,var(--color-neutral-background-rest),var(--color-neutral-background2-rest))"
            >
              <div style="font-size:0.875rem;font-weight:600">Flexible content</div>
              <div
                style="margin-top:0.5rem;font-size:0.8125rem;color:var(--color-neutral-foreground2-rest)"
              >
                Use this pattern when only one side of the layout is meant to stretch and compress.
              </div>
            </div>
          </ng-template>
        </ui-splitter>
      </div>
    </div>
  `,
})
export class SplitterFixedPanelDemoComponent {
  protected readonly panels: SplitterPanel[] = [
    { id: 'rail', size: 18, minSize: 160, maxSize: 200, resizable: false },
    { id: 'content', size: 82, minSize: 420 },
  ];
}
```

## Nested workspace shell
```ts
import { Component } from '@angular/core';
import { SplitterComponent, SplitterPanelDirective, type SplitterPanel } from 'ui';

@Component({
  selector: 'app-splitter-nested-workspace-demo',
  standalone: true,
  imports: [SplitterComponent, SplitterPanelDirective],
  template: `
    <div style="display:flex;flex-direction:column;gap:1rem;width:100%;max-width:62rem">
      <div style="font-size:0.875rem;line-height:1.5;color:var(--color-neutral-foreground2-rest)">
        Nested splitters are useful for editor-like shells with a sidebar, top context region, main
        canvas, and a secondary status or log area.
      </div>

      <div
        style="height:28rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:1rem;overflow:hidden;background:var(--color-neutral-background-rest)"
      >
        <ui-splitter [panels]="outerPanels" orientation="horizontal" [gutterSize]="8">
          <ng-template uiSplitterPanel="sidebar">
            <div
              style="height:100%;padding:1rem;background:var(--color-neutral-background-rest);border-right:1px solid var(--color-neutral-stroke-rest)"
            >
              <div style="font-size:0.875rem;font-weight:600">Project sidebar</div>
              <div
                style="margin-top:0.5rem;font-size:0.8125rem;color:var(--color-neutral-foreground2-rest)"
              >
                Folders, views, or explorer items usually live here.
              </div>
            </div>
          </ng-template>

          <ng-template uiSplitterPanel="main">
            <ui-splitter [panels]="innerPanels" orientation="vertical" [gutterSize]="8">
              <ng-template uiSplitterPanel="header">
                <div
                  style="height:100%;padding:1rem;background:var(--color-neutral-background-rest);border-bottom:1px solid var(--color-neutral-stroke-rest)"
                >
                  <div style="font-size:0.875rem;font-weight:600">Editor header</div>
                  <div
                    style="margin-top:0.5rem;font-size:0.8125rem;color:var(--color-neutral-foreground2-rest)"
                  >
                    Toolbar, breadcrumbs, or active file info.
                  </div>
                </div>
              </ng-template>

              <ng-template uiSplitterPanel="canvas">
                <div
                  style="height:100%;padding:1rem;background:linear-gradient(180deg,var(--color-neutral-background-rest),var(--color-neutral-background2-rest))"
                >
                  <div style="font-size:0.875rem;font-weight:600">Canvas</div>
                  <div
                    style="margin-top:0.5rem;font-size:0.8125rem;color:var(--color-neutral-foreground2-rest)"
                  >
                    This is the main content region that usually needs the most space.
                  </div>
                </div>
              </ng-template>

              <ng-template uiSplitterPanel="footer">
                <div
                  style="height:100%;padding:1rem;background:var(--color-neutral-background-rest);border-top:1px solid var(--color-neutral-stroke-rest)"
                >
                  <div style="font-size:0.875rem;font-weight:600">Output panel</div>
                  <div
                    style="margin-top:0.5rem;font-size:0.8125rem;color:var(--color-neutral-foreground2-rest)"
                  >
                    Logs, errors, or test output fit naturally in a smaller bottom region.
                  </div>
                </div>
              </ng-template>
            </ui-splitter>
          </ng-template>
        </ui-splitter>
      </div>
    </div>
  `,
})
export class SplitterNestedWorkspaceDemoComponent {
  protected readonly outerPanels: SplitterPanel[] = [
    { id: 'sidebar', size: 22, minSize: 180, maxSize: 320 },
    { id: 'main', size: 78, minSize: 540 },
  ];

  protected readonly innerPanels: SplitterPanel[] = [
    { id: 'header', size: 18, minSize: 90, maxSize: 160 },
    { id: 'canvas', size: 60, minSize: 240 },
    { id: 'footer', size: 22, minSize: 100, maxSize: 180 },
  ];
}
```

## Keyboard resizing
```ts
import { Component, signal } from '@angular/core';
import {
  ButtonComponent,
  SplitterComponent,
  SplitterPanelDirective,
  type SplitterPanel,
  type SplitterResizeEvent,
} from 'ui';

@Component({
  selector: 'app-splitter-keyboard-demo',
  standalone: true,
  imports: [ButtonComponent, SplitterComponent, SplitterPanelDirective],
  template: `
    <div style="display:flex;flex-direction:column;gap:1rem;width:100%;max-width:50rem">
      <div
        style="display:flex;flex-wrap:wrap;gap:0.75rem;align-items:center;padding:0.75rem 0.875rem;border:1px dashed var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background2-rest)"
      >
        <ui-button variant="secondary" appearance="outline" (click)="reset()">Reset</ui-button>
        <span style="font-size:0.8125rem;color:var(--color-neutral-foreground2-rest)"
          >Last resize: <strong>{{ lastResize() || 'none' }}</strong></span
        >
      </div>

      <div style="font-size:0.875rem;line-height:1.5;color:var(--color-neutral-foreground2-rest)">
        Tab to the gutter, then use arrow keys for small adjustments or <code>Home</code> /
        <code>End</code> to move toward the panel limits.
      </div>

      <div
        style="height:16rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:1rem;overflow:hidden;background:var(--color-neutral-background-rest)"
      >
        <ui-splitter
          [panels]="panels"
          orientation="horizontal"
          [gutterSize]="8"
          (panelResize)="onResize($event)"
        >
          <ng-template uiSplitterPanel="details">
            <div style="height:100%;padding:1rem;background:var(--color-neutral-background-rest)">
              <div style="font-size:0.875rem;font-weight:600">Details</div>
            </div>
          </ng-template>

          <ng-template uiSplitterPanel="preview">
            <div
              style="height:100%;padding:1rem;background:linear-gradient(180deg,var(--color-neutral-background-rest),var(--color-neutral-background2-rest))"
            >
              <div style="font-size:0.875rem;font-weight:600">Preview</div>
            </div>
          </ng-template>
        </ui-splitter>
      </div>
    </div>
  `,
})
export class SplitterKeyboardDemoComponent {
  protected readonly panels: SplitterPanel[] = [
    { id: 'details', size: 35, minSize: 220, maxSize: 420 },
    { id: 'preview', size: 65, minSize: 320 },
  ];

  protected readonly lastResize = signal('');

  protected onResize(event: SplitterResizeEvent): void {
    this.lastResize.set(`${event.panelId}: ${event.newSize.toFixed(0)}%`);
  }

  protected reset(): void {
    this.lastResize.set('');
  }
}
```

## Accessibility

### Group and separator semantics
`ui-splitter` renders the whole layout as a grouped region and exposes each active gutter as a keyboard-focusable `separator`. The gutter reflects `aria-orientation`, `aria-valuenow`, `aria-valuemin`, and `aria-valuemax`.

### Keyboard behavior
The gutter supports keyboard resizing.

| Key | Action |
| --- | --- |
| Tab / Shift+Tab | Moves focus to or from a resizable gutter. |
| ArrowLeft / ArrowRight | Resize horizontal panels in small steps. |
| ArrowUp / ArrowDown | Resize vertical panels in small steps. |
| Home | Moves the gutter toward the minimum size of the leading panel. |
| End | Moves the gutter toward the minimum size of the trailing panel. |

### Labels and constraints
Use a meaningful `ariaLabel` when the default splitter label is not specific enough for the surrounding screen. Panel size constraints are not just visual rules; they also prevent keyboard resizing from creating unusable regions.
