> ## Documentation Index
> Fetch the complete documentation index at: https://anvil.servicetitan.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Tables

> Reference implementations for tabular data — sorting, grouping, pinning, footers, expansion, and validation states.

export const LiveCode = ({children, customHeight, clickToLoad, example, fullWidth, fullHeight, hideCodeInLiveCode, screenshot, screenshotOnly, showCode: showCodeProp}) => {
  const SCREENSHOTS_BASE = "https://servicetitan.github.io/anvil2-docs-live-code/screenshots";
  const STACKBLITZ_BASE = "https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples";
  const [showCodeBlock, setShowCodeBlock] = useState(showCodeProp ?? false);
  const [isLocalOverride, setIsLocalOverride] = useState(false);
  useEffect(() => {
    const examplePath = `/images/live-code-screenshots-tmp/${example}.png`;
    fetch(examplePath, {
      method: "HEAD"
    }).then(r => {
      if (r.ok) setIsLocalOverride(true);
    }).catch(() => {});
  }, [example]);
  const screenshotBase = isLocalOverride ? "/images/live-code-screenshots-tmp" : SCREENSHOTS_BASE;
  if (screenshotOnly) {
    return <Frame className="flex flex-col">
        <div className="flex dark:hidden" style={{
      justifyContent: "center",
      alignItems: "center",
      width: fullWidth ? "100%" : "50%",
      minHeight: fullHeight ? "284px" : undefined,
      background: "#FFFFFF"
    }}>
          <img srcset={`${screenshotBase}/${example}.png, ${screenshotBase}/${example}-2x.png 2x`} src={`${screenshotBase}/${example}.png`} alt={example} noZoom />
        </div>
        <div className="hidden dark:flex" style={{
      justifyContent: "center",
      alignItems: "center",
      width: fullWidth ? "100%" : "50%",
      minHeight: fullHeight ? "284px" : undefined,
      background: "#141414"
    }}>
          <img srcset={`${screenshotBase}/${example}-dark.png, ${screenshotBase}/${example}-dark-2x.png 2x`} src={`${screenshotBase}/${example}-dark.png`} alt={example} noZoom />
        </div>
      </Frame>;
  }
  if (screenshot) {
    return <Frame className="flex flex-col -mb-2">
        <div className="flex dark:hidden bg-white dark:bg-codeblock border border-gray-950/10 dark:border-white/10 dark:twoslash-dark rounded-2xl overflow-hidden" style={{
      justifyContent: "center",
      alignItems: "center",
      width: fullWidth ? "100%" : "50%",
      minHeight: fullHeight ? "284px" : undefined
    }}>
          <img srcset={`${screenshotBase}/${example}.png, ${screenshotBase}/${example}-2x.png 2x`} src={`${screenshotBase}/${example}.png`} alt={example} noZoom />
        </div>

        <div className="hidden dark:flex bg-white dark:bg-codeblock border border-gray-950/10 dark:border-white/10 dark:twoslash-dark rounded-2xl overflow-hidden" style={{
      background: "#141414",
      justifyContent: "center",
      alignItems: "center",
      width: fullWidth ? "100%" : "50%",
      minHeight: fullHeight ? "284px" : undefined
    }}>
          <img srcset={`${screenshotBase}/${example}-dark.png, ${screenshotBase}/${example}-dark-2x.png 2x`} src={`${screenshotBase}/${example}-dark.png`} alt={example} noZoom />
        </div>

        <div className="flex justify-end items-center text-xs py-2 px-1 gap-4">
          {!showCodeProp ? <button className="inline-flex justify-end items-center text-gray-700 dark:text-gray-50 hover:text-blue-500 dark:hover:text-blue-300 transition-colors group self-end gap-1 cursor-pointer" onClick={() => setShowCodeBlock(!showCodeBlock)} style={{
      appearance: "none"
    }}>
              <Icon icon="code" size="12px" className="group-hover:bg-blue-500 dark:group-hover:bg-blue-300" />
              <span>{showCodeBlock ? "Hide code" : "Show code"}</span>
            </button> : null}

          <a className="inline-flex justify-end items-center hover:text-blue-500 dark:hover:text-blue-300 transition-colors group self-end gap-1" href={`${STACKBLITZ_BASE}/${example}?file=src/App.tsx`} target="_blank" rel="noreferrer">
            <Icon icon="bolt" size="12px" className="group-hover:bg-blue-500 dark:group-hover:bg-blue-300" />
            <span>StackBlitz demo</span>
          </a>
        </div>

        <div className="grid transition-[grid-template-rows] duration-300 ease-in-out overflow-auto overflow-y-hidden overflow-x-auto" style={showCodeBlock ? {
      gridTemplateRows: "1fr"
    } : {
      gridTemplateRows: "0fr"
    }}>
          <div style={{
      minHeight: 0,
      overflowX: "auto",
      overflowY: "hidden",
      marginBlockStart: "-1.25rem",
      marginBlockEnd: "-1.5rem"
    }}>
            {children}
          </div>
        </div>
      </Frame>;
  } else {
    return <div style={{
      display: "flex",
      width: fullWidth ? "100%" : "50%",
      minHeight: customHeight ? customHeight : "316px",
      resize: "vertical",
      overflow: "auto"
    }}>
        <iframe title={example} style={{
      flex: 1,
      width: fullWidth ? "100%" : "50%",
      minHeight: customHeight ? customHeight : "316px"
    }} src={`${STACKBLITZ_BASE}/${example}?embed=1&hideNavigation=1&hideExplorer=1&terminalHeight=0&file=src/App.tsx${clickToLoad ? "&ctl=1" : ""}${hideCodeInLiveCode ? "&view=preview" : ""}`} allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking" sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts" />
      </div>;
  }
};

Tables present structured, row-based data — orders, call logs, audit trails — for scanning, comparing, and drilling into records. These examples compose the beta `DataTable` with `Chip`, `Avatar`, and `Card` to cover the common patterns: plain text and associated-entity columns, sorted and grouped headers, pinned columns, custom footers, expandable and selectable sub rows, a custom expanded-row component, and cell- and row-level validation states. Each example below is a starting point: copy it, then wire it to your data.

## Resources

<Card title="Data Table" icon="layer-group" href="/docs/web/components/data-table/design">
  Data Table component anatomy and usage guidance.
</Card>

<Card title="Tables" icon="figma" href="https://www.figma.com/design/9OB8FwOZZc079PBR6XY8vP/Anvil-Blocks?node-id=3111-32167">
  Figma reference implementations for the Tables block, covering all eleven variants below.
</Card>

## Text Only, No Actions

A plain audit log — read-only rows with no sorting, selection, or row actions. Use this variant for history and activity records where the data itself is the point.

