// preview in
saved for every page

Splitter

Two panels with a handle between them that people drag, or move with the keyboard, to share the space. Use it in work-heavy screens like a mail client, an editor with a preview or a query tool.

Pattern WAI-ARIA Window SplitterClient componentSource ↗
minimal · light · #3fa34d

This release brings faster exports and a new invoice view.

Track what was billed, what is paid and what is late. Filter by customer or by month.

orientation

Installation

Set up Camaleo once (Installation), then import what you need. It renders on the client.

import { Splitter, SplitterPanel, SplitterHandle } from "@camaleo/react";

Anatomy

The value is the size of the first panel, in percent; the second takes the rest. Give the splitter a height. Panels clip their content, so put a ScrollArea inside the ones that can grow.

Examples

Every example is a real file you can copy. They all wear the skin, mode and brand from the bar above. Drag a handle, or press Tab until one has a focus ring and use the arrow keys.

minimal · light · #3fa34d

Release 4.2

This release brings faster exports and a new invoice view.

  • CSV export is 3× faster
  • Invoices show the payment status
  • Dark mode for reports
  • Paula LimaQ1 budget, final numbers
  • Rafael SouzaContract renewal with Padaria Estrela
  • BillingInvoice INV-2025-118 was paid
  • Julia AlvesDesign review moved to Friday

Q1 budget, final numbers

Paula Lima · 12 Mar 2025, 16:40

Hi team, the final Q1 numbers are in the shared folder. Revenue closed at $482,300, 6% above plan. Please review your area before Thursday so we can send it to the board.

Three columns

Put a second splitter inside the second panel. Each handle gets its own name, so people know which border they are moving.

export function InvoiceTable({ rows, currency }: Props) {
  const total = useMemo(
    () => rows.reduce((sum, r) => sum + r.amount, 0),
    [rows],
  );
  return <Table caption={format(total, currency)} rows={rows} />;
}
$ pnpm test invoice-table
 ✓ renders one row per invoice (12 ms)
 ✓ formats the total in the customer's currency (4 ms)
 ✗ updates the total when the currency changes
   Expected "R$ 1.240,00", received "$1,240.00"
Tests: 1 failed, 2 passed, 3 total

Stacked

orientation="vertical" puts one panel above the other, like an editor over its terminal. ↑ and ↓ move the handle.

Status
  • INV-2025-118 · Padaria Estrela · $1,240.00
  • INV-2025-117 · Café Aurora · $860.50
  • INV-2025-116 · Mercado Sol · $3,105.90
  • INV-2025-115 · Livraria Nova · $412.00

Collapsible panel

Click "Hide filters", or press Enter on the handle. Both collapse the panel to 0 and bring it back at the size it had; a collapsed panel is hidden, so its controls leave the Tab order.

Sidebar: 28%

Invoices

Track what was billed, what is paid and what is late. Filter by customer or by month.

Saved to your profile, so the layout is the same on your next visit.

Remembered size

Drag the handle, then reset it. With value and onValueChange, the size is yours to save in the user's profile.

Revenue vs. target
Jan
x
Feb
x
Mar
x
Apr
x
Details

The chart stays between 30% and 70%.

Limits

min and max keep both sides usable. Home and End jump straight to them.

  • Overview
  • Customers
  • Invoices
  • Reports
Invoices

Track what was billed, what is paid and what is late. Filter by customer or by month.

A name and a value in words

The handle is a focusable separator. Name it after what it resizes, and add aria-valuetext so a screen reader says "Sidebar 30% wide" instead of "30".

In context

These are whole pieces of UI you can copy, in the skin you picked.

  • Ana RibeiroOrder #4821 hasn’t shippedOpen
  • Carlos MendesWrong size deliveredOpen
  • Beatriz RochaHow do I change my address?Waiting
Ana Ribeiro

Hi! My order #4821 still says “preparing”. Is it on its way? I need it by Friday.

Support inboxSplitter · ScrollArea · Badge · Avatar · Textarea · Button
3 rows · 42 ms
CustomerRevenue
Padaria Estrela$48,210.00
Café Aurora$31,980.50
Mercado Sol$12,404.90
Query editorSplitter · Textarea · Button · Table
  • src
    • invoices
      • invoice-table.tsx
      • page.tsx
  • package.json
import { Table } from "@camaleo/react";

export function InvoiceTable({ rows }: Props) {
  return <Table rows={rows} />;
}
$ pnpm dev
 ▲ Ready on http://localhost:3000
 ✓ Compiled /invoices in 812 ms
Code editorSplitter · TreeView · ScrollArea

Guidelines

  • Overview
  • Customers
  • Invoices

Track what was billed, what is paid and what is late. Filter by customer or by month.

DoSensible minimums.

Set min so the narrow side always shows whole words.

  • Overview
  • Customers
  • Invoices

Track what was billed, what is paid and what is late. Filter by customer or by month.

Don’tPanels that shrink to nothing.

A 5% sidebar cuts every name off. People have to drag it open again before they can read anything.

INV-2025-118 · Padaria Estrela · $1,240.00

DoA button next to the handle.

Show and hide the panel with a labelled button, so people who don't drag can still reach it.

Filters are hidden here, and nothing on screen says so.

INV-2025-118 · Padaria Estrela · $1,240.00

Don’tContent reachable only by dragging.

A panel at 0% with no button looks like it doesn't exist. Essential content needs a visible way in.

Props

PropTypeDefaultDescription
orientation"horizontal" | "vertical""horizontal"horizontal puts the panels side by side; vertical stacks them.
valuenumber—Size of the first panel, in percent of the splitter. Use with onValueChange to control it.
onValueChange(value: number) => void—Called with the new size (percent, already clamped) on every drag move and key press; 0 when Enter collapses the first panel.
minnumber15Smallest size of the first panel, in percent. Home jumps here.
maxnumber85Largest size of the first panel, in percent. End jumps here.
stepnumber1Percent moved by one arrow key press; Shift + arrow moves five steps.

SplitterPanel takes panel ("first" or "second"). SplitterHandle takes aria-label (default "Resize panel"; pass it in the page's language) and aria-valuetext. Other props go to the elements.

Styling hooks

Restyle it without forking: target the attributes, or set the tokens on a parent, a skin or the splitter itself.

Attributes

data-orientationhorizontal · vertical, on every part
data-panelfirst · second, on the panels
data-draggingon the splitter and the handle while it is dragged

Tokens it reads

Color

--cml-border-control--cml-border-subtle--cml-brand--cml-fg--cml-focus-ring--cml-radius-surface--cml-surface

Shape & size

--cml-border-width

This component only

--cml-splitter-grip--cml-splitter-grip-length--cml-splitter-grip-thickness--cml-splitter-handle-size--cml-splitter-radius
/* a thin line instead of a bar, without the grip */
.editor [data-slot="splitter"] {
  --cml-splitter-handle-size: 0.375rem;
  --cml-splitter-handle-bg: transparent;
}
.editor [data-slot="splitter-grip"] { display: none; }

Accessibility

TabMoves focus to the handle.
← / →Side by side: makes the first panel narrower or wider by one step (1% by default).
↑ / ↓Stacked: makes the first panel shorter or taller by one step.
Shift + arrowMoves five steps at once.
Home / EndJumps to the smallest (min) or the largest (max) size.
EnterCollapses the first panel to 0, or brings it back at the size it had. An arrow key on a collapsed panel opens it at min.
Was this page helpful? · Edit this page on GitHub