Examples
Date select cell
Use this when a row needs one compact date field such as renewal, due date, or scheduled handoff.
Interactive cell
Live preview
Build the React component
The cell only needs to show the current date clearly and open a small picker. Keep the rest of the date logic outside the render path.
import { CalendarIcon } from "lucide-react"
import { format } from "date-fns"
import { useState } from "react"
import { Calendar } from "@/components/ui/calendar"
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
export function DateSelectCell({
value,
onChange,
}: {
value: string
onChange: (value: string) => void
}) {
const [open, setOpen] = useState(false)
const date = new Date(value + "T00:00:00Z")
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<button className="inline-flex h-7 items-center gap-1.5 rounded-md px-1.5 py-1 text-[12px] leading-4 hover:bg-accent" type="button">
<CalendarIcon className="size-3 text-muted-foreground" />
<span>{format(date, "MMM d, yyyy")}</span>
</button>
</PopoverTrigger>
<PopoverContent align="start" className="w-auto p-3">
<Calendar
className="p-0"
mode="single"
selected={date}
onSelect={(next) => {
if (!next) return
onChange(next.toISOString().slice(0, 10))
setOpen(false)
}}
/>
</PopoverContent>
</Popover>
)
}Wire it into a column definition
The column still keeps the durable field identity. The cell just gives that date field a compact inline picker.
Even with a custom cell, the column still needs an accessor. In this example that is
accessorKey="renewal". That gives the table a stable field for sorting, filtering, and saved state, while the cell can still read extra data from row.original.{
id: "renewal",
header: "Renewal",
accessorKey: "renewal",
width: 150,
enableSorting: false,
enableFiltering: false,
cell: ({ row }) => (
<DateSelectCell
value={row.original.renewal}
onChange={(renewal) => updateRow(row.original.id, { renewal })}
/>
),
}Render it in a small table
This table renders one custom renewal column beside plain text columns so the integration stays easy to copy.
function RenewalExampleTable() {
const [rows, setRows] = useState(seedRows)
const updateRow = (id: string, patch: Partial<CustomerRow>) => {
setRows((current) => current.map((row) => (row.id === id ? { ...row, ...patch } : row)))
}
return <Datatable tableKey="custom-cells" data={rows} columns={columns(updateRow)} getRowId={(row) => row.id} toolbar={false} />
}Mini table demo
Loading table preferences...
Persist changes with optimistic update
In production, optimistic date updates are usually enough: patch the row locally, send the mutation, then refetch for backend truth.
async function updateRenewal(id: string, renewal: string) {
setRows((current) => current.map((row) => (row.id === id ? { ...row, renewal } : row)))
try {
await api.customers.updateRenewal({ id, renewal })
await refetchCustomers()
} catch {
await refetchCustomers()
}
}