Skip to content
Tier

Live rows

Pro

The rows of a list whose data changes under the person: Inbox's items and ApprovalQueue's requests. A row added after the list first drew rises in; one that leaves the data stays, inert and hidden from screen readers, for its out moment in the place it had; a real re-sort slides each row to its new place. With selection, each live row has a checkbox in a 24px target before it. The item draws each row's content (row) and its empty state (empty), and passes its own motion moments, so each item's motion is named for it.

Key it by the view it shows, so changing the view starts it over: its rows are there, not arriving or leaving. An internal part of Inbox and ApprovalQueue, installed with them.

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, 4.2.0, 4.3.0, 4.4.0, 4.5.0 and 5.0.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/live-rows

Brings in: motion, tokens, utils

Usage

TSX · The example above
"use client"

import * as React from "react"
import { LiveRows } from "@/components/candor/live-rows"
import { motion } from "@/lib/candor/motion"

/** The example's own moments, as Inbox and ApprovalQueue name theirs. */
const rowMotion = {
  arrive: { style: motion("live-rows-example", "arrive", "arrive"), className: "cd-arrive" },
  leave: { style: motion("live-rows-example", "leave", "reveal"), className: "cd-reveal-out calm:hidden" },
  reorder: { style: motion("live-rows-example", "reorder", "move"), className: "data-[reorder]:cd-move" },
}

type Task = { id: string; title: string }

export default function Example() {
  const [tasks, setTasks] = React.useState<Task[]>([
    { id: "a", title: "Review the migration" },
    { id: "b", title: "Answer the region question" },
  ])
  const [sel, setSel] = React.useState<Set<string>>(() => new Set())
  const next = React.useRef(3)
  return (
    <div className="flex max-w-md flex-col gap-3">
      <div className="flex flex-wrap gap-2">
        <button
          type="button"
          className="rounded-md border px-2 py-1 text-sm"
          onClick={() => {
            const n = next.current++
            setTasks((list) => [{ id: `t${n}`, title: `New task ${n}` }, ...list])
          }}
        >
          Add one
        </button>
        <button type="button" className="rounded-md border px-2 py-1 text-sm" onClick={() => setTasks((list) => list.slice(1))}>
          Clear the first
        </button>
        <button type="button" className="rounded-md border px-2 py-1 text-sm" onClick={() => setTasks((list) => [...list].reverse())}>
          Re-sort
        </button>
      </div>
      <div className="rounded-xl border bg-card">
        <LiveRows
          items={tasks}
          keyOf={(task) => task.id}
          motion={rowMotion}
          className="relative divide-y"
          rowClassName="flex items-center"
          selection={{
            sel,
            toggle: (id) =>
              setSel((s) => {
                const out = new Set(s)
                if (out.has(id)) out.delete(id)
                else out.add(id)
                return out
              }),
            label: (task) => `Select ${task.title}`,
            className: "flex shrink-0 items-center ps-3",
          }}
          empty={<p className="px-4 py-8 text-center text-sm text-muted-foreground">Nothing left.</p>}
          row={(task) => (
            <button type="button" className="flex-1 px-3 py-2.5 text-start text-sm outline-none hover:bg-accent focus-visible:bg-accent">
              {task.title}
            </button>
          )}
        />
      </div>
    </div>
  )
}

<LiveRows>

PropTypeDefaultDescription
itemsrequiredT[]—The rows to draw, in order.
keyOfrequired(item: T) => string—A row's id: its key, `data-row-id`, and what `selection` holds.
motionrequiredLiveRowsMotion—The item's own motion moments.
rowrequired(item: T) => React.ReactNode—A row's content, after its checkbox: usually the button that opens it.
emptyrequiredReact.ReactNode—Drawn in place of the list while there are no rows, leaving ones included.
selectionLiveRowsSelection<T> | undefined—Adds a checkbox before each live row.
classNamestring | undefined—Classes for the list.
rowClassNamestring | undefined—Classes for the box inside each row that holds its checkbox and content.

Types

LiveRowsMoment
One of the item's motion moments: its chain, made with `motion(item, moment, family)` in the item's file, and the family utility that plays it, named there too, so each item's motion stays its own.
{ /** The moment's chain, for the row's style. */ style: React.CSSProperties; /** The family utility (and any classes it needs), for the row's classes. */ className: string; }
LiveRowsMotion
The item's own motion moments.
{ /** A row added after the list first drew rises in. */ arrive: LiveRowsMoment; /** A row that left the data collapses out. */ leave: LiveRowsMoment; /** A row slides to its new place when the data re-sorts the list; the part sets and applies the row's offset. */ reorder: LiveRowsMoment; }
LiveRowsSelection
Selection on the rows: a checkbox before each one that isn't leaving.
{ /** The ids selected. */ sel: Set<string>; /** Ticks or clears a row by its id. */ toggle: (id: string) => void; /** The checkbox's name for a row, in the item's words ("Select {title}"). */ label: (item: T) => string; /** Classes for the box's 24px target's holder, which places it beside the row. */ className: string; }