Loading home page
Generate a typed TanStack table from a Zod object schema and row data.
| Full name | Active | Profile | |
|---|---|---|---|
| Ada Lovelace | ada@example.com | true | {"role":"Engineer"} |
| Grace Hopper | grace@example.com | false | {"role":"Researcher"} |
| Margaret Hamilton | margaret@example.com | true | {"role":"Lead"} |
Install the component, its table primitive, and runtime dependencies.
bunx --bun shadcn@latest add https://huanngdev.site/r/zod-data-table.jsonPass a Zod object schema and rows inferred from its output type.
"use client";
import { z } from "zod";
import { ZodDataTable } from "@/components/zod-data-table/ZodDataTable";
const userSchema = z.object({
name: z.string().meta({ title: "Full name" }),
email: z.email(),
active: z.boolean(),
profile: z.object({ role: z.string() }),
});
const users: Array<z.output<typeof userSchema>> = [
{
name: "Ada Lovelace",
email: "ada@example.com",
active: true,
profile: { role: "Engineer" },
},
];
export function UsersTable() {
return <ZodDataTable schema={userSchema} data={users} />;
}The table uses the schema to define columns and infer types. Parse untrusted API or form data before rendering so validation does not repeat on every render.
const result = z.array(userSchema).safeParse(untrustedData);
if (!result.success) {
// Handle the validation error at the data boundary.
return;
}
<ZodDataTable schema={userSchema} data={result.data} />;Override only the fields that need different content or visibility.
| Full name | Contact | Status |
|---|---|---|
| Ada Lovelace | ada@example.com | Active |
| Grace Hopper | grace@example.com | Inactive |
| Margaret Hamilton | margaret@example.com | Active |
Use a field title for labels without adding a column override.
const userSchema = z.object({
firstName: z.string().meta({ title: "First name" }),
lastName: z.string().meta({ title: "Last name" }),
createdAt: z.date().meta({ title: "Created" }),
});Header priority is the column override, then the field metadata title, then a humanized field key. Put .meta() last when composing a field so the metadata belongs to the final schema instance.
Keep the generated headers visible while explaining that no rows matched.
| Full name | Active | Profile | |
|---|---|---|---|
| No users found. | |||
| Prop | Type | Default | Description |
|---|---|---|---|
| schema | TSchema extends z.ZodObject | — | Object schema whose top-level fields define the columns and row type. |
| data | Array<z.output<TSchema>> | — | Already-validated rows. The component does not parse them again. |
| columns | ZodDataTableColumns<Row> | — | Typed overrides for headers, cell rendering, and field visibility. |
| emptyMessage | string | "No results." | Message rendered when the data array is empty. |
| className | string | — | Classes applied to the bordered table container. |
type ZodDataTableColumnContext<TRow, TKey extends keyof TRow & string> = {
value: TRow[TKey];
row: TRow;
rowIndex: number;
};
type ZodDataTableColumn<TRow, TKey extends keyof TRow & string> = {
header?: React.ReactNode;
hidden?: boolean;
cell?: (context: ZodDataTableColumnContext<TRow, TKey>) => React.ReactNode;
};
type ZodDataTableColumns<TRow> = {
[TKey in keyof TRow & string]?: ZodDataTableColumn<TRow, TKey>;
};
type ZodDataTableProps<TSchema extends z.ZodObject> = {
schema: TSchema;
data: Array<z.output<TSchema>>;
columns?: ZodDataTableColumns<z.output<TSchema>>;
emptyMessage?: string;
className?: string;
};