<LiveCode example="block-tables-text-no-actions" screenshot fullWidth>
  ```tsx lines theme={null}
  import { Avatar, Flex, Text } from "@servicetitan/anvil2";
  import { DataTable, createColumnHelper } from "@servicetitan/anvil2/beta";

  type AuditEntry = {
    id: string;
    dateTime: string;
    username: string;
    deactivated?: boolean;
    action: string;
    visitLocation: string;
    field: string;
    before: string;
    after: string;
  };

  const entries: AuditEntry[] = [
    {
      id: "1",
      dateTime: "05/12/2024 10:15 AM",
      username: "mhyatt",
      action: "Added",
      visitLocation: "–",
      field: "Billing Address",
      before: "–",
      after: "600 World Way, Los Angeles, CA 90045 USA",
    },
    {
      id: "2",
      dateTime: "05/12/2024 10:15 AM",
      username: "mhyatt",
      action: "Added",
      visitLocation: "–",
      field: "Payment Method",
      before: "–",
      after: "Cash / Check",
    },
    {
      id: "3",
      dateTime: "05/12/2024 10:15 AM",
      username: "mhyatt",
      action: "Added",
      visitLocation: "–",
      field: "Payment Terms",
      before: "–",
      after: "Due Upon Receipt",
    },
    {
      id: "4",
      dateTime: "05/12/2024 10:15 AM",
      username: "mhyatt",
      action: "Added",
      visitLocation: "–",
      field: "Billing Schedule",
      before: "–",
      after: "Time of Service",
    },
    {
      id: "5",
      dateTime: "05/12/2024 10:13 AM",
      username: "mhyatt",
      action: "Added",
      visitLocation: "–",
      field: "Markup Value: Material Cost",
      before: "–",
      after: "6%",
    },
    {
      id: "6",
      dateTime: "05/12/2024 10:13 AM",
      username: "mhyatt",
      action: "Added",
      visitLocation: "–",
      field: "Markup Value: Labor Cost",
      before: "–",
      after: "10%",
    },
    {
      id: "7",
      dateTime: "05/12/2024 10:05 AM",
      username: "mhyatt",
      action: "Added",
      visitLocation: "–",
      field: "Visit",
      before: "–",
      after: "–",
    },
    {
      id: "8",
      dateTime: "05/12/2024 10:03 AM",
      username: "mhyatt",
      action: "Added",
      visitLocation: "Visit 1 at Location #3",
      field: "Visit",
      before: "–",
      after: "–",
    },
    {
      id: "9",
      dateTime: "05/12/2024 10:01 AM",
      username: "mhyatt",
      action: "Added",
      visitLocation: "Visit 2 at Location #1",
      field: "Visit",
      before: "–",
      after: "–",
    },
    {
      id: "10",
      dateTime: "05/12/2024 9:53 AM",
      username: "amyho",
      deactivated: true,
      action: "Added",
      visitLocation: "Visit 1 at Location #1",
      field: "Equipment",
      before: "–",
      after: "E1000 E1002 (Deactivated) E1005",
    },
    {
      id: "11",
      dateTime: "05/12/2024 9:50 AM",
      username: "mhyatt",
      action: "Added",
      visitLocation: "–",
      field: "Payment Method",
      before: "–",
      after: "Credit Card",
    },
    {
      id: "12",
      dateTime: "05/12/2024 9:48 AM",
      username: "mhyatt",
      action: "Added",
      visitLocation: "–",
      field: "Business Unit",
      before: "–",
      after: "HVAC Install",
    },
    {
      id: "13",
      dateTime: "05/12/2024 9:45 AM",
      username: "amyho",
      deactivated: true,
      action: "Added",
      visitLocation: "–",
      field: "Customer Type",
      before: "–",
      after: "Commercial",
    },
  ];

  const createColumn = createColumnHelper<AuditEntry>();

  const columns = [
    createColumn("dateTime", { header: { label: "Date & Time" }, minWidth: 170 }),
    createColumn("username", {
      header: { label: "Username" },
      minWidth: 160,
      renderCell: (
        value: AuditEntry["username"],
        { row }: { row: AuditEntry },
      ) => (
        <Flex alignItems="center" gap={2}>
          <Avatar name={value} size="small" />
          <Text inline subdued={row?.deactivated}>
            {value}
            {row?.deactivated ? " (Deactivated)" : ""}
          </Text>
        </Flex>
      ),
    }),
    createColumn("action", { header: { label: "Action" }, minWidth: 100 }),
    createColumn("visitLocation", {
      header: { label: "Visit / Location" },
      minWidth: 170,
    }),
    createColumn("field", { header: { label: "Field" }, minWidth: 190 }),
    createColumn("before", { header: { label: "Before" }, minWidth: 90 }),
    createColumn("after", { header: { label: "After" }, minWidth: 220 }),
  ];

  function App() {
    return (
      <Flex style={{ padding: "2rem", width: "100%" }}>
        <DataTable
          data={entries}
          columns={columns}
          pagination={{ rowsPerPage: 10, showCount: true }}
          style={{ width: "100%" }}
        />
      </Flex>
    );
  }

  export default App;
  ```
</LiveCode>

## Associated Entities

Columns that reference other records — accounts, customers, campaigns — render as `Chip` and `Avatar` instead of plain text, so a row's related entities are scannable at a glance.

<LiveCode example="block-tables-associated-entities" screenshot fullWidth>
  ```tsx lines theme={null}
  import { Avatar, Chip, Flex, Text } from "@servicetitan/anvil2";
  import { DataTable, createColumnHelper } from "@servicetitan/anvil2/beta";

  type Call = {
    id: string;
    status: string;
    date: string;
    duration: string;
    account: string;
    customer: string;
    queue: string;
    campaign: string;
    calls: string;
  };

  const calls: Call[] = [
    {
      id: "1",
      status: "Contact Attempted",
      date: "2026-04-16 3:00 PM",
      duration: "1m",
      account: "EverTemp HVAC",
      customer: "Wade Warren",
      queue: "Front Line",
      campaign: "Google",
      calls: "2 abandoned calls",
    },
    {
      id: "2",
      status: "New",
      date: "2026-04-16 2:00 PM",
      duration: "4m",
      account: "Automated Mechanical",
      customer: "Sadie Lewis",
      queue: "Front Line",
      campaign: "Yelp",
      calls: "–",
    },
    {
      id: "3",
      status: "New",
      date: "2026-04-16 12:00 PM",
      duration: "12s",
      account: "Enopia Solutions",
      customer: "Marvin McKinney",
      queue: "Overflow",
      campaign: "Facebook",
      calls: "–",
    },
    {
      id: "4",
      status: "New",
      date: "2026-04-16 11:00 AM",
      duration: "7m",
      account: "Stark Industries",
      customer: "Wade Warren",
      queue: "After Hours",
      campaign: "Direct Mail",
      calls: "–",
    },
    {
      id: "5",
      status: "New",
      date: "2026-04-16 11:00 AM",
      duration: "2m",
      account: "Massive Dynamic",
      customer: "Floyd Miles",
      queue: "Front Line",
      campaign: "Referral",
      calls: "3 abandoned calls",
    },
    {
      id: "6",
      status: "New",
      date: "2026-04-16 11:00 AM",
      duration: "45s",
      account: "Dach Group",
      customer: "Jerome Bell",
      queue: "Overflow",
      campaign: "Angi",
      calls: "–",
    },
    {
      id: "7",
      status: "New",
      date: "2026-04-16 11:00 AM",
      duration: "3m",
      account: "Hearty Home Décor",
      customer: "Arlene McCoy",
      queue: "After Hours",
      campaign: "Google",
      calls: "–",
    },
    {
      id: "8",
      status: "New",
      date: "2026-04-16 11:00 AM",
      duration: "1m",
      account: "The Sleek Apple",
      customer: "Bessie Cooper",
      queue: "Front Line",
      campaign: "Nextdoor",
      calls: "2 abandoned calls",
    },
    {
      id: "9",
      status: "New",
      date: "2026-04-16 11:00 AM",
      duration: "6m",
      account: "Ollivander's Wands",
      customer: "Darlene Robertson",
      queue: "Overflow",
      campaign: "Yelp",
      calls: "–",
    },
    {
      id: "10",
      status: "New",
      date: "2026-04-16 1:00 PM",
      duration: "28s",
      account: "The Showroom",
      customer: "Theresa Webb",
      queue: "Front Line",
      campaign: "Referral",
      calls: "–",
    },
  ];

  const createColumn = createColumnHelper<Call>();

  const columns = [
    createColumn("status", { header: { label: "Status" }, minWidth: 150 }),
    createColumn("date", {
      header: { label: "Date" },
      sortable: true,
      minWidth: 150,
    }),
    createColumn("duration", { header: { label: "Duration" }, minWidth: 90 }),
    createColumn("account", {
      header: { label: "Account" },
      minWidth: 190,
      renderCell: (value: Call["account"]) => <Chip label={value} size="small" />,
    }),
    createColumn("customer", {
      header: { label: "Customer" },
      minWidth: 170,
      renderCell: (value: Call["customer"]) => (
        <Flex alignItems="center" gap={2}>
          <Avatar name={value} size="small" />
          <Text inline>{value}</Text>
        </Flex>
      ),
    }),
    createColumn("queue", { header: { label: "Queue" }, minWidth: 110 }),
    createColumn("campaign", {
      header: { label: "Campaign" },
      minWidth: 110,
      renderCell: (value: Call["campaign"]) => (
        <Chip label={value} size="small" />
      ),
    }),
    createColumn("calls", { header: { label: "Calls" }, minWidth: 160 }),
  ];

  function App() {
    return (
      <Flex style={{ padding: "2rem", width: "100%" }}>
        <DataTable
          data={calls}
          columns={columns}
          pagination={{ rowsPerPage: 10, showCount: true }}
          style={{ width: "100%" }}
        />
      </Flex>
    );
  }

  export default App;
  ```
