Reconcile table
ProRows 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.
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
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
"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>
| Prop | Type | Default | Description |
|---|---|---|---|
| sourcesrequired | { left: string; right: string } | — | The two sources' names, such as "Invoice" and "Ledger". |
| fieldsrequired | ReconcileField[] | — | The fields to compare, one column each, in order. |
| rowsrequired | ReconcileRow[] | — | The rows, in the order to show them. |
| actionsrequired | ReconcileAction[] | — | 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. |
| pageSize | number | undefined | 25 | Rows per page. Defaults to 25. |
| title | string | undefined | — | A heading over the table, also its accessible name. Defaults to "Reconciliation" for the name, with no heading shown. |
| headingLevel | HeadingLevel | undefined | — | The heading's level when `title` is set. Defaults to one below the nearest `HeadingScope`, else 2 (`<h2>`). |
| className | string | 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.
| Key | English |
|---|---|
| label | Reconciliation |
| table | Rows from {left} and {right} |
| openCount | {open} to resolve |
| resolvedCount | {resolved} resolved |
| matchedCount | {matched} matched |
| show | Show |
| all | All |
| open | To resolve |
| conflict | Conflicts |
| only | Only in {source} |
| matched | Matched |
| option | {label} {count} |
| record | Record |
| status | Status |
| resolve | Resolve |
| differ | { one: "{count} field differs", other: "{count} fields differ" } |
| notIn | Not in {source} |
| notSet | Not set |
| both | {left}: {leftValue}; {right}: {rightValue} |
| nothingToResolve | Nothing to resolve |
| noActions | No way to resolve this here. |
| resolvedAs | Resolved: {action} |
| resolved | Resolved |
| change | Change |
| changeFor | Change how {record} is resolved |
| actionFor | {action}: {record} |
| empty | No rows to reconcile. |
| noneShown | No rows of this kind. |
| showing | Showing {from}–{to} of {total} |
| pages | Pages |
| previousPage | Previous page |
| nextPage | Next page |
| firstPage | This is the first page. |
| lastPage | This is the last page. |
| page | {page} / {pages} |
Motion
| Moment | Family | What it does |
|---|---|---|
| tint | tint | A 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