Split view
ProA list and the detail of what is open in it: side by side when there is room, one at a time when there isn't (a phone, a narrow panel). It responds to its own width, not the window's, so it behaves the same full-screen or embedded.
Narrower than at, the detail takes the whole width while something is open, with a Back button that says where it goes ("Back to Inbox"); Back calls onBack, and the app clears what is open, so detail becomes null. The list stays mounted while it is hidden, so its state (a search, a filter, an open group) is kept; its scroll position is put back when it shows again, and focus goes back to the element in the list that had it when the detail opened (the list itself if that element is gone). Focus never falls to the page when the detail closes, at any width, or when the view narrows with the detail open: focus in the detail stays there, and focus in the list, which hides, moves to the detail.
The list pane scrolls (it is the scroller), so let your list grow to its content inside it.
Pass detailKey, the id of what is open, so the detail arrives when the selection changes; without it, it arrives when something opens after nothing was.
Version 5.2.0· other versions
This page is for 5.2.0, the current release. An earlier release’s page is the Markdown that release published, so the docs match the version you have installed (candor-lock.json records it).
Not in the docs of 1.3.0, 1.4.0, 1.5.0, 1.5.1, 1.6.0, 1.7.0, 1.8.0, 1.8.1, 1.9.0, 1.9.1, 2.0.0, 2.1.0, 2.2.0, 2.3.0, 2.4.0, 2.5.0, 2.5.1, 2.6.0, 2.6.1, 2.6.2, 2.7.0, 2.8.0, 2.9.0, 2.10.0, 3.0.0, 4.0.0, 4.0.1, 4.0.2, 4.1.0 and 4.2.0: the page came later, or had another name then.
Docs pages weren’t kept for 1.0.0, 1.0.1, 1.1.0, 1.1.1, 1.2.0 and 1.2.1: they are kept from 1.3.0 on, and before that only llms.txt was published.
Preview
Loading the preview
Install
First time? Get started sets up the registry and theme. Or with shadcn directly: npx shadcn@latest add @candor/split-view
Brings in: button, locale, motion, tokens, utils · npm: lucide-react@^1.67.0
Usage
"use client"
import * as React from "react"
import { SplitView } from "@/components/candor/split-view"
const messages = [
{ id: "m1", from: "Billing agent", subject: "Invoice 2041 is overdue", body: "The invoice for Acme is 12 days overdue. I drafted a reminder; it is waiting for you in Approvals." },
{ id: "m2", from: "Deploy agent", subject: "Storefront deployed to Staging", body: "Build 418 is on Staging. The smoke tests passed in 2 minutes 10 seconds." },
{ id: "m3", from: "Access agent", subject: "Sam asked for admin on Billing", body: "Sam asked for admin on Billing until Friday. Nothing changes until you approve." },
]
export default function Example() {
const [open, setOpen] = React.useState<string | null>(null)
const [narrow, setNarrow] = React.useState(false)
const message = messages.find((m) => m.id === open)
return (
<div className="flex flex-col gap-3">
<label className="flex items-center gap-1.5 text-xs text-muted-foreground">
<input type="checkbox" checked={narrow} onChange={(e) => setNarrow(e.target.checked)} />
Phone width
</label>
<div className={narrow ? "h-80 w-80 overflow-hidden rounded-lg border" : "h-80 overflow-hidden rounded-lg border"}>
<SplitView
// Below `at` (720 by default) one pane shows at a time; this preview is narrower, so it's set lower.
at={480}
listWidth={240}
listLabel="Inbox"
detailLabel="Message"
// Back clears what's open; the list comes back where the person left it.
onBack={() => setOpen(null)}
// The id of what is open: the detail arrives when it changes.
detailKey={open}
list={
<ul className="flex flex-col p-1">
{messages.map((m) => (
<li key={m.id}>
<button
type="button"
aria-current={m.id === open ? "true" : undefined}
onClick={() => setOpen(m.id)}
className="flex w-full flex-col items-start rounded-md px-2 py-1.5 text-start text-sm hover:bg-accent aria-[current]:bg-accent"
>
<span className="font-medium">{m.subject}</span>
<span className="text-xs text-muted-foreground">{m.from}</span>
</button>
</li>
))}
</ul>
}
detail={
message ? (
<article className="flex flex-col gap-2 p-4 text-sm">
<h3 className="font-semibold">{message.subject}</h3>
<p className="text-xs text-muted-foreground">From {message.from}</p>
<p>{message.body}</p>
</article>
) : null
}
/>
</div>
</div>
)
}<SplitView>
| Prop | Type | Default | Description |
|---|---|---|---|
| listrequired | React.ReactNode | — | The list: a SessionList, an Inbox, rows of your own. It stays mounted when it is hidden behind the detail. |
| detailrequired | React.ReactNode | null | — | What is open, or null when nothing is. |
| detailKey | string | null | undefined | — | The id of what is open ("thread-42"): when it changes after the first draw, the detail arrives. |
| listLabelrequired | string | — | The list pane's accessible name ("Inbox"); Back says it: "Back to Inbox". |
| detailLabelrequired | string | — | The detail pane's accessible name ("Message"). |
| onBackrequired | () => void | — | Called by Back when only one pane fits. Clear what is open, so `detail` becomes null and the list shows again. |
| at | number | undefined | 720 | The frame width in pixels below which one pane shows at a time. Defaults to 720. |
| listWidth | number | undefined | 320 | The list pane's width in pixels when both show. Defaults to 320. |
| defaultWidth | number | undefined | 0 | The frame width to lay out for until it can be measured: on the server, and in test environments like jsdom that measure everything as 0. Defaults to 0, one pane at a time. |
| className | string | undefined | — | Classes for the frame, merged with Candor's. |
Strings
The fixed words, in English by default. Change any of them under splitView in <LocaleProvider strings={{ … }}>; the table is exported as splitViewStrings.
| Key | English |
|---|---|
| back | Back to {list} |
Motion
| Moment | Family | What it does |
|---|---|---|
| detail | arrive, rise | The detail rises in when the selection changes after the first draw; never at the first draw. |
Set with --cd-motion-split-view-<moment>-* in your brand file; the families are in the design reference.