</LiveCode>

## Sorted Column

Setting `defaultSortedColumn` sorts the table on load and shows the sort indicator in the header — here, orders sorted by Status.

<LiveCode example="block-tables-sorted-column" screenshot fullWidth>
  ```tsx lines theme={null}
  import { Chip, Flex } from "@servicetitan/anvil2";
  import { DataTable, createColumnHelper } from "@servicetitan/anvil2/beta";

  type Order = {
    id: string;
    customerName: string;
    amount: number | null;
    items: number;
    status: string;
    orderDate: string;
    paymentType: string;
    note: string;
  };

  const orders: Order[] = [
    {
      id: "ORD-2024-001",
      customerName: "Sophia Rodriguez",
      amount: 2450.75,
      items: 12,
      status: "Completed",
      orderDate: "10/02/25",
      paymentType: "Credit card",
      note: "Bulk order for office supplies",
    },
    {
      id: "ORD-2024-004",
      customerName: "Maria Gonzalez",
      amount: 1200.5,
      items: 8,
      status: "Processing",
      orderDate: "09/28/25",
      paymentType: "Credit card",
      note: "Rush delivery requested",
    },
    {
      id: "ORD-2024-005",
      customerName: "Chen Wei",
      amount: null,
      items: 1,
      status: "Cancelled",
      orderDate: "10/01/25",
      paymentType: "Credit card",
      note: "Customer cancelled before payment",
    },
    {
      id: "ORD-2024-003",
      customerName: "Ahmed Hassan",
      amount: 89.99,
      items: 2,
      status: "Pending",
      orderDate: "09/14/25",
      paymentType: "Paypal",
      note: "–",
    },
    {
      id: "ORD-2024-006",
      customerName: "Green Valley Farms",
      amount: 8925.0,
      items: 156,
      status: "Completed",
      orderDate: "10/03/25",
      paymentType: "Check",
      note: "–",
    },
    {
      id: "ORD-2024-010",
      customerName: "Moonlight Café",
      amount: 2100.0,
      items: 35,
      status: "Cancelled",
      orderDate: "08/22/25",
      paymentType: "Credit card",
      note: "Monthly coffee order",
    },
    {
      id: "ORD-2024-017",
      customerName: "Michael O'Connor",
      amount: 125.0,
      items: 1,
      status: "Pending",
      orderDate: "09/30/25",
      paymentType: "Credit card",
      note: "–",
    },
    {
      id: "ORD-2024-009",
      customerName: "Emily Watson",
      amount: 156.78,
      items: 4,
      status: "Completed",
      orderDate: "10/04/25",
      paymentType: "Paypal",
      note: "–",
    },
    {
      id: "ORD-2024-011",
      customerName: "David Thompson",
      amount: 750.0,
      items: 6,
      status: "Cancelled",
      orderDate: "07/19/25",
      paymentType: "Credit card",
      note: "Product out of stock",
    },
    {
      id: "ORD-2024-014",
      customerName: "Sarah Johnson",
      amount: 78.5,
      items: 2,
      status: "Completed",
      orderDate: "09/05/25",
      paymentType: "Cash",
      note: "–",
    },
    {
      id: "ORD-2024-020",
      customerName: "Anna Kowalski",
      amount: 89.95,
      items: 2,
      status: "Cancelled",
      orderDate: "10/05/25",
      paymentType: "Credit card",
      note: "Duplicate order",
    },
    {
      id: "ORD-2024-028",
      customerName: "Elena Popov",
      amount: null,
      items: 1,
      status: "Cancelled",
      orderDate: "08/30/25",
      paymentType: "Paypal",
      note: "Found cheaper price",
    },
  ];

  const currency = (value: number) =>
    `$${value.toLocaleString("en-US", {
      minimumFractionDigits: 2,
      maximumFractionDigits: 2,
    })}`;

  const createColumn = createColumnHelper<Order>();

  const columns = [
    createColumn("id", {
      header: { label: "Order ID" },
      sortable: true,
      minWidth: 130,
    }),
    createColumn("customerName", {
      header: { label: "Customer Name" },
      sortable: true,
    }),
    createColumn("amount", {
      header: { label: "Amount" },
      align: "end",
      sortable: true,
      renderCell: (value: Order["amount"]) =>
        value == null ? "–" : currency(value),
    }),
    createColumn("items", {
      header: { label: "Items" },
      align: "end",
      sortable: true,
      minWidth: 80,
    }),
    createColumn("status", {
      header: { label: "Status" },
      sortable: true,
      renderCell: (value: Order["status"]) => <Chip label={value} size="small" />,
    }),
    createColumn("orderDate", {
      header: { label: "Order Date" },
      sortable: true,
    }),
    createColumn("paymentType", {
      header: { label: "Payment Type" },
      minWidth: 150,
    }),
    createColumn("note", { header: { label: "Note" }, minWidth: 220 }),
  ];

  function App() {
    return (
      <Flex style={{ padding: "2rem", width: "100%" }}>
        <DataTable
          data={orders}
          columns={columns}
          defaultSortedColumn={{ id: "status", desc: false }}
          pagination={{ rowsPerPage: 10, showCount: true }}
          style={{ width: "100%" }}
        />
      </Flex>
    );
  }

  export default App;
  ```
</LiveCode>

## Column Groups

The column creator returned by `createColumnHelper` accepts a `{ group }` id as its first argument to nest related columns under a shared header — here, order identity groups separately from purchase details.

