Skip to content
Tier

Split view

Pro

A 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.

Markdown
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

With the Candor CLI

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

TSX · The example above
"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>

PropTypeDefaultDescription
listrequiredReact.ReactNode—The list: a SessionList, an Inbox, rows of your own. It stays mounted when it is hidden behind the detail.
detailrequiredReact.ReactNode | null—What is open, or null when nothing is.
detailKeystring | null | undefined—The id of what is open ("thread-42"): when it changes after the first draw, the detail arrives.
listLabelrequiredstring—The list pane's accessible name ("Inbox"); Back says it: "Back to Inbox".
detailLabelrequiredstring—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.
atnumber | undefined720The frame width in pixels below which one pane shows at a time. Defaults to 720.
listWidthnumber | undefined320The list pane's width in pixels when both show. Defaults to 320.
defaultWidthnumber | undefined0The 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.
classNamestring | 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.

KeyEnglish
backBack to {list}

Motion

MomentFamilyWhat it does
detailarrive, riseThe 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.