Skip to content
Tier

Reconcile table

Pro

Rows matched between two sources, side by side: the ones that agree, the ones in only one source, and the ones that conflict, with the fields that differ marked and both values shown. Each row that needs it offers the resolve actions your app lists (take one side, edit, skip); nothing is resolved for the person, and a row reads as resolved only once your app records it through resolution. A summary says how many are left, the list filters by status, and a large set pages.

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 and 2.8.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/reconcile-table

Brings in: bidi, button, heading-level, heading-scope, locale, locked, tokens, utils · npm: lucide-react@^1.67.0

Usage

TSX · The example above
"use client"

import * as React from "react"
import { ReconcileTable, type ReconcileAction, type ReconcileRow } from "@/components/candor/reconcile-table"

const fields = [
  { id: "supplier", label: "Supplier" },
  { id: "amount", label: "Amount" },
  { id: "due", label: "Due" },
]

const found: ReconcileRow[] = [
  { id: "inv-2041", label: "INV-2041", left: { supplier: "Northwind", amount: "1,200.00", due: "1 Dec 2026" }, right: { supplier: "Northwind", amount: "1,200.00", due: "1 Dec 2026" } },
  { id: "inv-2042", label: "INV-2042", left: { supplier: "Fabrikam", amount: "860.00", due: "3 Dec 2026" }, right: { supplier: "Fabrikam", amount: "680.00", due: "3 Dec 2026" } },
  { id: "inv-2043", label: "INV-2043", left: { supplier: "Tailspin", amount: "95.50", due: "5 Dec 2026" }, right: { supplier: "Tailspin Toys", amount: "95.50", due: null } },
  { id: "inv-2044", label: "INV-2044", left: { supplier: "Contoso", amount: "2,310.00", due: "8 Dec 2026" } },
  { id: "led-7781", label: "LED-7781", right: { supplier: "Litware", amount: "140.00", due: "30 Nov 2026" } },
]

const actions: ReconcileAction[] = [
  { id: "take-left", label: "Take invoice", for: ["conflict"] },
  { id: "take-right", label: "Take ledger", for: ["conflict"] },
  { id: "post", label: "Post to ledger", for: ["left-only"] },
  { id: "skip", label: "Skip" },
]

export default function Example() {
  const [rows, setRows] = React.useState(found)
  const set = (id: string, patch: Partial<ReconcileRow>) => setRows((all) => all.map((r) => (r.id === id ? ({ ...r, ...patch } as ReconcileRow) : r)))
  return (
    <ReconcileTable
      className="max-w-3xl"
      title="November invoices against the ledger"
      sources={{ left: "Invoice", right: "Ledger" }}
      fields={fields}
      rows={rows}
      actions={actions}
      onResolve={(row, action) => {
        // Your server records the choice; the row reads as resolved once it has. An Edit action would open your own form first.
        set(row.id, { cannotResolve: "Saving your choice…" })
        window.setTimeout(() => set(row.id, { cannotResolve: undefined, resolution: action }), 800)
      }}
    />
  )
}

<ReconcileTable>

PropTypeDefaultDescription
sourcesrequired{ left: string; right: string }—The two sources' names, such as "Invoice" and "Ledger".
fieldsrequiredReconcileField[]—The fields to compare, one column each, in order.
rowsrequiredReconcileRow[]—The rows, in the order to show them.
actionsrequiredReconcileAction[]—The ways to resolve a row, in the order to offer them. None is chosen for the person.
onResolverequired(row: ReconcileRow, action: string) => void—Called with the row and the action's id when the person picks one. Record it on your server, then pass the id back as the row's `resolution`. For an edit, open your own editor first and record what the person saved.
pageSizenumber | undefined25Rows per page. Defaults to 25.
titlestring | undefined—A heading over the table, also its accessible name. Defaults to "Reconciliation" for the name, with no heading shown.
headingLevelHeadingLevel | undefined—The heading's level when `title` is set. Defaults to one below the nearest `HeadingScope`, else 2 (`<h2>`).
classNamestring | undefined—Extra classes for the outer element.

Strings

The fixed words, in English by default. Change any of them under reconcileTable in <LocaleProvider strings={{ … }}>; the table is exported as reconcileTableStrings.

KeyEnglish
labelReconciliation
tableRows from {left} and {right}
openCount{open} to resolve
resolvedCount{resolved} resolved
matchedCount{matched} matched
showShow
allAll
openTo resolve
conflictConflicts
onlyOnly in {source}
matchedMatched
option{label} {count}
recordRecord
statusStatus
resolveResolve
differ{ one: "{count} field differs", other: "{count} fields differ" }
notInNot in {source}
notSetNot set
both{left}: {leftValue}; {right}: {rightValue}
nothingToResolveNothing to resolve
noActionsNo way to resolve this here.
resolvedAsResolved: {action}
resolvedResolved
changeChange
changeForChange how {record} is resolved
actionFor{action}: {record}
emptyNo rows to reconcile.
noneShownNo rows of this kind.
showingShowing {from}–{to} of {total}
pagesPages
previousPagePrevious page
nextPageNext page
firstPageThis is the first page.
lastPageThis is the last page.
page{page} / {pages}

Motion

MomentFamilyWhat it does
tinttintA filter option changes its background when chosen, and a ring fades in on keyboard focus.

Set with --cd-motion-reconcile-table-<moment>-* in your brand file; the families are in the design reference.

Types

ReconcileValues
A record's values in one source, by field id, as text your app formatted. `null` means the source has no value.
Record<string, string | null | undefined>
ReconcileStatus
How a row stands between the two sources.
"matched" | "conflict" | "left-only" | "right-only"
ReconcileRow
One row: matched between both sources, or found in only one of them.
RowBase & ({ left: ReconcileValues; right: ReconcileValues } | { left: ReconcileValues; right?: undefined } | { left?: undefined; right: ReconcileValues })
ReconcileField
A column: one field both sources may have.
{ id: string; label: string }
ReconcileAction
A way to resolve a row that your app lists, such as take the left value, take the right one, edit, or skip. `for` says which rows offer it; it defaults to every row that isn't matched.
{ id: string; label: string; for?: ReconcileStatus[] | undefined }

Types from other items

The props above take these. Each is defined by another item; import it from the path shown.

HeadingLevel from @/lib/candor/heading-level
A heading's level in the page's outline: `1` is `<h1>`, `6` is `<h6>`. Every Candor item that draws a heading takes its `headingLevel` as this type; the level is where the heading sits in the outline, never how large it looks.
1 | 2 | 3 | 4 | 5 | 6