<LiveCode example="block-tables-groups" screenshot fullWidth>
  ```tsx lines theme={null}
  import { Chip, Flex } from "@servicetitan/anvil2";
  import { DataTable, createColumnHelper } from "@servicetitan/anvil2/beta";

  type Order = {
    id: string;
    customerName: string;
    amount: number | null;
    items: number;
    status: string;
    orderDate: string;
    paymentType: string;
    note: string;
  };

  const orders: Order[] = [
    {
      id: "ORD-2024-001",
      customerName: "Sophia Rodriguez",
      amount: 2450.75,
      items: 12,
      status: "Completed",
      orderDate: "10/02/25",
      paymentType: "Credit card",
      note: "Bulk order for office supplies",
    },
    {
      id: "ORD-2024-002",
      customerName: "TechCorp Solutions",
      amount: 15750.0,
      items: 45,
      status: "Shipped",
      orderDate: "10/02/25",
      paymentType: "Bank transfer",
      note: "–",
    },
    {
      id: "ORD-2024-003",
      customerName: "Ahmed Hassan",
      amount: 89.99,
      items: 2,
      status: "Pending",
      orderDate: "10/02/25",
      paymentType: "Paypal",
      note: "–",
    },
    {
      id: "ORD-2024-004",
      customerName: "Maria Gonzalez",
      amount: 1200.5,
      items: 8,
      status: "Processing",
      orderDate: "10/02/25",
      paymentType: "Credit card",
      note: "Rush delivery requested",
    },
    {
      id: "ORD-2024-005",
      customerName: "Chen Wei",
      amount: null,
      items: 1,
      status: "Cancelled",
      orderDate: "10/02/25",
      paymentType: "Credit card",
      note: "Customer cancelled before payment",
    },
    {
      id: "ORD-2024-006",
      customerName: "Green Valley Farms",
      amount: 8925.0,
      items: 156,
      status: "Completed",
      orderDate: "10/02/25",
      paymentType: "Check",
      note: "–",
    },
    {
      id: "ORD-2024-007",
      customerName: "Jennifer Kim",
      amount: 45.99,
      items: 1,
      status: "Shipped",
      orderDate: "10/02/25",
      paymentType: "Cash",
      note: "–",
    },
    {
      id: "ORD-2024-008",
      customerName: "Roberto Silva",
      amount: 3780.25,
      items: 23,
      status: "Processing",
      orderDate: "10/02/25",
      paymentType: "Bank transfer",
      note: "International shipping",
    },
    {
      id: "ORD-2024-009",
      customerName: "Emily Watson",
      amount: 156.78,
      items: 4,
      status: "Completed",
      orderDate: "10/02/25",
      paymentType: "Paypal",
      note: "–",
    },
    {
      id: "ORD-2024-010",
      customerName: "Moonlight Café",
      amount: 2100.0,
      items: 35,
      status: "Cancelled",
      orderDate: "10/02/25",
      paymentType: "Credit card",
      note: "Monthly coffee order",
    },
  ];

  const currency = (value: number) =>
    `$${value.toLocaleString("en-US", {
      minimumFractionDigits: 2,
      maximumFractionDigits: 2,
    })}`;

  const createColumn = createColumnHelper<Order>();

  const orderInfoColumns = [
    createColumn("id", {
      header: { label: "Order ID" },
      sortable: true,
      minWidth: 130,
    }),
    createColumn("customerName", {
      header: { label: "Customer Name" },
      sortable: true,
    }),
    createColumn("amount", {
      header: { label: "Amount" },
      align: "end",
      sortable: true,
      renderCell: (value: Order["amount"]) =>
        value == null ? "–" : currency(value),
    }),
  ];

  const purchaseDetailsColumns = [
    createColumn("items", {
      header: { label: "Items" },
      align: "end",
      sortable: true,
      minWidth: 80,
    }),
    createColumn("status", {
      header: { label: "Status" },
      renderCell: (value: Order["status"]) => <Chip label={value} size="small" />,
    }),
    createColumn("orderDate", {
      header: { label: "Order Date" },
      sortable: true,
    }),
    createColumn("paymentType", {
      header: { label: "Payment Type" },
      minWidth: 150,
    }),
    createColumn("note", { header: { label: "Note" }, minWidth: 220 }),
  ];

  const columns = [
    createColumn(
      { group: "order-info" },
      { header: { label: "Order Info" }, columns: orderInfoColumns },
    ),
    createColumn(
      { group: "purchase-details" },
      { header: { label: "Purchase Details" }, columns: purchaseDetailsColumns },
    ),
  ];

  function App() {
    return (
      <Flex style={{ padding: "2rem", width: "100%" }}>
        <DataTable
          data={orders}
          columns={columns}
          pagination={{ rowsPerPage: 10, showCount: true }}
          style={{ width: "100%" }}
        />
      </Flex>
    );
  }

  export default App;
  ```
</LiveCode>

## Pinned Columns

Setting `pinned: "left"` or `pinned: "right"` on a column keeps it fixed while the rest of the table scrolls horizontally — here, Order ID stays pinned left and Status stays pinned right.

<LiveCode example="block-tables-pinned-columns" screenshot fullWidth>
  ```tsx lines theme={null}
  import { Chip, Flex } from "@servicetitan/anvil2";
  import { DataTable, createColumnHelper } from "@servicetitan/anvil2/beta";

  type Order = {
    id: string;
    customerName: string;
    amount: number | null;
    items: number;
    orderDate: string;
    paymentType: string;
    note: string;
    status: string;
  };

  const orders: Order[] = [
    {
      id: "ORD-2024-003",
      customerName: "Ahmed Hassan",
      amount: 89.99,
      items: 2,
      orderDate: "10/02/25",
      paymentType: "Paypal",
      note: "–",
      status: "Pending",
    },
    {
      id: "ORD-2024-020",
      customerName: "Anna Kowalski",
      amount: 89.95,
      items: 2,
      orderDate: "10/02/25",
      paymentType: "Credit card",
      note: "Duplicate order",
      status: "Pending",
    },
    {
      id: "ORD-2024-019",
      customerName: "Blue Ocean Seafood",
      amount: 12300.0,
      items: 234,
      orderDate: "10/02/25",
      paymentType: "Check",
      note: "Weekly seafood delivery",
      status: "Pending",
    },
    {
      id: "ORD-2024-005",
      customerName: "Chen Wei",
      amount: null,
      items: 1,
      orderDate: "10/02/25",
      paymentType: "Credit card",
      note: "Customer cancelled before payment",
      status: "Processing",
    },
    {
      id: "ORD-2024-011",
      customerName: "David Thompson",
      amount: 750.0,
      items: 6,
      orderDate: "10/02/25",
      paymentType: "Credit card",
      note: "Product out of stock",
      status: "Processing",
    },
    {
      id: "ORD-2024-009",
      customerName: "Emily Watson",
      amount: 156.78,
      items: 4,
      orderDate: "10/02/25",
      paymentType: "Paypal",
      note: "–",
      status: "Processing",
    },
    {
      id: "ORD-2024-018",
      customerName: "Fatima Al-Zahra",
      amount: 567.89,
      items: 9,
      orderDate: "10/02/25",
      paymentType: "Paypal",
      note: "Gift wrap requested",
      status: "Processing",
    },
    {
      id: "ORD-2024-006",
      customerName: "Green Valley Farms",
      amount: 8925.0,
      items: 156,
      orderDate: "10/02/25",
      paymentType: "Check",
      note: "–",
      status: "Shipped",
    },
    {
      id: "ORD-2024-015",
      customerName: "Hiroshi Tanaka",
      amount: 4950.0,
      items: 18,
      orderDate: "10/02/25",
      paymentType: "Paypal",
      note: "Electronics bundle",
      status: "Shipped",
    },
    {
      id: "ORD-2024-031",
      customerName: "Ian Wright",
      amount: 220.0,
      items: 3,
      orderDate: "10/02/25",
      paymentType: "Credit card",
      note: "–",
      status: "Shipped",
    },
  ];

  const currency = (value: number) =>
    `$${value.toLocaleString("en-US", {
      minimumFractionDigits: 2,
      maximumFractionDigits: 2,
    })}`;

  const createColumn = createColumnHelper<Order>();

  const columns = [
    createColumn("id", {
      header: { label: "Order ID" },
      sortable: true,
      minWidth: 140,
      pinned: "left",
    }),
    createColumn("customerName", {
      header: { label: "Customer Name" },
      minWidth: 170,
    }),
    createColumn("amount", {
      header: { label: "Amount" },
      align: "end",
      minWidth: 110,
      renderCell: (value: Order["amount"]) =>
        value == null ? "–" : currency(value),
    }),
    createColumn("items", {
      header: { label: "Items" },
      align: "end",
      minWidth: 80,
    }),
    createColumn("orderDate", { header: { label: "Order Date" }, minWidth: 110 }),
    createColumn("paymentType", {
      header: { label: "Payment Type" },
      minWidth: 150,
    }),
    createColumn("note", { header: { label: "Note" }, minWidth: 260 }),
    createColumn("status", {
      header: { label: "Status" },
      minWidth: 120,
      pinned: "right",
      renderCell: (value: Order["status"]) => <Chip label={value} size="small" />,
    }),
  ];

  function App() {
    return (
      <Flex style={{ padding: "2rem", width: "100%" }}>
        <DataTable
          data={orders}
          columns={columns}
          pagination={{ rowsPerPage: 10, showCount: true }}
          style={{ width: "100%", maxWidth: 900 }}
        />
      </Flex>
    );
  }

  export default App;
  ```
</LiveCode>

## Custom Footer

Per-column `footerContent` renders one or more summary rows beneath the data — totals, averages, and computed percentages, aligned under their column.

<LiveCode example="block-tables-custom-footer" screenshot fullWidth>
  ```tsx lines theme={null}
  import { Chip, Flex } from "@servicetitan/anvil2";
  import { DataTable, createColumnHelper } from "@servicetitan/anvil2/beta";

  type Order = {
    id: string;
    customerName: string;
    amount: number | null;
    items: number;
    orderDate: string;
    paymentType: string;
    note: string;
    status: string;
  };

  const orders: Order[] = [
    {
      id: "ORD-2024-003",
      customerName: "Ahmed Hassan",
      amount: 89.99,
      items: 2,
      orderDate: "10/02/25",
      paymentType: "Paypal",
      note: "–",
      status: "Pending",
    },
    {
      id: "ORD-2024-020",
      customerName: "Anna Kowalski",
      amount: 89.95,
      items: 2,
      orderDate: "10/02/25",
      paymentType: "Credit card",
      note: "Duplicate order",
      status: "Pending",
    },
    {
      id: "ORD-2024-019",
      customerName: "Blue Ocean Seafood",
      amount: 12300.0,
      items: 234,
      orderDate: "10/02/25",
      paymentType: "Check",
      note: "Weekly seafood delivery",
      status: "Pending",
    },
    {
      id: "ORD-2024-005",
      customerName: "Chen Wei",
      amount: null,
      items: 1,
      orderDate: "10/02/25",
      paymentType: "Credit card",
      note: "Customer cancelled before payment",
      status: "Processing",
    },
    {
      id: "ORD-2024-011",
      customerName: "David Thompson",
      amount: 750.0,
      items: 6,
      orderDate: "10/02/25",
      paymentType: "Credit card",
      note: "Product out of stock",
      status: "Processing",
    },
    {
      id: "ORD-2024-009",
      customerName: "Emily Watson",
      amount: 156.78,
      items: 4,
      orderDate: "10/02/25",
      paymentType: "Paypal",
      note: "–",
      status: "Processing",
    },
    {
      id: "ORD-2024-018",
      customerName: "Fatima Al-Zahra",
      amount: 567.89,
      items: 9,
      orderDate: "10/02/25",
      paymentType: "Paypal",
      note: "Gift wrap requested",
      status: "Processing",
    },
    {
      id: "ORD-2024-006",
      customerName: "Green Valley Farms",
      amount: 8925.0,
      items: 156,
      orderDate: "10/02/25",
      paymentType: "Check",
      note: "–",
      status: "Shipped",
    },
    {
      id: "ORD-2024-015",
      customerName: "Hiroshi Tanaka",
      amount: 4950.0,
      items: 18,
      orderDate: "10/02/25",
      paymentType: "Paypal",
      note: "Electronics bundle",
      status: "Shipped",
    },
    {
      id: "ORD-2024-031",
      customerName: "Ian Wright",
      amount: 220.0,
      items: 3,
      orderDate: "10/02/25",
      paymentType: "Credit card",
      note: "–",
      status: "Shipped",
    },
  ];

  const currency = (value: number) =>
    `$${value.toLocaleString("en-US", {
      minimumFractionDigits: 2,
      maximumFractionDigits: 2,
    })}`;

  const ordersWithAmount = orders.filter(
    (row): row is Order & { amount: number } => row.amount !== null,
  );
  const totalAmount = ordersWithAmount.reduce((acc, row) => acc + row.amount, 0);
  const averageAmount = totalAmount / ordersWithAmount.length;
  const shippedShare =
    orders.filter((row) => row.status === "Shipped").length / orders.length;

  const createColumn = createColumnHelper<Order>();

  const columns = [
    createColumn("id", {
      header: { label: "Order ID" },
      sortable: true,
      minWidth: 140,
      footerContent: (
        <span>
          <b>Rows:</b> {orders.length}
        </span>
      ),
    }),
    createColumn("customerName", {
      header: { label: "Customer Name" },
      minWidth: 170,
    }),
    createColumn("amount", {
      header: { label: "Amount" },
      align: "end",
      minWidth: 110,
      renderCell: (value: Order["amount"]) =>
        value == null ? "–" : currency(value),
      footerContent: [
        <span key="total">{currency(totalAmount)}</span>,
        <span key="average">{currency(averageAmount)} (avg)</span>,
      ],
    }),
    createColumn("items", {
      header: { label: "Items" },
      align: "end",
      minWidth: 80,
    }),
    createColumn("orderDate", { header: { label: "Order Date" }, minWidth: 110 }),
    createColumn("paymentType", {
      header: { label: "Payment Type" },
      minWidth: 150,
    }),
    createColumn("note", {
      header: { label: "Note" },
      minWidth: 260,
    }),
    createColumn("status", {
      header: { label: "Status" },
      minWidth: 120,
      renderCell: (value: Order["status"]) => <Chip label={value} size="small" />,
      footerContent: <span>Shipped: {(shippedShare * 100).toFixed(0)}%</span>,
    }),
  ];

  function App() {
    return (
      <Flex style={{ padding: "2rem", width: "100%" }}>
        <DataTable data={orders} columns={columns} style={{ width: "100%" }} />
      </Flex>
    );
  }

  export default App;
  ```
</LiveCode>

## Sub Rows

Nesting a `subRows` array under a row makes it expandable — `defaultExpandedRowIds` controls which rows start open. Use this variant when a row's data naturally decomposes into child records, such as an order split across shipments.

<LiveCode example="block-tables-sub-rows" screenshot fullWidth>
  ```tsx lines theme={null}
  import { Chip, Flex } from "@servicetitan/anvil2";
  import {
    DataTable,
    createColumnHelper,
    type TableRow,
  } from "@servicetitan/anvil2/beta";

  type Order = {
    id: string;
    customerName: string;
    amount: number | null;
    items: number;
    orderDate: string;
    paymentType: string;
    note: string;
    status: string;
  };

  const orders: TableRow<Order>[] = [
    {
      id: "ORD-2024-003",
      customerName: "Ahmed Hassan",
      amount: 89.99,
      items: 4,
      orderDate: "10/02/25",
      paymentType: "Paypal",
      note: "–",
      status: "Pending",
      subRows: [
        {
          id: "ORD-2024-003-01",
          customerName: "Ahmed Hassan",
          amount: 44.99,
          items: 3,
          orderDate: "10/02/25",
          paymentType: "Paypal",
          note: "–",
          status: "–",
        },
        {
          id: "ORD-2024-003-02",
          customerName: "Ahmed Hassan",
          amount: 45.0,
          items: 1,
          orderDate: "10/02/25",
          paymentType: "Paypal",
          note: "–",
          status: "–",
        },
      ],
    },
    {
      id: "ORD-2024-020",
      customerName: "Anna Kowalski",
      amount: 89.95,
      items: 2,
      orderDate: "10/02/25",
      paymentType: "Credit card",
      note: "Duplicate order",
      status: "Pending",
    },
    {
      id: "ORD-2024-019",
      customerName: "Blue Ocean Seafood",
      amount: 12300.0,
      items: 234,
      orderDate: "10/02/25",
      paymentType: "Check",
      note: "Weekly seafood delivery",
      status: "Pending",
    },
    {
      id: "ORD-2024-005",
      customerName: "Chen Wei",
      amount: null,
      items: 1,
      orderDate: "10/02/25",
      paymentType: "Credit card",
      note: "Customer cancelled before payment",
      status: "Processing",
    },
    {
      id: "ORD-2024-011",
      customerName: "David Thompson",
      amount: 750.0,
      items: 6,
      orderDate: "10/02/25",
      paymentType: "Credit card",
      note: "Product out of stock",
      status: "Processing",
    },
    {
      id: "ORD-2024-009",
      customerName: "Emily Watson",
      amount: 156.78,
      items: 4,
      orderDate: "10/02/25",
      paymentType: "Paypal",
      note: "–",
      status: "Processing",
    },
  ];

  const currency = (value: number) =>
    `$${value.toLocaleString("en-US", {
      minimumFractionDigits: 2,
      maximumFractionDigits: 2,
    })}`;

  const createColumn = createColumnHelper<Order>();

  const columns = [
    createColumn("id", { header: { label: "Order ID" }, minWidth: 160 }),
    createColumn("customerName", {
      header: { label: "Customer Name" },
      minWidth: 170,
    }),
    createColumn("amount", {
      header: { label: "Amount" },
      align: "end",
      minWidth: 110,
      renderCell: (value: Order["amount"]) =>
        value == null ? "–" : currency(value),
    }),
    createColumn("items", {
      header: { label: "Items" },
      align: "end",
      minWidth: 80,
    }),
    createColumn("orderDate", { header: { label: "Order Date" }, minWidth: 110 }),
    createColumn("paymentType", {
      header: { label: "Payment Type" },
      minWidth: 150,
    }),
    createColumn("note", { header: { label: "Note" }, minWidth: 260 }),
    createColumn("status", {
      header: { label: "Status" },
      minWidth: 120,
      renderCell: (value: Order["status"]) =>
        value === "–" ? value : <Chip label={value} size="small" />,
    }),
  ];

  function App() {
    return (
      <Flex style={{ padding: "2rem", width: "100%" }}>
        <DataTable
          data={orders}
          columns={columns}
          defaultExpandedRowIds={["ORD-2024-003"]}
          style={{ width: "100%" }}
        />
      </Flex>
    );
  }

  export default App;
  ```
</LiveCode>

## Selectable Sub Rows

Row selection and row expansion compose cleanly — `isSelectable` adds checkboxes to every row, parent and child alike, alongside the expand affordance from `subRows`.

<LiveCode example="block-tables-selectable-sub-rows" screenshot fullWidth>
  ```tsx lines theme={null}
  import { Chip, Flex } from "@servicetitan/anvil2";
  import {
    DataTable,
    createColumnHelper,
    type TableRow,
  } from "@servicetitan/anvil2/beta";

  type Order = {
    id: string;
    customerName: string;
    amount: number | null;
    items: number;
    orderDate: string;
    paymentType: string;
    note: string;
    status: string;
  };

  const orders: TableRow<Order>[] = [
    {
      id: "ORD-2024-003",
      customerName: "Ahmed Hassan",
      amount: 89.99,
      items: 4,
      orderDate: "10/02/25",
      paymentType: "Paypal",
      note: "–",
      status: "Pending",
      subRows: [
        {
          id: "ORD-2024-003-01",
          customerName: "Ahmed Hassan",
          amount: 44.99,
          items: 3,
          orderDate: "10/02/25",
          paymentType: "Paypal",
          note: "–",
          status: "–",
        },
        {
          id: "ORD-2024-003-02",
          customerName: "Ahmed Hassan",
          amount: 45.0,
          items: 1,
          orderDate: "10/02/25",
          paymentType: "Paypal",
          note: "–",
          status: "–",
        },
      ],
    },
    {
      id: "ORD-2024-020",
      customerName: "Anna Kowalski",
      amount: 89.95,
      items: 2,
      orderDate: "10/02/25",
      paymentType: "Credit card",
      note: "Duplicate order",
      status: "Pending",
    },
    {
      id: "ORD-2024-019",
      customerName: "Blue Ocean Seafood",
      amount: 12300.0,
      items: 234,
      orderDate: "10/02/25",
      paymentType: "Check",
      note: "Weekly seafood delivery",
      status: "Pending",
    },
    {
      id: "ORD-2024-005",
      customerName: "Chen Wei",
      amount: null,
      items: 1,
      orderDate: "10/02/25",
      paymentType: "Credit card",
      note: "Customer cancelled before payment",
      status: "Processing",
    },
    {
      id: "ORD-2024-011",
      customerName: "David Thompson",
      amount: 750.0,
      items: 6,
      orderDate: "10/02/25",
      paymentType: "Credit card",
      note: "Product out of stock",
      status: "Processing",
    },
    {
      id: "ORD-2024-009",
      customerName: "Emily Watson",
      amount: 156.78,
      items: 4,
      orderDate: "10/02/25",
      paymentType: "Paypal",
      note: "–",
      status: "Processing",
    },
  ];

  const currency = (value: number) =>
    `$${value.toLocaleString("en-US", {
      minimumFractionDigits: 2,
      maximumFractionDigits: 2,
    })}`;

  const createColumn = createColumnHelper<Order>();

  const columns = [
    createColumn("id", { header: { label: "Order ID" }, minWidth: 160 }),
    createColumn("customerName", {
      header: { label: "Customer Name" },
      minWidth: 170,
    }),
    createColumn("amount", {
      header: { label: "Amount" },
      align: "end",
      minWidth: 110,
      renderCell: (value: Order["amount"]) =>
        value == null ? "–" : currency(value),
    }),
    createColumn("items", {
      header: { label: "Items" },
      align: "end",
      minWidth: 80,
    }),
    createColumn("orderDate", { header: { label: "Order Date" }, minWidth: 110 }),
    createColumn("paymentType", {
      header: { label: "Payment Type" },
      minWidth: 150,
    }),
    createColumn("note", { header: { label: "Note" }, minWidth: 260 }),
    createColumn("status", {
      header: { label: "Status" },
      minWidth: 120,
      renderCell: (value: Order["status"]) =>
        value === "–" ? value : <Chip label={value} size="small" />,
    }),
  ];

  function App() {
    return (
      <Flex style={{ padding: "2rem", width: "100%" }}>
        <DataTable
          data={orders}
          columns={columns}
          isSelectable
          defaultExpandedRowIds={["ORD-2024-003"]}
          defaultSelectedRowIds={["ORD-2024-003-01"]}
          style={{ width: "100%" }}
        />
      </Flex>
    );
  }

  export default App;
  ```
</LiveCode>

## Custom Sub Component

A row's `subComponent` can render arbitrary content on expansion, rather than a nested table — here, an invoice preview built from `Card`, `Divider`, and `Text`. Use this variant when a row's detail view doesn't fit the sub-rows shape.

<LiveCode example="block-tables-custom-sub-component" screenshot fullWidth>
  ```tsx lines theme={null}
  import { Card, Chip, Divider, Flex, Text } from "@servicetitan/anvil2";
  import {
    DataTable,
    createColumnHelper,
    type TableRow,
  } from "@servicetitan/anvil2/beta";

  type Order = {
    id: string;
    customerName: string;
    amount: number | null;
    items: number;
    orderDate: string;
    paymentType: string;
    note: string;
    status: string;
  };

  type LineItem = {
    name: string;
    description: string;
    hours: number;
    rate: number;
  };

  const lineItems: LineItem[] = [
    {
      name: "Logo redesign",
      description: "New logo and digital asset playbook.",
      hours: 20,
      rate: 100,
    },
    {
      name: "Website redesign",
      description: "Design and program new company website.",
      hours: 52,
      rate: 100,
    },
    {
      name: "Business cards",
      description: 'Design and print of 3.5" x 2.0" business cards.',
      hours: 12,
      rate: 100,
    },
  ];

  const currency = (value: number) =>
    `$${value.toLocaleString("en-US", {
      minimumFractionDigits: 2,
      maximumFractionDigits: 2,
    })}`;

  const invoicePreview = (
    <Card flexDirection="column" gap={6} style={{ maxWidth: 900, width: "100%" }}>
      <Text variant="headline" el="h3" size="small">
        Invoice
      </Text>
      <Flex justifyContent="space-between" style={{ width: "100%" }}>
        <Text subdued>
          Issued on <b>January 23, 2024</b>
        </Text>
        <Text subdued>
          Due on <b>January 31, 2024</b>
        </Text>
      </Flex>
      <Divider />
      <Flex justifyContent="space-between" style={{ width: "100%" }}>
        <Flex direction="column" gap={1}>
          <Text style={{ fontWeight: 600 }}>From</Text>
          <Text subdued>Acme, Inc.</Text>
          <Text subdued>7452 Cynthia Pass</Text>
          <Text subdued>Toronto, ON N3Y 4H8</Text>
        </Flex>
        <Flex direction="column" gap={1}>
          <Text style={{ fontWeight: 600 }}>To</Text>
          <Text subdued>Tuple, Inc</Text>
          <Text subdued>8865 Walter Street</Text>
          <Text subdued>New York, NY 10019</Text>
        </Flex>
      </Flex>
      <Flex direction="column" gap={3} style={{ width: "100%" }}>
        <Flex
          justifyContent="space-between"
          style={{ width: "100%", fontWeight: 600 }}
        >
          <Text style={{ fontWeight: 600 }}>Projects</Text>
          <Flex gap={8}>
            <Text style={{ fontWeight: 600, width: 60 }}>Hours</Text>
            <Text style={{ fontWeight: 600, width: 70 }}>Rate</Text>
            <Text style={{ fontWeight: 600, width: 80 }}>Price</Text>
          </Flex>
        </Flex>
        {lineItems.map((item) => (
          <Flex
            key={item.name}
            justifyContent="space-between"
            style={{ width: "100%" }}
          >
            <Flex direction="column">
              <Text style={{ fontWeight: 600 }}>{item.name}</Text>
              <Text subdued size="small">
                {item.description}
              </Text>
            </Flex>
            <Flex gap={8}>
              <Text subdued style={{ width: 60 }}>
                {item.hours.toFixed(1)}
              </Text>
              <Text subdued style={{ width: 70 }}>
                {currency(item.rate)}
              </Text>
              <Text subdued style={{ width: 80 }}>
                {currency(item.hours * item.rate)}
              </Text>
            </Flex>
          </Flex>
        ))}
      </Flex>
    </Card>
  );

  const orders: TableRow<Order>[] = [
    {
      id: "ORD-2024-003",
      customerName: "Ahmed Hassan",
      amount: 89.99,
      items: 2,
      orderDate: "10/02/25",
      paymentType: "Paypal",
      note: "–",
      status: "Pending",
      subComponent: invoicePreview,
    },
    {
      id: "ORD-2024-020",
      customerName: "Anna Kowalski",
      amount: 89.95,
      items: 2,
      orderDate: "10/02/25",
      paymentType: "Credit card",
      note: "Duplicate order",
      status: "Pending",
    },
    {
      id: "ORD-2024-019",
      customerName: "Blue Ocean Seafood",
      amount: 12300.0,
      items: 234,
      orderDate: "10/02/25",
      paymentType: "Check",
      note: "Weekly seafood delivery",
      status: "Pending",
    },
    {
      id: "ORD-2024-005",
      customerName: "Chen Wei",
      amount: null,
      items: 1,
      orderDate: "10/02/25",
      paymentType: "Credit card",
      note: "Customer cancelled before payment",
      status: "Processing",
    },
  ];

  const createColumn = createColumnHelper<Order>();

  const columns = [
    createColumn("id", { header: { label: "Order ID" }, minWidth: 160 }),
    createColumn("customerName", {
      header: { label: "Customer Name" },
      minWidth: 170,
    }),
    createColumn("amount", {
      header: { label: "Amount" },
      align: "end",
      minWidth: 110,
      renderCell: (value: Order["amount"]) =>
        value == null ? "–" : currency(value),
    }),
    createColumn("items", {
      header: { label: "Items" },
      align: "end",
      minWidth: 80,
    }),
    createColumn("orderDate", { header: { label: "Order Date" }, minWidth: 110 }),
    createColumn("paymentType", {
      header: { label: "Payment Type" },
      minWidth: 150,
    }),
    createColumn("note", { header: { label: "Note" }, minWidth: 260 }),
    createColumn("status", {
      header: { label: "Status" },
      minWidth: 120,
      renderCell: (value: Order["status"]) => <Chip label={value} size="small" />,
    }),
  ];

  function App() {
    return (
      <Flex style={{ padding: "2rem", width: "100%" }}>
        <DataTable
          data={orders}
          columns={columns}
          defaultExpandedRowIds={["ORD-2024-003"]}
          style={{ width: "100%" }}
        />
      </Flex>
    );
  }

  export default App;
  ```
</LiveCode>

## Cell Errors and Warnings

A row's `meta.errors` or `meta.warnings`, keyed by column id, marks the corresponding cell with an error or warning treatment and an accessible message — errors take priority when both exist for the same cell.

<LiveCode example="block-tables-errored-warning-cells" screenshot fullWidth>
  ```tsx lines theme={null}
  import { Chip, Flex } from "@servicetitan/anvil2";
  import {
    DataTable,
    createColumnHelper,
    type TableRow,
  } from "@servicetitan/anvil2/beta";

  type Order = {
    id: string;
    customerName: string;
    amount: number | null;
    items: number;
    orderDate: string;
    paymentType: string;
    note: string;
    status: string;
  };

  const orders: TableRow<Order>[] = [
    {
      id: "ORD-2024-003",
      customerName: "Ahmed Hassan",
      amount: 89.99,
      items: 2,
      orderDate: "10/02/25",
      paymentType: "Paypal",
      note: "–",
      status: "Pending",
    },
    {
      id: "ORD-2024-020",
      customerName: "Anna Kowalski",
      amount: 89.95,
      items: 2,
      orderDate: "10/02/25",
      paymentType: "Credit card",
      note: "Duplicate order",
      status: "Pending",
    },
    {
      id: "ORD-2024-019",
      customerName: "Blue Ocean Seafood",
      amount: 12300.0,
      items: 234,
      orderDate: "10/02/25",
      paymentType: "Check",
      note: "Weekly seafood delivery",
      status: "Pending",
      meta: {
        errors: {
          amount: "Amount exceeds the customer's approved credit limit",
        },
      },
    },
    {
      id: "ORD-2024-005",
      customerName: "Chen Wei",
      amount: null,
      items: 1,
      orderDate: "10/02/25",
      paymentType: "Credit card",
      note: "Customer cancelled before payment",
      status: "Processing",
      meta: {
        warnings: {
          orderDate: "Order date is more than 90 days old",
        },
      },
    },
    {
      id: "ORD-2024-011",
      customerName: "David Thompson",
      amount: 750.0,
      items: 6,
      orderDate: "10/02/25",
      paymentType: "Credit card",
      note: "Product out of stock",
      status: "Processing",
      meta: {
        errors: {
          customerName: "Customer name could not be verified",
        },
      },
    },
    {
      id: "ORD-2024-009",
      customerName: "Emily Watson",
      amount: 156.78,
      items: 4,
      orderDate: "10/02/25",
      paymentType: "Paypal",
      note: "–",
      status: "Processing",
    },
    {
      id: "ORD-2024-018",
      customerName: "Fatima Al-Zahra",
      amount: 567.89,
      items: 9,
      orderDate: "10/02/25",
      paymentType: "Paypal",
      note: "Gift wrap requested",
      status: "Processing",
    },
    {
      id: "ORD-2024-006",
      customerName: "Green Valley Farms",
      amount: 8925.0,
      items: 156,
      orderDate: "10/02/25",
      paymentType: "Check",
      note: "–",
      status: "Shipped",
    },
    {
      id: "ORD-2024-015",
      customerName: "Hiroshi Tanaka",
      amount: 4950.0,
      items: 18,
      orderDate: "10/02/25",
      paymentType: "Paypal",
      note: "Electronics bundle",
      status: "Shipped",
    },
    {
      id: "ORD-2024-031",
      customerName: "Ian Wright",
      amount: 220.0,
      items: 3,
      orderDate: "10/02/25",
      paymentType: "Credit card",
      note: "–",
      status: "Shipped",
    },
  ];

  const currency = (value: number) =>
    `$${value.toLocaleString("en-US", {
      minimumFractionDigits: 2,
      maximumFractionDigits: 2,
    })}`;

  const createColumn = createColumnHelper<Order>();

  const columns = [
    createColumn("id", { header: { label: "Order ID" }, minWidth: 140 }),
    createColumn("customerName", {
      header: { label: "Customer Name" },
      minWidth: 170,
    }),
    createColumn("amount", {
      header: { label: "Amount" },
      align: "end",
      minWidth: 110,
      renderCell: (value: Order["amount"]) =>
        value == null ? "–" : currency(value),
    }),
    createColumn("items", {
      header: { label: "Items" },
      align: "end",
      minWidth: 80,
    }),
    createColumn("orderDate", { header: { label: "Order Date" }, minWidth: 110 }),
    createColumn("paymentType", {
      header: { label: "Payment Type" },
      minWidth: 150,
    }),
    createColumn("note", { header: { label: "Note" }, minWidth: 260 }),
    createColumn("status", {
      header: { label: "Status" },
      minWidth: 120,
      renderCell: (value: Order["status"]) => <Chip label={value} size="small" />,
    }),
  ];

  function App() {
    return (
      <Flex style={{ padding: "2rem", width: "100%" }}>
        <DataTable
          data={orders}
          columns={columns}
          pagination={{ rowsPerPage: 10, showCount: true }}
          style={{ width: "100%" }}
        />
      </Flex>
    );
  }

  export default App;
  ```
</LiveCode>

## Row-Level Errors and Warnings

A row's `meta.rowError` or `meta.rowWarning` marks the entire row and adds a status icon in its own column automatically — no column configuration required. Use this variant when the problem belongs to the record as a whole, not a single field.

<LiveCode example="block-tables-row-level-error-warning" screenshot fullWidth>
  ```tsx lines theme={null}
  import { Chip, Flex } from "@servicetitan/anvil2";
  import {
    DataTable,
    createColumnHelper,
    type TableRow,
  } from "@servicetitan/anvil2/beta";

  type Order = {
    id: string;
    customerName: string;
    amount: number | null;
    items: number;
    orderDate: string;
    paymentType: string;
    note: string;
    status: string;
  };

  const orders: TableRow<Order>[] = [
    {
      id: "ORD-2024-003",
      customerName: "Ahmed Hassan",
      amount: 89.99,
      items: 2,
      orderDate: "10/02/25",
      paymentType: "Paypal",
      note: "–",
      status: "Pending",
    },
    {
      id: "ORD-2024-020",
      customerName: "Anna Kowalski",
      amount: 89.95,
      items: 2,
      orderDate: "10/02/25",
      paymentType: "Credit card",
      note: "Duplicate order",
      status: "Pending",
    },
    {
      id: "ORD-2024-019",
      customerName: "Blue Ocean Seafood",
      amount: 12300.0,
      items: 234,
      orderDate: "10/02/25",
      paymentType: "Check",
      note: "Weekly seafood delivery",
      status: "Pending",
      meta: {
        rowError: "This order failed payment validation and needs review",
      },
    },
    {
      id: "ORD-2024-005",
      customerName: "Chen Wei",
      amount: null,
      items: 1,
      orderDate: "10/02/25",
      paymentType: "Credit card",
      note: "Customer cancelled before payment",
      status: "Processing",
    },
    {
      id: "ORD-2024-011",
      customerName: "David Thompson",
      amount: 750.0,
      items: 6,
      orderDate: "10/02/25",
      paymentType: "Credit card",
      note: "Product out of stock",
      status: "Processing",
    },
    {
      id: "ORD-2024-009",
      customerName: "Emily Watson",
      amount: 156.78,
      items: 4,
      orderDate: "10/02/25",
      paymentType: "Paypal",
      note: "–",
      status: "Processing",
      meta: {
        rowWarning: "Shipping address may be incomplete",
      },
    },
    {
      id: "ORD-2024-018",
      customerName: "Fatima Al-Zahra",
      amount: 567.89,
      items: 9,
      orderDate: "10/02/25",
      paymentType: "Paypal",
      note: "Gift wrap requested",
      status: "Processing",
    },
    {
      id: "ORD-2024-006",
      customerName: "Green Valley Farms",
      amount: 8925.0,
      items: 156,
      orderDate: "10/02/25",
      paymentType: "Check",
      note: "–",
      status: "Shipped",
    },
    {
      id: "ORD-2024-015",
      customerName: "Hiroshi Tanaka",
      amount: 4950.0,
      items: 18,
      orderDate: "10/02/25",
      paymentType: "Paypal",
      note: "Electronics bundle",
      status: "Shipped",
    },
    {
      id: "ORD-2024-031",
      customerName: "Ian Wright",
      amount: 220.0,
      items: 3,
      orderDate: "10/02/25",
      paymentType: "Credit card",
      note: "–",
      status: "Shipped",
    },
  ];

  const currency = (value: number) =>
    `$${value.toLocaleString("en-US", {
      minimumFractionDigits: 2,
      maximumFractionDigits: 2,
    })}`;

  const createColumn = createColumnHelper<Order>();

  const columns = [
    createColumn("id", { header: { label: "Order ID" }, minWidth: 140 }),
    createColumn("customerName", {
      header: { label: "Customer Name" },
      minWidth: 170,
    }),
    createColumn("amount", {
      header: { label: "Amount" },
      align: "end",
      minWidth: 110,
      renderCell: (value: Order["amount"]) =>
        value == null ? "–" : currency(value),
    }),
    createColumn("items", {
      header: { label: "Items" },
      align: "end",
      minWidth: 80,
    }),
    createColumn("orderDate", { header: { label: "Order Date" }, minWidth: 110 }),
    createColumn("paymentType", {
      header: { label: "Payment Type" },
      minWidth: 150,
    }),
    createColumn("note", { header: { label: "Note" }, minWidth: 260 }),
    createColumn("status", {
      header: { label: "Status" },
      minWidth: 120,
      renderCell: (value: Order["status"]) => <Chip label={value} size="small" />,
    }),
  ];

  function App() {
    return (
      <Flex style={{ padding: "2rem", width: "100%" }}>
        <DataTable
          data={orders}
          columns={columns}
          pagination={{ rowsPerPage: 10, showCount: true }}
          style={{ width: "100%" }}
        />
      </Flex>
    );
  }

  export default App;
  ```
</LiveCode>
