# Data table

> A table for real data, with sorting, search, row selection, pinned columns, a sticky header and pagination.

Docs: https://hextaui.com/docs/data-table
Markdown: https://hextaui.com/docs/data-table.md

```tsx title="components/examples/data-table/demo.tsx"
"use client"

import { Badge, BadgeDot } from "@/components/ui/badge"
import {
  createDataTableColumns,
  DataTable,
  DataTableColumnHeader,
  DataTableContent,
  DataTablePagination,
  DataTableSearch,
  DataTableSelectAll,
  DataTableSelectRow,
  DataTableToolbar,
  DataTableViewOptions,
  useDataTable,
} from "@/components/ui/data-table"

type Payment = {
  id: string
  email: string
  status: "paid" | "pending" | "failed"
  amount: number
  method: string
  country: string
  date: string
}

const statuses = ["paid", "pending", "failed"] as const
const methods = ["Card", "PayPal", "Bank", "Apple Pay"]
const countries = ["United States", "Germany", "India", "Japan", "Brazil"]
const names = ["olivia", "jackson", "isabella", "william", "sofia", "liam"]

function makePayments(count: number): Payment[] {
  return Array.from({ length: count }, (_, index) => ({
    id: `pay_${String(index + 1).padStart(4, "0")}`,
    email: `${names[index % names.length]}.${Math.floor(index / names.length) + 1}@example.com`,
    status: statuses[(index * 7) % 3],
    amount: Math.round(((index * 7919) % 100000) / 10) / 10 + 5,
    method: methods[index % methods.length],
    country: countries[(index * 3) % countries.length],
    date: `2026-${String((index % 9) + 1).padStart(2, "0")}-${String((index % 27) + 1).padStart(2, "0")}`,
  }))
}

const payments = makePayments(42)

const currency = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD",
})

const statusVariant = {
  paid: "success",
  pending: "warning",
  failed: "destructive",
} as const

const helper = createDataTableColumns<Payment>()

const columns = helper.columns([
  helper.display({
    id: "select",
    header: ({ table }) => <DataTableSelectAll table={table} />,
    cell: ({ row }) => <DataTableSelectRow row={row} />,
    enableSorting: false,
    enableHiding: false,
  }),
  helper.accessor("email", {
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="Email" />
    ),
  }),
  helper.accessor("status", {
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="Status" />
    ),
    cell: ({ getValue }) => {
      const status = getValue()
      return (
        <Badge variant={statusVariant[status]}>
          <BadgeDot />
          <span className="capitalize">{status}</span>
        </Badge>
      )
    },
  }),
  helper.accessor("method", { header: "Method" }),
  helper.accessor("country", { header: "Country" }),
  helper.accessor("date", {
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="Date" />
    ),
  }),
  helper.accessor("amount", {
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="Amount" />
    ),
    cell: ({ getValue }) => currency.format(getValue()),
    meta: { align: "end" },
  }),
])

export function DataTableDemo() {
  const table = useDataTable({
    data: payments,
    columns,
    getRowId: (row) => row.id,
  })

  return (
    <DataTable table={table}>
      <DataTableToolbar>
        <DataTableSearch placeholder="Search payments…" />
        <DataTableViewOptions />
      </DataTableToolbar>
      <DataTableContent emptyMessage="No payments match your search." />
      <DataTablePagination />
    </DataTable>
  )
}
```

## Installation

### CLI

```bash
npx shadcn@latest add https://hextaui.com/r/data-table.json
```

This adds the component, the HextaUI theme tokens and any HextaUI components it depends on.

### Manual

Add the HextaUI theme tokens (https://hextaui.com/docs/installation#theme) to your global CSS if you haven't yet, then install the dependencies.

```bash
pnpm add @base-ui/react @tabler/icons-react @tanstack/react-table class-variance-authority cn
```

Copy and paste the following code into your project.

```tsx title="components/ui/data-table.tsx"
"use client"

import * as React from "react"
import { Menu as MenuPrimitive } from "@base-ui/react/menu"
import {
  IconAdjustmentsHorizontal,
  IconArrowDown,
  IconArrowUp,
  IconCheck,
  IconChevronDown,
  IconChevronLeft,
  IconChevronRight,
  IconChevronsLeft,
  IconChevronsRight,
  IconSearch,
  IconSelector,
  IconX,
} from "@tabler/icons-react"
import {
  columnFilteringFeature,
  columnVisibilityFeature,
  createFilteredRowModel,
  createPaginatedRowModel,
  createSortedRowModel,
  createTableHook,
  createTableHookContexts,
  filterFn_includesString,
  globalFilteringFeature,
  rowPaginationFeature,
  rowSelectionFeature,
  rowSortingFeature,
  sortFn_alphanumeric,
  sortFn_basic,
  sortFn_datetime,
  sortFn_text,
  Subscribe,
  tableFeatures,
  type Column,
  type Row,
  type RowData,
  type RowSelectionState,
  type Table as CoreTable,
} from "@tanstack/react-table"
import { cn } from "cn"

import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import { Skeleton } from "@/components/ui/skeleton"
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table"

const dataTableFeatures = tableFeatures({
  rowSortingFeature,
  sortedRowModel: createSortedRowModel(),
  sortFns: {
    alphanumeric: sortFn_alphanumeric,
    text: sortFn_text,
    datetime: sortFn_datetime,
    basic: sortFn_basic,
  },
  columnFilteringFeature,
  globalFilteringFeature,
  filteredRowModel: createFilteredRowModel(),
  filterFns: { includesString: filterFn_includesString },
  rowPaginationFeature,
  paginatedRowModel: createPaginatedRowModel(),
  rowSelectionFeature,
  columnVisibilityFeature,
})

type DataTableFeatures = typeof dataTableFeatures

const dataTableContexts = createTableHookContexts<DataTableFeatures>()

const dataTableHook = createTableHook({
  features: dataTableFeatures,
  ...dataTableContexts,
  globalFilterFn: "includesString",
  getColumnCanGlobalFilter: (column) => column.id !== "select",
})

type UseDataTableOptions<TData extends RowData> = Parameters<
  typeof dataTableHook.useAppTable<TData>
>[0]

type DataTableInstance<TData extends RowData = RowData> = ReturnType<
  typeof dataTableHook.useAppTable<TData>
>

type AnyColumn = Column<DataTableFeatures, RowData, unknown>
type AnyRow = Row<DataTableFeatures, RowData>
type AnyCoreTable = CoreTable<DataTableFeatures, RowData>

function useDataTable<TData extends RowData>(
  options: UseDataTableOptions<TData>
) {
  return dataTableHook.useAppTable<TData>({
    ...options,
    initialState: {
      pagination: { pageIndex: 0, pageSize: 10 },
      ...options.initialState,
    },
  })
}

const createDataTableColumns = dataTableHook.createAppColumnHelper

function useDataTableContext() {
  return dataTableHook.useTableContext()
}

function columnLabel(column: AnyColumn) {
  const header = column.columnDef.header
  if (typeof header === "string") {
    return header
  }
  return column.id.charAt(0).toUpperCase() + column.id.slice(1)
}

function DataTableAnnouncer() {
  const table = useDataTableContext()
  const sorting = table.state.sorting
  const search = table.state.globalFilter as string | undefined
  const count = table.getFilteredRowModel().rows.length
  const sortKey = JSON.stringify(sorting)
  const [message, setMessage] = React.useState("")
  const [previousSort, setPreviousSort] = React.useState(sortKey)

  const first = sorting[0]
  const sortedColumn = first ? table.getColumn(first.id) : undefined
  const sortMessage = first
    ? `Sorted by ${sortedColumn ? columnLabel(sortedColumn as AnyColumn) : first.id}, ${first.desc ? "descending" : "ascending"}`
    : "Sorting removed"

  if (sortKey !== previousSort) {
    setPreviousSort(sortKey)
    setMessage(sortMessage)
  }

  React.useEffect(() => {
    if (!search) {
      return
    }
    const timer = setTimeout(
      () => setMessage(count === 1 ? "1 result" : `${count} results`),
      500
    )
    return () => clearTimeout(timer)
  }, [count, search])

  return (
    <span
      data-slot="data-table-announcer"
      role="status"
      aria-live="polite"
      aria-atomic
      className="sr-only"
    >
      {message}
    </span>
  )
}

type DataTableProps<TData extends RowData> = React.ComponentProps<"div"> & {
  table: DataTableInstance<TData>
}

function DataTable<TData extends RowData>({
  table,
  className,
  children,
  ...props
}: DataTableProps<TData>) {
  return (
    <table.AppTable>
      <div
        data-slot="data-table"
        className={cn("flex w-full min-w-0 flex-col gap-3", className)}
        {...props}
      >
        {children}
        <DataTableAnnouncer />
      </div>
    </table.AppTable>
  )
}

function DataTableToolbar({
  className,
  ...props
}: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="data-table-toolbar"
      className={cn("flex flex-wrap items-center gap-2", className)}
      {...props}
    />
  )
}

const fieldClassName =
  "h-8 w-full min-w-0 rounded-md bg-background text-sm inset-ring-(length:--hairline) forced-colors:border inset-ring-input transition-[box-shadow] duration-150 ease-out-quint outline-none focus-visible:outline-hidden placeholder:text-muted-foreground focus-visible:ring-3 focus-visible:ring-focus-ring focus-visible:inset-ring-ring pointer-coarse:h-11 pointer-coarse:text-[max(16px,1rem)] dark:bg-input/30"

type DataTableSearchProps = Omit<
  React.ComponentProps<"input">,
  "value" | "defaultValue" | "onChange"
> & {
  clearLabel?: string
}

function DataTableSearch({
  className,
  placeholder = "Search…",
  clearLabel = "Clear search",
  onKeyDown,
  "aria-label": ariaLabel = "Search table",
  ...props
}: DataTableSearchProps) {
  const table = useDataTableContext()
  const value = (table.state.globalFilter as string | undefined) ?? ""
  const inputRef = React.useRef<HTMLInputElement>(null)

  const setSearch = (next: string) => {
    table.setGlobalFilter(next)
    table.setPageIndex(0)
  }

  return (
    <div
      data-slot="data-table-search"
      className={cn(
        "relative flex min-w-0 flex-1 items-center sm:max-w-xs",
        className
      )}
    >
      <IconSearch
        aria-hidden
        className="pointer-events-none absolute start-2.5 size-4 text-muted-foreground"
      />
      <input
        ref={inputRef}
        type="search"
        value={value}
        placeholder={placeholder}
        aria-label={ariaLabel}
        onChange={(event) => setSearch(event.target.value)}
        onKeyDown={(event) => {
          onKeyDown?.(event)
          if (
            !event.defaultPrevented &&
            event.key === "Escape" &&
            value !== ""
          ) {
            event.preventDefault()
            setSearch("")
          }
        }}
        className={cn(
          fieldClassName,
          "ps-8 pe-8 [&::-webkit-search-cancel-button]:hidden"
        )}
        {...props}
      />
      <Button
        type="button"
        variant="ghost"
        size="icon-xs"
        tabIndex={-1}
        aria-label={clearLabel}
        aria-hidden={value === "" || undefined}
        onClick={() => {
          setSearch("")
          inputRef.current?.focus()
        }}
        className={cn(
          "absolute end-1 rounded-full text-muted-foreground transition-[opacity,scale,color,background-color] duration-150 hover:text-foreground",
          value === ""
            ? "pointer-events-none opacity-0 motion-safe:scale-75"
            : "opacity-100"
        )}
      >
        <IconX />
      </Button>
    </div>
  )
}

type DataTableColumnHeaderProps<
  TData extends RowData,
  TValue,
> = React.ComponentProps<"div"> & {
  column: Column<DataTableFeatures, TData, TValue>
  title: string
}

function DataTableColumnHeader<TData extends RowData, TValue>({
  column,
  title,
  className,
  ...props
}: DataTableColumnHeaderProps<TData, TValue>) {
  const target = column as unknown as AnyColumn

  if (!target.getCanSort()) {
    return (
      <div
        data-slot="data-table-column-header"
        className={className}
        {...props}
      >
        {title}
      </div>
    )
  }

  return (
    <Subscribe
      source={target.table.store}
      selector={(state) =>
        state.sorting.find((entry) => entry.id === target.id)?.desc
      }
    >
      {(desc) => (
        <div
          data-slot="data-table-column-header"
          className={cn(
            "flex in-data-[align=center]:justify-center in-data-[align=end]:justify-end",
            className
          )}
          {...props}
        >
          <Button
            type="button"
            variant="ghost"
            size="sm"
            onClick={target.getToggleSortingHandler()}
            className="-mx-2.5 h-8 gap-1.5 px-2.5 font-medium text-muted-foreground hover:text-foreground in-data-[align=end]:flex-row-reverse data-sorted:text-foreground"
            data-sorted={desc === undefined ? undefined : ""}
          >
            {title}
            {desc === undefined ? (
              <IconSelector aria-hidden className="size-3.5 opacity-60" />
            ) : desc ? (
              <IconArrowDown aria-hidden className="size-3.5" />
            ) : (
              <IconArrowUp aria-hidden className="size-3.5" />
            )}
          </Button>
        </div>
      )}
    </Subscribe>
  )
}

function DataTableSelectAll<TData extends RowData>({
  table,
  "aria-label": ariaLabel = "Select all rows on this page",
}: {
  table: CoreTable<DataTableFeatures, TData>
  "aria-label"?: string
}) {
  const target = table as unknown as AnyCoreTable

  return (
    <Subscribe
      source={target.store}
      selector={(state) => [
        state.rowSelection,
        state.pagination,
        state.globalFilter,
      ]}
    >
      {() => {
        const all = target.getIsAllPageRowsSelected()
        const some = target.getIsSomePageRowsSelected()
        return (
          <Checkbox
            aria-label={ariaLabel}
            className="z-1"
            checked={all}
            indeterminate={!all && some}
            onCheckedChange={(checked) =>
              target.toggleAllPageRowsSelected(checked)
            }
          />
        )
      }}
    </Subscribe>
  )
}

function DataTableSelectRow<TData extends RowData>({
  row,
  "aria-label": ariaLabel = "Select row",
}: {
  row: Row<DataTableFeatures, TData>
  "aria-label"?: string
}) {
  const target = row as unknown as AnyRow

  return (
    <Subscribe
      source={target.table.store}
      selector={(state) => Boolean(state.rowSelection[target.id])}
    >
      {() => (
        <Checkbox
          aria-label={ariaLabel}
          className="z-1"
          checked={target.getIsSelected()}
          disabled={!target.getCanSelect()}
          onCheckedChange={(checked, details) => {
            const event = details.event as Event & { shiftKey?: boolean }
            target.getToggleSelectedHandler()({
              target: { checked },
              shiftKey: Boolean(event?.shiftKey),
              nativeEvent: event,
            })
          }}
        />
      )}
    </Subscribe>
  )
}

type DataTableContentProps = {
  className?: string
  emptyMessage?: React.ReactNode
  loading?: boolean
  loadingRows?: number
  pinStart?: string[]
  stickyHeader?: boolean
  containerClassName?: string
  swipeSelect?: boolean
}

function scrollParentOf(element: HTMLElement | null) {
  return element && element.scrollHeight > element.clientHeight + 1
    ? element
    : null
}

function useSwipeSelect(
  table: AnyCoreTable,
  containerRef: React.RefObject<HTMLDivElement | null>,
  enabled: boolean
) {
  return (event: React.PointerEvent<HTMLTableSectionElement>) => {
    if (
      !enabled ||
      !event.shiftKey ||
      event.button !== 0 ||
      event.pointerType === "touch"
    ) {
      return
    }
    const target = event.target as Element
    const box = target.closest("[data-slot=checkbox]")
    const startRow = box?.closest<HTMLElement>("tr[data-row-id]")
    if (!box || !startRow) {
      return
    }
    const ids = table.getRowModel().rows.map((row) => row.id)
    const anchor = ids.indexOf(startRow.dataset.rowId ?? "")
    const anchorRow = anchor === -1 ? undefined : table.getRow(ids[anchor])
    if (!anchorRow || !anchorRow.getCanSelect()) {
      return
    }
    event.preventDefault()

    const value = !anchorRow.getIsSelected()
    const snapshot: RowSelectionState = { ...table.store.state.rowSelection }
    const body = event.currentTarget
    const container = containerRef.current
    let current = anchor
    let dragged = false
    let pointer = { x: event.clientX, y: event.clientY }
    let frame = 0

    const apply = (index: number) => {
      const next: RowSelectionState = { ...snapshot }
      const from = Math.min(anchor, index)
      const to = Math.max(anchor, index)
      for (let position = from; position <= to; position++) {
        const row = table.getRow(ids[position])
        if (!row?.getCanSelect()) {
          continue
        }
        if (value) {
          next[ids[position]] = true
        } else {
          delete next[ids[position]]
        }
      }
      table.setRowSelection(next)
    }

    const visibleBounds = () => {
      const scroller = scrollParentOf(container)
      const bounds = scroller
        ? scroller.getBoundingClientRect()
        : { top: 0, bottom: window.innerHeight }
      const head = scroller?.querySelector("thead")
      const top = head
        ? Math.max(bounds.top, head.getBoundingClientRect().bottom)
        : bounds.top
      return { top, bottom: bounds.bottom }
    }

    const hitTest = () => {
      const { top, bottom } = visibleBounds()
      const rows = body.getBoundingClientRect()
      const y = Math.min(Math.max(pointer.y, top + 6), bottom - 6)
      const x = Math.min(Math.max(pointer.x, rows.left + 1), rows.right - 1)
      const hit = document
        .elementFromPoint(x, y)
        ?.closest<HTMLElement>("tr[data-row-id]")
      let index = hit ? ids.indexOf(hit.dataset.rowId ?? "") : -1
      if (index === -1) {
        if (pointer.y < rows.top) {
          index = 0
        } else if (pointer.y > rows.bottom) {
          index = ids.length - 1
        } else {
          return
        }
      }
      if (index === current) {
        return
      }
      current = index
      if (index !== anchor) {
        dragged = true
      }
      if (dragged) {
        apply(index)
      }
    }

    const autoScroll = () => {
      frame = 0
      const scroller = scrollParentOf(container)
      const { top, bottom } = visibleBounds()
      const edge = 40
      let delta = 0
      if (pointer.y < top + edge) {
        delta = -Math.ceil((top + edge - pointer.y) / 4)
      } else if (pointer.y > bottom - edge) {
        delta = Math.ceil((pointer.y - (bottom - edge)) / 4)
      }
      if (delta === 0) {
        return
      }
      if (scroller) {
        scroller.scrollTop += delta
      } else {
        window.scrollBy(0, delta)
      }
      hitTest()
      frame = requestAnimationFrame(autoScroll)
    }

    const onMove = (moveEvent: PointerEvent) => {
      pointer = { x: moveEvent.clientX, y: moveEvent.clientY }
      hitTest()
      if (!frame) {
        frame = requestAnimationFrame(autoScroll)
      }
    }

    const stopClick = (clickEvent: MouseEvent) => {
      clickEvent.preventDefault()
      clickEvent.stopPropagation()
    }

    const onUp = () => {
      window.removeEventListener("pointermove", onMove)
      window.removeEventListener("pointerup", onUp)
      window.removeEventListener("pointercancel", onUp)
      cancelAnimationFrame(frame)
      container?.removeAttribute("data-swipe-selecting")
      if (dragged) {
        window.addEventListener("click", stopClick, {
          capture: true,
          once: true,
        })
        setTimeout(
          () =>
            window.removeEventListener("click", stopClick, { capture: true }),
          0
        )
      }
    }

    container?.setAttribute("data-swipe-selecting", "")
    window.addEventListener("pointermove", onMove)
    window.addEventListener("pointerup", onUp)
    window.addEventListener("pointercancel", onUp)
  }
}

function DataTableContent({
  className,
  emptyMessage = "No results.",
  loading = false,
  loadingRows = 5,
  pinStart,
  stickyHeader = false,
  containerClassName,
  swipeSelect = true,
}: DataTableContentProps) {
  const table = useDataTableContext()
  const containerRef = React.useRef<HTMLDivElement>(null)
  const onSwipeStart = useSwipeSelect(
    table as unknown as AnyCoreTable,
    containerRef,
    swipeSelect
  )
  const leafColumns = table.getVisibleLeafColumns()
  const firstData = leafColumns.find((column) => column.id !== "select")
  const pinned = (
    pinStart ?? ["select", ...(firstData ? [firstData.id] : [])]
  ).filter((id) => leafColumns.some((column) => column.id === id))
  const rows = table.getRowModel().rows
  const pageCount = table.getPageCount()
  const pageIndex = table.state.pagination.pageIndex

  React.useEffect(() => {
    if (pageCount > 0 && pageIndex > pageCount - 1) {
      table.setPageIndex(pageCount - 1)
    }
  }, [pageCount, pageIndex, table])

  const alignOf = (column: { columnDef: { meta?: unknown } }) =>
    (
      column.columnDef.meta as
        { align?: "start" | "center" | "end" } | undefined
    )?.align

  const pinProps = (columnId: string) => {
    const index = pinned.indexOf(columnId)
    if (index === -1) {
      return { "data-pin-column": undefined }
    }
    return {
      pinned: "start" as const,
      pinnedEdge: index === pinned.length - 1,
      "data-pin-column": columnId,
    }
  }

  React.useLayoutEffect(() => {
    const container = containerRef.current
    if (!container) {
      return
    }
    const apply = () => {
      let offset = 0
      for (const id of pinned) {
        const cells = container.querySelectorAll<HTMLElement>(
          `[data-pin-column="${CSS.escape(id)}"]`
        )
        cells.forEach((cell) =>
          cell.style.setProperty("--pin-offset", `${offset}px`)
        )
        const head = container.querySelector<HTMLElement>(
          `th[data-pin-column="${CSS.escape(id)}"]`
        )
        offset += head?.getBoundingClientRect().width ?? 0
      }
    }
    apply()
    const observer =
      typeof ResizeObserver === "function" ? new ResizeObserver(apply) : null
    const head = container.querySelector("thead")
    if (head) {
      observer?.observe(head)
    }
    return () => observer?.disconnect()
  })

  return (
    <Table
      containerRef={containerRef}
      stickyHeader={stickyHeader}
      scrollFade={false}
      containerClassName={cn(
        "rounded-lg border data-swipe-selecting:cursor-default data-swipe-selecting:select-none",
        containerClassName
      )}
      className={className}
      aria-busy={loading || undefined}
    >
      <TableHeader>
        {table.getHeaderGroups().map((group) => (
          <TableRow key={group.id}>
            {group.headers.map((header) => {
              const sorted = header.column.getIsSorted()
              return (
                <TableHead
                  key={header.id}
                  colSpan={header.colSpan}
                  align={alignOf(header.column)}
                  aria-sort={
                    sorted === "asc"
                      ? "ascending"
                      : sorted === "desc"
                        ? "descending"
                        : header.column.getCanSort()
                          ? "none"
                          : undefined
                  }
                  {...pinProps(header.column.id)}
                >
                  {header.isPlaceholder ? null : (
                    <table.FlexRender header={header} />
                  )}
                </TableHead>
              )
            })}
          </TableRow>
        ))}
      </TableHeader>
      <TableBody onPointerDown={onSwipeStart}>
        {loading ? (
          Array.from({ length: loadingRows }, (_, index) => (
            <TableRow
              key={`loading-${index}`}
              data-slot="data-table-loading-row"
            >
              {leafColumns.map((column) => (
                <TableCell key={column.id} {...pinProps(column.id)}>
                  <Skeleton
                    className={
                      column.id === "select"
                        ? "size-4 rounded-[calc(var(--radius-sm)*0.5)]"
                        : "h-4 w-full max-w-32"
                    }
                  />
                </TableCell>
              ))}
            </TableRow>
          ))
        ) : rows.length === 0 ? (
          <TableRow data-slot="data-table-empty">
            <TableCell
              colSpan={Math.max(1, leafColumns.length)}
              className="h-24 text-center whitespace-normal text-muted-foreground"
            >
              {emptyMessage}
            </TableCell>
          </TableRow>
        ) : (
          rows.map((row) => (
            <TableRow
              key={row.id}
              data-row-id={row.id}
              data-state={row.getIsSelected() ? "selected" : undefined}
            >
              {row.getVisibleCells().map((cell) => (
                <TableCell
                  key={cell.id}
                  align={alignOf(cell.column)}
                  {...pinProps(cell.column.id)}
                >
                  <table.FlexRender cell={cell} />
                </TableCell>
              ))}
            </TableRow>
          ))
        )}
      </TableBody>
    </Table>
  )
}

const menuPopupClassName =
  "relative max-h-(--available-height) max-w-[min(var(--available-width),20rem)] min-w-44 origin-(--transform-origin) overflow-x-hidden overflow-y-auto overscroll-none rounded-lg bg-popover p-1 text-popover-foreground shadow-md ring-(length:--hairline) forced-colors:border ring-foreground/10 transition-[opacity,scale] duration-150 ease-out-quint outline-none focus-visible:outline-hidden data-ending-style:opacity-0 data-starting-style:opacity-0 data-instant:transition-none motion-safe:data-starting-style:scale-96 motion-reduce:transition-opacity"

const menuItemClassName =
  "relative flex min-h-8 w-full min-w-0 cursor-default items-center gap-2 rounded-[max(calc(var(--radius-sm)*0.5),calc(var(--radius-lg)-0.25rem))] py-1.5 ps-8 pe-2 text-start text-sm outline-none focus-visible:outline-hidden select-none data-highlighted:bg-accent forced-colors:data-highlighted:outline-2 forced-colors:data-highlighted:-outline-offset-2 forced-colors:data-highlighted:outline-solid data-highlighted:text-accent-foreground pointer-coarse:min-h-11"

type DataTableViewOptionsProps = {
  label?: string
  groupLabel?: string
  getLabel?: (column: AnyColumn) => string
}

function DataTableViewOptions({
  label = "View",
  groupLabel = "Toggle columns",
  getLabel = columnLabel,
}: DataTableViewOptionsProps) {
  const table = useDataTableContext()
  const columns = table
    .getAllLeafColumns()
    .filter((column) => column.getCanHide()) as AnyColumn[]

  return (
    <MenuPrimitive.Root>
      <MenuPrimitive.Trigger
        render={
          <Button variant="outline" size="sm" className="ms-auto shrink-0" />
        }
      >
        <IconAdjustmentsHorizontal />
        {label}
        <IconChevronDown className="text-muted-foreground" />
      </MenuPrimitive.Trigger>
      <MenuPrimitive.Portal>
        <MenuPrimitive.Positioner
          align="end"
          sideOffset={6}
          className="isolate z-50 outline-none focus-visible:outline-hidden"
        >
          <MenuPrimitive.Popup
            data-slot="data-table-view-options"
            className={menuPopupClassName}
          >
            <MenuPrimitive.Group>
              <MenuPrimitive.GroupLabel className="px-2 pt-1.5 pb-1 text-xs font-medium text-muted-foreground">
                {groupLabel}
              </MenuPrimitive.GroupLabel>
              {columns.map((column) => (
                <MenuPrimitive.CheckboxItem
                  key={column.id}
                  checked={column.getIsVisible()}
                  onCheckedChange={(checked) =>
                    column.toggleVisibility(checked)
                  }
                  closeOnClick={false}
                  className={menuItemClassName}
                >
                  <span className="pointer-events-none absolute start-2 flex size-4 items-center justify-center">
                    <MenuPrimitive.CheckboxItemIndicator className="flex items-center justify-center">
                      <IconCheck className="size-4" />
                    </MenuPrimitive.CheckboxItemIndicator>
                  </span>
                  <span className="min-w-0 truncate">{getLabel(column)}</span>
                </MenuPrimitive.CheckboxItem>
              ))}
            </MenuPrimitive.Group>
          </MenuPrimitive.Popup>
        </MenuPrimitive.Positioner>
      </MenuPrimitive.Portal>
    </MenuPrimitive.Root>
  )
}

type DataTablePaginationLabels = {
  rowsPerPage: string
  firstPage: string
  previousPage: string
  nextPage: string
  lastPage: string
}

type DataTablePaginationProps = React.ComponentProps<"div"> & {
  pageSizes?: number[]
  showSelection?: boolean
  labels?: Partial<DataTablePaginationLabels>
  formatSelection?: (selected: number, total: number) => React.ReactNode
  formatRows?: (total: number) => React.ReactNode
  formatPage?: (page: number, pageCount: number) => React.ReactNode
}

const numberFormat = new Intl.NumberFormat("en-US")

const defaultPaginationLabels: DataTablePaginationLabels = {
  rowsPerPage: "Rows per page",
  firstPage: "First page",
  previousPage: "Previous page",
  nextPage: "Next page",
  lastPage: "Last page",
}

function defaultFormatSelection(selected: number, total: number) {
  return `${numberFormat.format(selected)} of ${numberFormat.format(total)} row${total === 1 ? "" : "s"} selected`
}

function defaultFormatRows(total: number) {
  return `${numberFormat.format(total)} row${total === 1 ? "" : "s"}`
}

function defaultFormatPage(page: number, pageCount: number) {
  return `Page ${numberFormat.format(page)} of ${numberFormat.format(pageCount)}`
}

function DataTablePagination({
  className,
  pageSizes = [10, 20, 50, 100],
  showSelection = true,
  labels: labelOverrides,
  formatSelection = defaultFormatSelection,
  formatRows = defaultFormatRows,
  formatPage = defaultFormatPage,
  ...props
}: DataTablePaginationProps) {
  const labels = { ...defaultPaginationLabels, ...labelOverrides }
  const table = useDataTableContext()
  const { pageIndex, pageSize } = table.state.pagination
  const pageCount = Math.max(1, table.getPageCount())
  const selected = Object.keys(table.state.rowSelection).length
  const total = table.getFilteredRowModel().rows.length

  return (
    <div
      data-slot="data-table-pagination"
      className={cn(
        "flex flex-wrap items-center justify-between gap-x-6 gap-y-2 text-sm text-muted-foreground",
        className
      )}
      {...props}
    >
      <div className="min-w-0 tabular-nums [unicode-bidi:plaintext]">
        {showSelection ? formatSelection(selected, total) : formatRows(total)}
      </div>
      <div className="flex flex-wrap items-center gap-x-6 gap-y-2">
        <label className="flex items-center gap-2">
          <span className="max-sm:sr-only">{labels.rowsPerPage}</span>
          <span className="relative flex items-center">
            <select
              value={pageSize}
              onChange={(event) =>
                table.setPageSize(Number(event.target.value))
              }
              className={cn(
                fieldClassName,
                "w-auto cursor-pointer appearance-none ps-2.5 pe-7 text-foreground tabular-nums"
              )}
            >
              {pageSizes.map((size) => (
                <option key={size} value={size}>
                  {size}
                </option>
              ))}
            </select>
            <IconChevronDown
              aria-hidden
              className="pointer-events-none absolute end-2 size-3.5 text-muted-foreground"
            />
          </span>
        </label>
        <div
          className="text-foreground tabular-nums [unicode-bidi:plaintext]"
          aria-live="polite"
        >
          {formatPage(pageIndex + 1, pageCount)}
        </div>
        <div className="flex items-center gap-1">
          <Button
            variant="outline"
            size="icon-sm"
            className="max-sm:hidden"
            aria-label={labels.firstPage}
            disabled={!table.getCanPreviousPage()}
            onClick={() => table.firstPage()}
          >
            <IconChevronsLeft className="rtl:-scale-x-100" />
          </Button>
          <Button
            variant="outline"
            size="icon-sm"
            aria-label={labels.previousPage}
            disabled={!table.getCanPreviousPage()}
            onClick={() => table.previousPage()}
          >
            <IconChevronLeft className="rtl:-scale-x-100" />
          </Button>
          <Button
            variant="outline"
            size="icon-sm"
            aria-label={labels.nextPage}
            disabled={!table.getCanNextPage()}
            onClick={() => table.nextPage()}
          >
            <IconChevronRight className="rtl:-scale-x-100" />
          </Button>
          <Button
            variant="outline"
            size="icon-sm"
            className="max-sm:hidden"
            aria-label={labels.lastPage}
            disabled={!table.getCanNextPage()}
            onClick={() => table.lastPage()}
          >
            <IconChevronsRight className="rtl:-scale-x-100" />
          </Button>
        </div>
      </div>
    </div>
  )
}

export {
  DataTable,
  DataTableToolbar,
  DataTableSearch,
  DataTableViewOptions,
  DataTableContent,
  DataTableColumnHeader,
  DataTableSelectAll,
  DataTableSelectRow,
  DataTablePagination,
  createDataTableColumns,
  dataTableFeatures,
  useDataTable,
  useDataTableContext,
}
export type {
  DataTableFeatures,
  DataTableInstance,
  DataTablePaginationLabels,
  UseDataTableOptions,
}
```

```tsx title="components/ui/table.tsx"
"use client"

import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "cn"

function mergeRefs<T>(
  ...refs: (React.Ref<T> | ((node: T | null) => void) | undefined)[]
) {
  return (node: T | null) => {
    for (const ref of refs) {
      if (typeof ref === "function") {
        ref(node)
      } else if (ref) {
        ref.current = node
      }
    }
  }
}

function useScrollEdges(container: HTMLDivElement | null) {
  React.useLayoutEffect(() => {
    if (!container) {
      return
    }
    const update = () => {
      const offset = Math.abs(container.scrollLeft)
      const max = container.scrollWidth - container.clientWidth
      const overflowing =
        max > 0.5 || container.scrollHeight - container.clientHeight > 0.5
      container.toggleAttribute("data-scrolled-start", offset > 0.5)
      container.toggleAttribute("data-scrolled-end", offset < max - 0.5)
      container.toggleAttribute("data-scrolled-top", container.scrollTop > 0.5)
      container.toggleAttribute("data-overflowing", overflowing)
      if (overflowing) {
        container.tabIndex = 0
      } else {
        container.removeAttribute("tabindex")
      }
    }
    update()
    container.addEventListener("scroll", update, { passive: true })
    const observer =
      typeof ResizeObserver === "function" ? new ResizeObserver(update) : null
    observer?.observe(container)
    const table = container.firstElementChild
    if (table) {
      observer?.observe(table)
    }
    return () => {
      container.removeEventListener("scroll", update)
      observer?.disconnect()
    }
  }, [container])
}

const tableVariants = cva(
  "group/table relative w-full min-w-0 [--table-bg:var(--background)] [--table-hover:color-mix(in_oklab,var(--muted)_60%,var(--table-bg))] in-data-[slot=card]:[--table-bg:var(--card)] in-data-[slot=popover-content]:[--table-bg:var(--popover)]",
  {
    variants: {
      variant: {
        default: "",
        surface:
          "isolate overflow-clip rounded-xl bg-(--table-bg) ring-(length:--hairline) ring-border [--table-bg:var(--card)] [--table-head-bg:color-mix(in_oklab,var(--muted)_85%,var(--table-bg))] in-data-[slot=card]:[--table-bg:var(--card)] forced-colors:border",
      },
      size: {
        sm: "[--table-cell-px:--spacing(2.5)] [--table-cell-py:--spacing(1.5)] [--table-head-h:--spacing(8)]",
        default:
          "[--table-cell-px:--spacing(3)] [--table-cell-py:--spacing(2.5)] [--table-head-h:--spacing(10)]",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
)

type TableVariant = NonNullable<VariantProps<typeof tableVariants>["variant"]>
type TableSize = NonNullable<VariantProps<typeof tableVariants>["size"]>

type TableProps = React.ComponentProps<"table"> & {
  variant?: TableVariant
  size?: TableSize
  wrap?: boolean
  scrollFade?: boolean
  stickyHeader?: boolean
  containerClassName?: string
  containerRef?: React.Ref<HTMLDivElement>
}

function Table({
  className,
  variant = "default",
  size = "default",
  wrap = false,
  scrollFade = true,
  stickyHeader = false,
  containerClassName,
  containerRef,
  ...props
}: TableProps) {
  const [container, setContainer] = React.useState<HTMLDivElement | null>(null)
  useScrollEdges(container)

  const setRef = React.useMemo(
    () => mergeRefs(setContainer, containerRef),
    [containerRef]
  )

  return (
    <div
      data-slot="table-frame"
      data-variant={variant}
      data-size={size}
      data-wrap={wrap ? "" : undefined}
      className={tableVariants({ variant, size })}
    >
      <div
        ref={setRef}
        data-slot="table-container"
        data-sticky-header={stickyHeader ? "" : undefined}
        className={cn(
          "relative w-full overflow-x-auto overscroll-x-none rounded-[inherit] outline-none [--fade-end:0px] [--fade-start:0px] focus-visible:ring-3 focus-visible:ring-focus-ring focus-visible:outline-hidden focus-visible:ring-inset data-sticky-header:overflow-y-auto",
          scrollFade &&
            "not-has-[[data-pinned]]:mask-[linear-gradient(to_right,transparent,#000_var(--fade-start),#000_calc(100%-var(--fade-end)),transparent)] data-scrolled-end:[--fade-end:--spacing(8)] data-scrolled-start:[--fade-start:--spacing(8)] rtl:not-has-[[data-pinned]]:mask-[linear-gradient(to_left,transparent,#000_var(--fade-start),#000_calc(100%-var(--fade-end)),transparent)]",
          containerClassName
        )}
      >
        <table
          data-slot="table"
          className={cn(
            "w-full caption-bottom border-separate border-spacing-0 text-sm",
            className
          )}
          {...props}
        />
      </div>
    </div>
  )
}

function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
  return (
    <thead
      data-slot="table-header"
      className={cn(
        "[&_tr]:bg-transparent [&_tr]:hover:bg-transparent",
        className
      )}
      {...props}
    />
  )
}

function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
  return (
    <tbody
      data-slot="table-body"
      className={cn(
        "[&>tr:last-child>*]:border-b-0 [&>tr:last-child>*]:border-b-transparent",
        className
      )}
      {...props}
    />
  )
}

function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
  return (
    <tfoot
      data-slot="table-footer"
      className={cn(
        "font-medium [&>tr>*]:border-t [&>tr>*]:border-b-0 [&>tr>*]:bg-(--table-head-bg,var(--table-bg))",
        className
      )}
      {...props}
    />
  )
}

function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
  return (
    <tr
      data-slot="table-row"
      className={cn(
        "group/table-row bg-(--table-bg) hover:bg-(--table-hover) data-[state=selected]:bg-muted",
        className
      )}
      {...props}
    />
  )
}

const cellBase =
  "border-b align-middle whitespace-nowrap group-data-wrap/table:align-top group-data-wrap/table:whitespace-normal group-data-wrap/table:text-pretty group-data-wrap/table:wrap-break-word data-[align=center]:text-center data-[align=end]:text-end data-[align=end]:tabular-nums data-pinned:sticky data-pinned:z-1 data-pinned:has-[:focus-visible]:z-4 data-pinned:has-[[data-slot=checkbox]]:z-2 data-pinned:bg-(--table-bg) group-hover/table-row:data-pinned:bg-(--table-hover) group-data-[state=selected]/table-row:data-pinned:bg-muted data-[pinned=start]:start-(--pin-offset) data-[pinned=end]:end-(--pin-offset) data-pinned:[--pin-offset:0px] data-pinned-edge:after:pointer-events-none data-pinned-edge:after:absolute data-pinned-edge:after:inset-y-0 data-pinned-edge:after:hidden data-pinned-edge:after:w-4 data-[pinned=start]:data-pinned-edge:after:-end-4 data-[pinned=start]:data-pinned-edge:after:bg-linear-to-r data-[pinned=start]:data-pinned-edge:after:from-foreground/6 data-[pinned=start]:data-pinned-edge:after:to-transparent rtl:data-[pinned=start]:data-pinned-edge:after:bg-linear-to-l data-[pinned=end]:data-pinned-edge:after:-start-4 data-[pinned=end]:data-pinned-edge:after:bg-linear-to-l data-[pinned=end]:data-pinned-edge:after:from-foreground/6 data-[pinned=end]:data-pinned-edge:after:to-transparent rtl:data-[pinned=end]:data-pinned-edge:after:bg-linear-to-r in-data-scrolled-start:data-[pinned=start]:data-pinned-edge:after:block in-data-scrolled-end:data-[pinned=end]:data-pinned-edge:after:block [&:has([role=checkbox])]:w-px [&>[role=checkbox]]:flex group-data-[variant=surface]/table:first:ps-4 group-data-[variant=surface]/table:last:pe-4"

type TableCellExtraProps = {
  align?: "start" | "center" | "end"
  pinned?: "start" | "end"
  pinnedEdge?: boolean
}

function TableHead({
  className,
  align,
  pinned,
  pinnedEdge,
  ...props
}: Omit<React.ComponentProps<"th">, "align"> & TableCellExtraProps) {
  return (
    <th
      data-slot="table-head"
      data-align={align}
      data-pinned={pinned}
      data-pinned-edge={pinnedEdge ? "" : undefined}
      className={cn(
        cellBase,
        "h-(--table-head-h) bg-(--table-head-bg,var(--table-bg)) px-(--table-cell-px) text-start font-medium text-muted-foreground group-data-wrap/table:align-middle in-data-scrolled-top:shadow-[inset_0_calc(var(--hairline)*-1)_0_var(--border)] in-data-sticky-header:sticky in-data-sticky-header:top-0 in-data-sticky-header:z-2 in-[tbody]:h-auto in-[tbody]:bg-transparent in-[tbody]:py-(--table-cell-py) in-[tbody]:text-foreground in-[tbody]:shadow-none in-[tbody]:group-data-wrap/table:align-top data-pinned:bg-(--table-head-bg,var(--table-bg)) in-data-sticky-header:data-pinned:z-3 in-[tbody]:data-pinned:bg-(--table-bg) in-[tbody]:group-hover/table-row:data-pinned:bg-(--table-hover) in-[tbody]:group-data-[state=selected]/table-row:data-pinned:bg-muted [&:has([role=checkbox])]:pe-0",
        className
      )}
      {...props}
    />
  )
}

function TableCell({
  className,
  align,
  pinned,
  pinnedEdge,
  ...props
}: Omit<React.ComponentProps<"td">, "align"> & TableCellExtraProps) {
  return (
    <td
      data-slot="table-cell"
      data-align={align}
      data-pinned={pinned}
      data-pinned-edge={pinnedEdge ? "" : undefined}
      className={cn(
        cellBase,
        "px-(--table-cell-px) py-(--table-cell-py) [&:has([role=checkbox])]:pe-0",
        className
      )}
      {...props}
    />
  )
}

function TableCaption({
  className,
  ...props
}: React.ComponentProps<"caption">) {
  return (
    <caption
      data-slot="table-caption"
      className={cn(
        "mt-4 text-sm text-muted-foreground group-data-[variant=surface]/table:mt-0 group-data-[variant=surface]/table:border-t group-data-[variant=surface]/table:px-4 group-data-[variant=surface]/table:py-2.5 group-data-[variant=surface]/table:text-start",
        className
      )}
      {...props}
    />
  )
}

export {
  Table,
  TableHeader,
  TableBody,
  TableFooter,
  TableHead,
  TableRow,
  TableCell,
  TableCaption,
  tableVariants,
}
export type { TableProps, TableSize, TableVariant }
```

```tsx title="components/ui/button.tsx"
"use client"

import * as React from "react"
import { Button as ButtonPrimitive } from "@base-ui/react/button"
import { IconAlertCircle, IconCircleCheck } from "@tabler/icons-react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "cn"

import { Spinner } from "@/components/ui/spinner"
import {
  useButtonFeedback,
  type ButtonFeedbackOptions,
  type ButtonStatus,
} from "@/hooks/use-button-feedback"

const buttonVariants = cva(
  "group/button relative inline-flex items-center justify-center rounded-md bg-clip-padding text-sm font-medium whitespace-nowrap inset-ring-(length:--hairline) inset-ring-transparent transition-[color,background-color,box-shadow,opacity,translate,scale] duration-200 ease-out-quint outline-none select-none focus-visible:ring-3 focus-visible:ring-focus-ring focus-visible:inset-ring-ring focus-visible:outline-hidden active:duration-100 active:not-aria-[haspopup]:not-aria-busy:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-busy:cursor-progress aria-invalid:ring-3 aria-invalid:ring-destructive/20 aria-invalid:inset-ring-destructive motion-safe:active:not-aria-[haspopup]:not-aria-busy:scale-[0.97] dark:aria-invalid:ring-destructive/40 dark:aria-invalid:inset-ring-destructive/50 forced-colors:border [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
  {
    variants: {
      variant: {
        default: "bg-primary text-primary-foreground hover:bg-primary/80",
        outline:
          "bg-background inset-ring-border hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:bg-input/30 dark:inset-ring-input dark:hover:bg-input/50",
        secondary:
          "bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
        ghost:
          "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50 forced-colors:border-0",
        destructive:
          "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:ring-destructive/70 focus-visible:inset-ring-destructive/40 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/70",
        link: "text-primary underline-offset-4 hover:underline forced-colors:border-0",
        success:
          "bg-success text-success-foreground hover:bg-success/90 focus-visible:ring-success/70 focus-visible:inset-ring-success/40",
        error:
          "bg-destructive text-destructive-foreground hover:bg-destructive/90 focus-visible:ring-destructive/70 focus-visible:inset-ring-destructive/40 motion-safe:animate-button-shake",
      },
      size: {
        default:
          "h-9 gap-1.5 px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pe-2 has-data-[icon=inline-start]:ps-2",
        xs: "h-6 gap-1 rounded-[min(var(--radius-md),8px)] px-2 text-xs in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pe-1.5 has-data-[icon=inline-start]:ps-1.5 [&_svg:not([class*='size-'])]:size-3",
        sm: "h-8 gap-1 rounded-[min(var(--radius-md),10px)] px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pe-1.5 has-data-[icon=inline-start]:ps-1.5",
        lg: "h-10 gap-1.5 px-2.5 has-data-[icon=inline-end]:pe-2 has-data-[icon=inline-start]:ps-2",
        icon: "size-9 shrink-0",
        "icon-xs":
          "size-6 shrink-0 rounded-[min(var(--radius-md),8px)] in-data-[slot=button-group]:rounded-md pointer-coarse:after:absolute pointer-coarse:after:-inset-2.5 pointer-coarse:in-data-[slot=button-group]:after:hidden [&_svg:not([class*='size-'])]:size-3",
        "icon-sm":
          "size-8 shrink-0 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-md pointer-coarse:after:absolute pointer-coarse:after:-inset-1.5 pointer-coarse:in-data-[slot=button-group]:after:hidden",
        "icon-lg": "size-10 shrink-0",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
)

type ButtonVariant = Exclude<
  NonNullable<VariantProps<typeof buttonVariants>["variant"]>,
  "success" | "error"
>

type ButtonClickEvent = Parameters<
  NonNullable<ButtonPrimitive.Props["onClick"]>
>[0]

type ButtonProps = Omit<ButtonPrimitive.Props, "onClick"> &
  Omit<VariantProps<typeof buttonVariants>, "variant"> &
  ButtonFeedbackOptions & {
    variant?: ButtonVariant | null
    onClick?: (event: ButtonClickEvent) => unknown
    feedback?: boolean
    loading?: boolean
    status?: ButtonStatus
    loadingLabel?: React.ReactNode
    successLabel?: React.ReactNode
    errorLabel?: React.ReactNode | ((error: unknown) => React.ReactNode)
  }

const stateTransitionTime = 220
const statuses: ButtonStatus[] = ["idle", "loading", "success", "error"]
const quietVariants: ButtonVariant[] = ["ghost", "link"]
const quietStatusClasses: Partial<Record<ButtonStatus, string>> = {
  success: "text-success hover:text-success",
  error:
    "text-destructive hover:text-destructive motion-safe:animate-button-shake",
}

function isThenable(value: unknown): value is PromiseLike<unknown> {
  return (
    typeof value === "object" &&
    value !== null &&
    typeof (value as PromiseLike<unknown>).then === "function"
  )
}

function useStatusTransition(status: ButtonStatus) {
  const [shown, setShown] = React.useState(status)
  const [leaving, setLeaving] = React.useState<ButtonStatus | null>(null)
  const [transitionKey, setTransitionKey] = React.useState(0)

  if (status !== shown) {
    setLeaving(shown)
    setShown(status)
    setTransitionKey((key) => key + 1)
  }

  React.useEffect(() => {
    if (leaving === null) {
      return
    }
    const timer = setTimeout(() => setLeaving(null), stateTransitionTime)
    return () => clearTimeout(timer)
  }, [leaving, transitionKey])

  return { leaving, transitionKey }
}

function useLayerWidth(status: ButtonStatus, active: boolean) {
  const contentRef = React.useRef<HTMLSpanElement>(null)
  const layersRef = React.useRef(new Map<ButtonStatus, HTMLElement>())
  const observerRef = React.useRef<ResizeObserver | null>(null)
  const statusRef = React.useRef(status)

  const applyWidth = React.useCallback(() => {
    const content = contentRef.current
    const layer = layersRef.current.get(statusRef.current)
    if (!content || !layer) {
      return
    }
    const natural = parseFloat(getComputedStyle(layer).width)
    if (!Number.isFinite(natural)) {
      return
    }
    content.style.width = `${natural}px`
    if (!content.hasAttribute("data-measured")) {
      requestAnimationFrame(() => content.setAttribute("data-measured", ""))
    }
  }, [])

  React.useLayoutEffect(() => {
    statusRef.current = status
    if (active) {
      applyWidth()
    }
  }, [active, applyWidth, status])

  React.useLayoutEffect(() => {
    if (!active || typeof ResizeObserver === "undefined") {
      return
    }
    const observer = new ResizeObserver(applyWidth)
    observerRef.current = observer
    layersRef.current.forEach((layer) => observer.observe(layer))
    document.fonts?.addEventListener?.("loadingdone", applyWidth)
    return () => {
      observer.disconnect()
      observerRef.current = null
      document.fonts?.removeEventListener?.("loadingdone", applyWidth)
    }
  }, [active, applyWidth])

  const layerRef = React.useCallback((node: HTMLElement | null) => {
    if (!node) {
      return
    }
    const layer = node.dataset.layer as ButtonStatus
    layersRef.current.set(layer, node)
    observerRef.current?.observe(node)
    return () => {
      observerRef.current?.unobserve(node)
      if (layersRef.current.get(layer) === node) {
        layersRef.current.delete(layer)
      }
    }
  }, [])

  return { contentRef, layerRef }
}

function composeHandlers<Event>(
  first: ((event: Event) => void) | undefined,
  second: (event: Event) => void
) {
  return (event: Event) => {
    first?.(event)
    second(event)
  }
}

function Button({
  className,
  variant = "default",
  size = "default",
  children,
  onClick,
  disabled,
  focusableWhenDisabled,
  feedback,
  loading,
  status: statusProp,
  onStatusChange,
  onError,
  resetAfter,
  loadingLabel,
  successLabel = "Done",
  errorLabel = "Failed",
  onPointerEnter,
  onPointerLeave,
  onFocus,
  onBlur,
  ...props
}: ButtonProps) {
  const internal = useButtonFeedback({ resetAfter, onStatusChange, onError })
  const status = statusProp ?? (loading ? "loading" : internal.status)
  const requested =
    Boolean(feedback) || loading !== undefined || statusProp !== undefined
  const [enabled, setEnabled] = React.useState(requested)
  if (requested && !enabled) {
    setEnabled(true)
  }

  const { leaving, transitionKey } = useStatusTransition(status)
  const iconOnly = size?.startsWith("icon") ?? false
  const widthStatus =
    status === "loading" && loadingLabel === undefined ? "idle" : status
  const { contentRef, layerRef } = useLayerWidth(
    widthStatus,
    enabled && !iconOnly
  )
  const isLoading = status === "loading"
  const quiet = quietVariants.includes(variant ?? "default")
  const resolvedVariant =
    !quiet && status === "success"
      ? "success"
      : !quiet && status === "error"
        ? "error"
        : variant
  const resolvedErrorLabel =
    typeof errorLabel !== "function"
      ? errorLabel
      : internal.error !== undefined
        ? errorLabel(internal.error)
        : "Failed"

  const handleClick = (event: ButtonClickEvent) => {
    if (feedback && internal.isPending()) {
      return
    }
    const result = onClick?.(event)
    if (feedback && isThenable(result)) {
      internal.track(result)
    }
  }

  const layerContent = (layer: ButtonStatus, animated: boolean) => {
    if (layer === "idle") {
      return children
    }
    if (layer === "loading") {
      return (
        <>
          <Spinner aria-hidden size={null} animated={animated} />
          {iconOnly ? null : loadingLabel}
        </>
      )
    }
    if (layer === "success") {
      return (
        <>
          <IconCircleCheck
            className={cn(
              animated &&
                "motion-safe:[&>path:first-child]:animate-button-draw-circle motion-safe:[&>path:first-child]:[stroke-dasharray:57] motion-safe:[&>path:first-child]:[stroke-dashoffset:57] motion-safe:[&>path:last-child]:animate-button-draw-check motion-safe:[&>path:last-child]:[stroke-dasharray:9] motion-safe:[&>path:last-child]:[stroke-dashoffset:9]"
            )}
          />
          {iconOnly ? null : successLabel}
        </>
      )
    }
    return (
      <>
        <IconAlertCircle />
        {iconOnly ? null : resolvedErrorLabel}
      </>
    )
  }

  const button = (
    <ButtonPrimitive
      data-slot="button"
      data-status={enabled ? status : undefined}
      aria-busy={isLoading || undefined}
      disabled={disabled || isLoading}
      focusableWhenDisabled={focusableWhenDisabled ?? (isLoading || undefined)}
      className={cn(
        buttonVariants({ variant: resolvedVariant, size }),
        quiet && quietStatusClasses[status],
        className
      )}
      onClick={handleClick}
      onPointerEnter={composeHandlers(
        onPointerEnter,
        internal.buttonProps.onPointerEnter
      )}
      onPointerLeave={composeHandlers(
        onPointerLeave,
        internal.buttonProps.onPointerLeave
      )}
      onFocus={composeHandlers(onFocus, internal.buttonProps.onFocus)}
      onBlur={composeHandlers(onBlur, internal.buttonProps.onBlur)}
      {...props}
    >
      {enabled ? (
        <span
          ref={contentRef}
          data-slot="button-content"
          className="relative inline-grid grid-cols-[minmax(0,1fr)] place-items-center gap-[inherit] overflow-x-clip *:col-start-1 *:row-start-1 motion-safe:data-measured:transition-[width] motion-safe:data-measured:duration-220 motion-safe:data-measured:ease-[cubic-bezier(0.2,0,0,1)]"
        >
          {statuses.map((layer) => {
            const active = layer === status
            const exiting = layer === leaving
            const namesButton =
              layer === "idle" && isLoading && loadingLabel === undefined

            return (
              <span
                key={active ? `${layer}-${transitionKey}` : layer}
                ref={layerRef}
                data-slot="button-layer"
                data-layer={layer}
                aria-hidden={active || namesButton ? undefined : true}
                className={cn(
                  "inline-flex items-center justify-center gap-[inherit] backface-hidden",
                  !active &&
                    !exiting &&
                    "pointer-events-none absolute opacity-0",
                  active &&
                    transitionKey > 0 &&
                    "origin-bottom motion-safe:animate-button-state-in motion-reduce:animate-in motion-reduce:animation-duration-150 motion-reduce:fade-in-0",
                  exiting &&
                    "pointer-events-none origin-top opacity-0 motion-safe:animate-button-state-out"
                )}
              >
                {layerContent(layer, active || exiting)}
              </span>
            )
          })}
        </span>
      ) : (
        children
      )}
    </ButtonPrimitive>
  )

  if (!enabled) {
    return button
  }

  const announcement =
    status === "loading"
      ? typeof loadingLabel === "string"
        ? loadingLabel
        : "Loading"
      : status === "success"
        ? typeof successLabel === "string"
          ? successLabel
          : "Done"
        : status === "error"
          ? typeof resolvedErrorLabel === "string"
            ? resolvedErrorLabel
            : "Failed"
          : ""

  return (
    <>
      {button}
      <span data-slot="button-status" role="status" className="sr-only">
        {announcement}
      </span>
    </>
  )
}

export { Button, buttonVariants, useButtonFeedback }
export type { ButtonProps, ButtonStatus, ButtonFeedbackOptions }
```

```tsx title="components/ui/checkbox.tsx"
"use client"

import * as React from "react"
import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox"
import { CheckboxGroup as CheckboxGroupPrimitive } from "@base-ui/react/checkbox-group"
import { IconCheck, IconMinus } from "@tabler/icons-react"
import { cn } from "cn"

function mergeClassName<State>(
  base: string,
  className: string | ((state: State) => string | undefined) | undefined
) {
  return typeof className === "function"
    ? (state: State) => cn(base, className(state))
    : cn(base, className)
}

const checkboxClassName =
  "peer relative inline-flex size-4 shrink-0 items-center justify-center rounded-[calc(var(--radius-sm)*0.5)] inset-ring-(length:--hairline) forced-colors:border inset-ring-muted-foreground/80 bg-background text-primary-foreground transition-[background-color,border-color,box-shadow,scale] duration-150 ease-out-quint outline-none focus-visible:outline-hidden select-none after:absolute after:-inset-2.5 pointer-coarse:after:-inset-3.5 hover:inset-ring-foreground/70 [@media(hover:hover)]:in-[label:hover]:inset-ring-foreground/70 focus-visible:inset-ring-ring focus-visible:ring-3 focus-visible:ring-focus-ring motion-safe:active:scale-95 motion-safe:in-[label:active]:scale-95 data-checked:inset-ring-primary data-checked:bg-primary data-indeterminate:inset-ring-primary data-indeterminate:bg-primary data-disabled:cursor-not-allowed data-disabled:opacity-50 data-disabled:hover:inset-ring-muted-foreground/80 data-disabled:[@media(hover:hover)]:in-[label:hover]:inset-ring-muted-foreground/80 motion-safe:data-disabled:active:scale-100 motion-safe:data-disabled:in-[label:active]:scale-100 data-readonly:cursor-default motion-safe:data-readonly:active:scale-100 motion-safe:data-readonly:in-[label:active]:scale-100 aria-invalid:inset-ring-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-invalid:inset-ring-destructive data-invalid:ring-3 data-invalid:ring-destructive/20 dark:bg-input/30 dark:aria-invalid:inset-ring-destructive/50 dark:aria-invalid:ring-destructive/40 dark:data-invalid:inset-ring-destructive/50 dark:data-invalid:ring-destructive/40 data-checked:data-invalid:inset-ring-primary data-checked:aria-invalid:inset-ring-primary dark:data-checked:bg-primary dark:data-indeterminate:bg-primary"

function CheckboxMark({ indeterminate }: { indeterminate: boolean }) {
  const markRef = React.useRef<SVGSVGElement>(null)

  React.useLayoutEffect(() => {
    const mark = markRef.current
    const root = mark?.closest("[data-slot=checkbox]")

    if (mark && root?.hasAttribute("data-mounted")) {
      mark.setAttribute("data-draw", "")
    }
  }, [])

  const Icon = indeterminate ? IconMinus : IconCheck

  return (
    <Icon
      ref={markRef}
      aria-hidden
      stroke={2.75}
      className={cn(
        "pointer-events-none size-3.5 motion-safe:data-draw:animate-checkbox-draw",
        indeterminate
          ? "motion-safe:data-draw:[stroke-dasharray:15] motion-safe:data-draw:[stroke-dashoffset:15]"
          : "motion-safe:data-draw:[stroke-dasharray:22] motion-safe:data-draw:[stroke-dashoffset:22]"
      )}
    />
  )
}

function Checkbox({
  className,
  children,
  ref,
  ...props
}: CheckboxPrimitive.Root.Props) {
  const rootRef = React.useRef<HTMLElement | null>(null)

  const setRoot = React.useCallback(
    (node: HTMLElement | null) => {
      rootRef.current = node
      if (typeof ref === "function") {
        ref(node)
      } else if (ref) {
        ref.current = node
      }
    },
    [ref]
  )

  React.useEffect(() => {
    const frame = requestAnimationFrame(() => {
      rootRef.current?.setAttribute("data-mounted", "")
    })
    return () => cancelAnimationFrame(frame)
  }, [])

  return (
    <CheckboxPrimitive.Root
      ref={setRoot}
      className={mergeClassName(checkboxClassName, className)}
      {...props}
      data-slot="checkbox"
    >
      <CheckboxPrimitive.Indicator
        data-slot="checkbox-indicator"
        className="grid place-content-center text-current transition-[opacity,scale] duration-100 ease-out-quint data-ending-style:opacity-0 motion-safe:data-ending-style:scale-75"
        render={(indicatorProps, state) => (
          <span {...indicatorProps}>
            <CheckboxMark
              key={state.indeterminate ? "indeterminate" : "checked"}
              indeterminate={state.indeterminate}
            />
          </span>
        )}
      />
      {children}
    </CheckboxPrimitive.Root>
  )
}

function CheckboxGroup({ className, ...props }: CheckboxGroupPrimitive.Props) {
  return (
    <CheckboxGroupPrimitive
      className={mergeClassName("flex flex-col gap-3", className)}
      {...props}
      data-slot="checkbox-group"
    />
  )
}

export { Checkbox, CheckboxGroup }
```

```tsx title="components/ui/skeleton.tsx"
"use client"

import * as React from "react"
import { mergeProps } from "@base-ui/react/merge-props"
import { useRender } from "@base-ui/react/use-render"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "cn"

const skeletonVariants = cva(
  "relative overflow-hidden rounded-md bg-muted motion-safe:animate-in motion-safe:animation-duration-300 motion-safe:fade-in-0 motion-safe:fill-mode-backwards motion-safe:[animation-delay:150ms] rtl:[--skeleton-dir:-1] [&:where(span)]:inline-block [&:where(span)]:align-bottom",
  {
    variants: {
      animation: {
        shimmer:
          "after:pointer-events-none after:absolute after:inset-0 after:[translate:-100%_0] after:bg-linear-to-r after:from-transparent after:via-foreground/8 after:to-transparent motion-safe:after:animate-shimmer dark:after:via-foreground/10",
        pulse:
          "after:pointer-events-none after:absolute after:inset-0 after:bg-background after:opacity-0 motion-safe:after:animate-skeleton-pulse",
        none: "",
      },
    },
    defaultVariants: {
      animation: "shimmer",
    },
  }
)

type SkeletonAnimation = NonNullable<
  VariantProps<typeof skeletonVariants>["animation"]
>

type SkeletonProps = useRender.ComponentProps<"div"> & {
  animation?: SkeletonAnimation
  loading?: boolean
}

function useReveal(loading: boolean | undefined) {
  const [previous, setPrevious] = React.useState(loading)
  const [revealed, setRevealed] = React.useState(false)

  if (loading !== previous) {
    setPrevious(loading)
    setRevealed(previous === true && loading === false)
  }

  return revealed
}

function Skeleton({
  className,
  animation = "shimmer",
  loading,
  render,
  children,
  ...props
}: SkeletonProps) {
  const revealed = useReveal(loading)
  const wraps = loading !== undefined
  const showsSkeleton = !wraps || loading

  return useRender({
    defaultTagName: "div",
    render,
    props: mergeProps<"div">(
      {
        ...({ "data-slot": "skeleton" } as Record<string, string>),
        className: cn(
          wraps && "w-fit max-w-full",
          showsSkeleton && skeletonVariants({ animation }),
          wraps &&
            !loading &&
            revealed &&
            "motion-safe:animate-in motion-safe:ease-out-quint motion-safe:animation-duration-200 motion-safe:fade-in-0",
          className
        ),
        "aria-hidden": wraps ? undefined : true,
        "aria-busy": loading || undefined,
        children: wraps ? (
          <span
            data-slot="skeleton-content"
            className={cn("contents", loading && "invisible")}
            inert={loading || undefined}
            aria-hidden={loading || undefined}
          >
            {children}
          </span>
        ) : (
          children
        ),
      },
      props,
      {
        "data-animation": showsSkeleton ? animation : undefined,
        "data-loading": wraps ? String(Boolean(loading)) : undefined,
      } as Record<string, string | undefined>
    ),
  })
}

type SkeletonTextProps = React.ComponentProps<"div"> & {
  lines?: number
  animation?: SkeletonAnimation
}

function SkeletonText({
  className,
  lines = 3,
  animation,
  ...props
}: SkeletonTextProps) {
  const count = Math.min(
    50,
    Math.max(1, Math.floor(Number.isFinite(lines) ? lines : 1))
  )

  return (
    <div
      data-slot="skeleton-text"
      aria-hidden="true"
      className={cn("flex w-full flex-col", className)}
      {...props}
    >
      {Array.from({ length: count }, (_, index) => (
        <div key={index} className="flex h-[1lh] items-center">
          <Skeleton
            animation={animation}
            className={cn(
              "h-[0.8em] rounded-sm",
              count > 1 && index === count - 1 ? "w-3/5" : "w-full"
            )}
          />
        </div>
      ))}
    </div>
  )
}

export { Skeleton, SkeletonText, skeletonVariants }
export type { SkeletonProps, SkeletonTextProps, SkeletonAnimation }
```

Update the import paths to match your project setup.

## Usage

The data table is TanStack Table v9 with the sorting, filtering, pagination, selection and column visibility features already wired up. Define columns once with `createDataTableColumns`, create the table with `useDataTable`, then compose the parts you need.

```tsx
import {
  createDataTableColumns,
  DataTable,
  DataTableColumnHeader,
  DataTableContent,
  DataTablePagination,
  DataTableSearch,
  DataTableSelectAll,
  DataTableSelectRow,
  DataTableToolbar,
  DataTableViewOptions,
  useDataTable,
} from "@/components/ui/data-table"
```

```tsx
type Payment = { id: string; email: string; amount: number }

const helper = createDataTableColumns<Payment>()

const columns = helper.columns([
  helper.display({
    id: "select",
    header: ({ table }) => <DataTableSelectAll table={table} />,
    cell: ({ row }) => <DataTableSelectRow row={row} />,
    enableSorting: false,
    enableHiding: false,
  }),
  helper.accessor("email", {
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="Email" />
    ),
  }),
  helper.accessor("amount", {
    header: "Amount",
    meta: { align: "end" },
  }),
])
```

```tsx
export function Payments({ data }: { data: Payment[] }) {
  const table = useDataTable({
    data,
    columns,
    getRowId: (row) => row.id,
  })

  return (
    <DataTable table={table}>
      <DataTableToolbar>
        <DataTableSearch placeholder="Search payments…" />
        <DataTableViewOptions />
      </DataTableToolbar>
      <DataTableContent />
      <DataTablePagination />
    </DataTable>
  )
}
```

Pass any TanStack state you want to own, such as sorting or row selection, with its change handler.

```tsx
const [sorting, setSorting] = React.useState([{ id: "date", desc: true }])

const table = useDataTable({
  data,
  columns,
  state: { sorting },
  onSortingChange: setSorting,
})
```

## Composition

```text
DataTable
├── DataTableToolbar
│   ├── DataTableSearch
│   └── DataTableViewOptions
├── DataTableContent
└── DataTablePagination
```

`DataTableColumnHeader`, `DataTableSelectAll` and `DataTableSelectRow` go in your column definitions, as a column’s header or cell.

## Examples

### Plain table

The plain `<Table />` parts the data table is built from. Use them on their own for static data, with a caption and a footer total.

```tsx title="components/examples/data-table/table.tsx"
import {
  Table,
  TableBody,
  TableCaption,
  TableCell,
  TableFooter,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table"

const invoices = [
  { id: "INV001", status: "Paid", method: "Credit card", amount: 250 },
  { id: "INV002", status: "Pending", method: "PayPal", amount: 150 },
  { id: "INV003", status: "Unpaid", method: "Bank transfer", amount: 350 },
  { id: "INV004", status: "Paid", method: "Credit card", amount: 450 },
]

const currency = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD",
})

export function TableDemo() {
  const total = invoices.reduce((sum, invoice) => sum + invoice.amount, 0)

  return (
    <Table>
      <TableCaption>A list of your recent invoices.</TableCaption>
      <TableHeader>
        <TableRow>
          <TableHead>Invoice</TableHead>
          <TableHead>Status</TableHead>
          <TableHead>Method</TableHead>
          <TableHead align="end">Amount</TableHead>
        </TableRow>
      </TableHeader>
      <TableBody>
        {invoices.map((invoice) => (
          <TableRow key={invoice.id}>
            <TableCell>{invoice.id}</TableCell>
            <TableCell>{invoice.status}</TableCell>
            <TableCell>{invoice.method}</TableCell>
            <TableCell align="end">{currency.format(invoice.amount)}</TableCell>
          </TableRow>
        ))}
      </TableBody>
      <TableFooter>
        <TableRow>
          <TableCell colSpan={3}>Total</TableCell>
          <TableCell align="end">{currency.format(total)}</TableCell>
        </TableRow>
      </TableFooter>
    </Table>
  )
}
```

### Loading

With `loading`, skeleton rows fill the same space as real rows, so nothing jumps when the data arrives. The table is marked `aria-busy` meanwhile.

```tsx title="components/examples/data-table/loading.tsx"
"use client"

import * as React from "react"

import { Badge, BadgeDot } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
  createDataTableColumns,
  DataTable,
  DataTableColumnHeader,
  DataTableContent,
  DataTablePagination,
  DataTableSearch,
  DataTableSelectAll,
  DataTableSelectRow,
  DataTableToolbar,
  DataTableViewOptions,
  useDataTable,
} from "@/components/ui/data-table"

type Payment = {
  id: string
  email: string
  status: "paid" | "pending" | "failed"
  amount: number
  method: string
  country: string
  date: string
}

const statuses = ["paid", "pending", "failed"] as const
const methods = ["Card", "PayPal", "Bank", "Apple Pay"]
const countries = ["United States", "Germany", "India", "Japan", "Brazil"]
const names = ["olivia", "jackson", "isabella", "william", "sofia", "liam"]

function makePayments(count: number): Payment[] {
  return Array.from({ length: count }, (_, index) => ({
    id: `pay_${String(index + 1).padStart(4, "0")}`,
    email: `${names[index % names.length]}.${Math.floor(index / names.length) + 1}@example.com`,
    status: statuses[(index * 7) % 3],
    amount: Math.round(((index * 7919) % 100000) / 10) / 10 + 5,
    method: methods[index % methods.length],
    country: countries[(index * 3) % countries.length],
    date: `2026-${String((index % 9) + 1).padStart(2, "0")}-${String((index % 27) + 1).padStart(2, "0")}`,
  }))
}

const payments = makePayments(5)

const currency = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD",
})

const statusVariant = {
  paid: "success",
  pending: "warning",
  failed: "destructive",
} as const

const helper = createDataTableColumns<Payment>()

const columns = helper.columns([
  helper.display({
    id: "select",
    header: ({ table }) => <DataTableSelectAll table={table} />,
    cell: ({ row }) => <DataTableSelectRow row={row} />,
    enableSorting: false,
    enableHiding: false,
  }),
  helper.accessor("email", {
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="Email" />
    ),
  }),
  helper.accessor("status", {
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="Status" />
    ),
    cell: ({ getValue }) => {
      const status = getValue()
      return (
        <Badge variant={statusVariant[status]}>
          <BadgeDot />
          <span className="capitalize">{status}</span>
        </Badge>
      )
    },
  }),
  helper.accessor("method", { header: "Method" }),
  helper.accessor("country", { header: "Country" }),
  helper.accessor("date", {
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="Date" />
    ),
  }),
  helper.accessor("amount", {
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="Amount" />
    ),
    cell: ({ getValue }) => currency.format(getValue()),
    meta: { align: "end" },
  }),
])

export function DataTableLoading() {
  const [loading, setLoading] = React.useState(true)
  const table = useDataTable({
    data: payments,
    columns,
    getRowId: (row) => row.id,
  })

  return (
    <div className="flex w-full flex-col items-start gap-3">
      <Button
        variant="outline"
        size="sm"
        onClick={() => setLoading((value) => !value)}
      >
        {loading ? "Finish loading" : "Load again"}
      </Button>
      <DataTable table={table}>
        <DataTableToolbar>
          <DataTableSearch placeholder="Search payments…" />
          <DataTableViewOptions />
        </DataTableToolbar>
        <DataTableContent loading={loading} />
        <DataTablePagination />
      </DataTable>
    </div>
  )
}
```

### Empty

`emptyMessage` fills the body when there is no data or nothing matches the search.

```tsx title="components/examples/data-table/empty.tsx"
"use client"

import { Badge, BadgeDot } from "@/components/ui/badge"
import {
  createDataTableColumns,
  DataTable,
  DataTableColumnHeader,
  DataTableContent,
  DataTablePagination,
  DataTableSearch,
  DataTableSelectAll,
  DataTableSelectRow,
  DataTableToolbar,
  DataTableViewOptions,
  useDataTable,
} from "@/components/ui/data-table"

type Payment = {
  id: string
  email: string
  status: "paid" | "pending" | "failed"
  amount: number
  method: string
  country: string
  date: string
}

const statuses = ["paid", "pending", "failed"] as const
const methods = ["Card", "PayPal", "Bank", "Apple Pay"]
const countries = ["United States", "Germany", "India", "Japan", "Brazil"]
const names = ["olivia", "jackson", "isabella", "william", "sofia", "liam"]

function makePayments(count: number): Payment[] {
  return Array.from({ length: count }, (_, index) => ({
    id: `pay_${String(index + 1).padStart(4, "0")}`,
    email: `${names[index % names.length]}.${Math.floor(index / names.length) + 1}@example.com`,
    status: statuses[(index * 7) % 3],
    amount: Math.round(((index * 7919) % 100000) / 10) / 10 + 5,
    method: methods[index % methods.length],
    country: countries[(index * 3) % countries.length],
    date: `2026-${String((index % 9) + 1).padStart(2, "0")}-${String((index % 27) + 1).padStart(2, "0")}`,
  }))
}

const payments: Payment[] = []

const currency = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD",
})

const statusVariant = {
  paid: "success",
  pending: "warning",
  failed: "destructive",
} as const

const helper = createDataTableColumns<Payment>()

const columns = helper.columns([
  helper.display({
    id: "select",
    header: ({ table }) => <DataTableSelectAll table={table} />,
    cell: ({ row }) => <DataTableSelectRow row={row} />,
    enableSorting: false,
    enableHiding: false,
  }),
  helper.accessor("email", {
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="Email" />
    ),
  }),
  helper.accessor("status", {
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="Status" />
    ),
    cell: ({ getValue }) => {
      const status = getValue()
      return (
        <Badge variant={statusVariant[status]}>
          <BadgeDot />
          <span className="capitalize">{status}</span>
        </Badge>
      )
    },
  }),
  helper.accessor("method", { header: "Method" }),
  helper.accessor("country", { header: "Country" }),
  helper.accessor("date", {
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="Date" />
    ),
  }),
  helper.accessor("amount", {
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="Amount" />
    ),
    cell: ({ getValue }) => currency.format(getValue()),
    meta: { align: "end" },
  }),
])

export function DataTableEmpty() {
  const table = useDataTable({
    data: payments,
    columns,
    getRowId: (row) => row.id,
  })

  return (
    <DataTable table={table}>
      <DataTableToolbar>
        <DataTableSearch placeholder="Search payments…" />
        <DataTableViewOptions />
      </DataTableToolbar>
      <DataTableContent emptyMessage="No payments yet." />
      <DataTablePagination />
    </DataTable>
  )
}
```

### Sticky header

Give the container a max height with `containerClassName` and set `stickyHeader`. The header stays put and gains a hairline once the rows scroll under it. Shift-drag over the checkboxes scrolls the box as you near its edge.

```tsx title="components/examples/data-table/sticky-header.tsx"
"use client"

import { Badge, BadgeDot } from "@/components/ui/badge"
import {
  createDataTableColumns,
  DataTable,
  DataTableColumnHeader,
  DataTableContent,
  DataTablePagination,
  DataTableSearch,
  DataTableSelectAll,
  DataTableSelectRow,
  DataTableToolbar,
  DataTableViewOptions,
  useDataTable,
} from "@/components/ui/data-table"

type Payment = {
  id: string
  email: string
  status: "paid" | "pending" | "failed"
  amount: number
  method: string
  country: string
  date: string
}

const statuses = ["paid", "pending", "failed"] as const
const methods = ["Card", "PayPal", "Bank", "Apple Pay"]
const countries = ["United States", "Germany", "India", "Japan", "Brazil"]
const names = ["olivia", "jackson", "isabella", "william", "sofia", "liam"]

function makePayments(count: number): Payment[] {
  return Array.from({ length: count }, (_, index) => ({
    id: `pay_${String(index + 1).padStart(4, "0")}`,
    email: `${names[index % names.length]}.${Math.floor(index / names.length) + 1}@example.com`,
    status: statuses[(index * 7) % 3],
    amount: Math.round(((index * 7919) % 100000) / 10) / 10 + 5,
    method: methods[index % methods.length],
    country: countries[(index * 3) % countries.length],
    date: `2026-${String((index % 9) + 1).padStart(2, "0")}-${String((index % 27) + 1).padStart(2, "0")}`,
  }))
}

const payments = makePayments(1000)

const currency = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD",
})

const statusVariant = {
  paid: "success",
  pending: "warning",
  failed: "destructive",
} as const

const helper = createDataTableColumns<Payment>()

const columns = helper.columns([
  helper.display({
    id: "select",
    header: ({ table }) => <DataTableSelectAll table={table} />,
    cell: ({ row }) => <DataTableSelectRow row={row} />,
    enableSorting: false,
    enableHiding: false,
  }),
  helper.accessor("email", {
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="Email" />
    ),
  }),
  helper.accessor("status", {
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="Status" />
    ),
    cell: ({ getValue }) => {
      const status = getValue()
      return (
        <Badge variant={statusVariant[status]}>
          <BadgeDot />
          <span className="capitalize">{status}</span>
        </Badge>
      )
    },
  }),
  helper.accessor("method", { header: "Method" }),
  helper.accessor("country", { header: "Country" }),
  helper.accessor("date", {
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="Date" />
    ),
  }),
  helper.accessor("amount", {
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="Amount" />
    ),
    cell: ({ getValue }) => currency.format(getValue()),
    meta: { align: "end" },
  }),
])

export function DataTableStickyHeader() {
  const table = useDataTable({
    data: payments,
    columns,
    getRowId: (row) => row.id,
    initialState: { pagination: { pageIndex: 0, pageSize: 100 } },
  })

  return (
    <DataTable table={table}>
      <DataTableToolbar>
        <DataTableSearch placeholder="Search 1,000 payments…" />
        <DataTableViewOptions />
      </DataTableToolbar>
      <DataTableContent stickyHeader containerClassName="max-h-80" />
      <DataTablePagination />
    </DataTable>
  )
}
```

### Pinned columns

The selection column and the first data column are pinned by default. Choose your own with `pinStart`. A soft shadow marks the edge once the table scrolls sideways.

```tsx title="components/examples/data-table/pinned-columns.tsx"
"use client"

import { Badge, BadgeDot } from "@/components/ui/badge"
import {
  createDataTableColumns,
  DataTable,
  DataTableColumnHeader,
  DataTableContent,
  DataTablePagination,
  DataTableSearch,
  DataTableSelectAll,
  DataTableSelectRow,
  DataTableToolbar,
  DataTableViewOptions,
  useDataTable,
} from "@/components/ui/data-table"

type Payment = {
  id: string
  email: string
  status: "paid" | "pending" | "failed"
  amount: number
  method: string
  country: string
  date: string
}

const statuses = ["paid", "pending", "failed"] as const
const methods = ["Card", "PayPal", "Bank", "Apple Pay"]
const countries = ["United States", "Germany", "India", "Japan", "Brazil"]
const names = ["olivia", "jackson", "isabella", "william", "sofia", "liam"]

function makePayments(count: number): Payment[] {
  return Array.from({ length: count }, (_, index) => ({
    id: `pay_${String(index + 1).padStart(4, "0")}`,
    email: `${names[index % names.length]}.${Math.floor(index / names.length) + 1}@example.com`,
    status: statuses[(index * 7) % 3],
    amount: Math.round(((index * 7919) % 100000) / 10) / 10 + 5,
    method: methods[index % methods.length],
    country: countries[(index * 3) % countries.length],
    date: `2026-${String((index % 9) + 1).padStart(2, "0")}-${String((index % 27) + 1).padStart(2, "0")}`,
  }))
}

const payments = makePayments(12)

const currency = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD",
})

const statusVariant = {
  paid: "success",
  pending: "warning",
  failed: "destructive",
} as const

const helper = createDataTableColumns<Payment>()

const columns = helper.columns([
  helper.display({
    id: "select",
    header: ({ table }) => <DataTableSelectAll table={table} />,
    cell: ({ row }) => <DataTableSelectRow row={row} />,
    enableSorting: false,
    enableHiding: false,
  }),
  helper.accessor("email", {
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="Email" />
    ),
  }),
  helper.accessor("status", {
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="Status" />
    ),
    cell: ({ getValue }) => {
      const status = getValue()
      return (
        <Badge variant={statusVariant[status]}>
          <BadgeDot />
          <span className="capitalize">{status}</span>
        </Badge>
      )
    },
  }),
  helper.accessor("method", { header: "Method" }),
  helper.accessor("country", { header: "Country" }),
  helper.accessor("date", {
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="Date" />
    ),
  }),
  helper.accessor("amount", {
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="Amount" />
    ),
    cell: ({ getValue }) => currency.format(getValue()),
    meta: { align: "end" },
  }),
])

export function DataTablePinnedColumns() {
  const table = useDataTable({
    data: payments,
    columns,
    getRowId: (row) => row.id,
  })

  return (
    <div className="w-full max-w-md">
      <DataTable table={table}>
        <DataTableToolbar>
          <DataTableSearch placeholder="Search payments…" />
          <DataTableViewOptions />
        </DataTableToolbar>
        <DataTableContent pinStart={["select", "email", "status"]} />
        <DataTablePagination />
      </DataTable>
    </div>
  )
}
```

### Right to left

Every label and count can be replaced with `labels` and the format functions. Pagination arrows and pinned columns flip with the direction.

```tsx title="components/examples/data-table/rtl.tsx"
"use client"

import { Badge, BadgeDot } from "@/components/ui/badge"
import {
  createDataTableColumns,
  DataTable,
  DataTableColumnHeader,
  DataTableContent,
  DataTablePagination,
  DataTableSearch,
  DataTableSelectAll,
  DataTableSelectRow,
  DataTableToolbar,
  DataTableViewOptions,
  useDataTable,
} from "@/components/ui/data-table"

type Payment = {
  id: string
  email: string
  status: "paid" | "pending" | "failed"
  amount: number
  method: string
  country: string
  date: string
}

const statuses = ["paid", "pending", "failed"] as const
const methods = ["Card", "PayPal", "Bank", "Apple Pay"]
const countries = ["United States", "Germany", "India", "Japan", "Brazil"]
const names = ["olivia", "jackson", "isabella", "william", "sofia", "liam"]

function makePayments(count: number): Payment[] {
  return Array.from({ length: count }, (_, index) => ({
    id: `pay_${String(index + 1).padStart(4, "0")}`,
    email: `${names[index % names.length]}.${Math.floor(index / names.length) + 1}@example.com`,
    status: statuses[(index * 7) % 3],
    amount: Math.round(((index * 7919) % 100000) / 10) / 10 + 5,
    method: methods[index % methods.length],
    country: countries[(index * 3) % countries.length],
    date: `2026-${String((index % 9) + 1).padStart(2, "0")}-${String((index % 27) + 1).padStart(2, "0")}`,
  }))
}

const payments = makePayments(6)

const currency = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD",
})

const statusVariant = {
  paid: "success",
  pending: "warning",
  failed: "destructive",
} as const

const helper = createDataTableColumns<Payment>()

const columns = helper.columns([
  helper.display({
    id: "select",
    header: ({ table }) => <DataTableSelectAll table={table} />,
    cell: ({ row }) => <DataTableSelectRow row={row} />,
    enableSorting: false,
    enableHiding: false,
  }),
  helper.accessor("email", {
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="Email" />
    ),
  }),
  helper.accessor("status", {
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="Status" />
    ),
    cell: ({ getValue }) => {
      const status = getValue()
      return (
        <Badge variant={statusVariant[status]}>
          <BadgeDot />
          <span className="capitalize">{status}</span>
        </Badge>
      )
    },
  }),
  helper.accessor("method", { header: "Method" }),
  helper.accessor("country", { header: "Country" }),
  helper.accessor("date", {
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="Date" />
    ),
  }),
  helper.accessor("amount", {
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="Amount" />
    ),
    cell: ({ getValue }) => currency.format(getValue()),
    meta: { align: "end" },
  }),
])

const arabicNumber = new Intl.NumberFormat("ar")

export function DataTableRtl() {
  const table = useDataTable({
    data: payments,
    columns,
    getRowId: (row) => row.id,
  })

  return (
    <div dir="rtl" className="w-full">
      <DataTable table={table}>
        <DataTableToolbar>
          <DataTableSearch placeholder="ابحث في المدفوعات…" />
          <DataTableViewOptions label="عرض" groupLabel="إظهار الأعمدة" />
        </DataTableToolbar>
        <DataTableContent emptyMessage="لا توجد نتائج." />
        <DataTablePagination
          labels={{
            rowsPerPage: "صفوف في الصفحة",
            firstPage: "الصفحة الأولى",
            previousPage: "الصفحة السابقة",
            nextPage: "الصفحة التالية",
            lastPage: "الصفحة الأخيرة",
          }}
          formatSelection={(selected, total) =>
            `تم تحديد ${arabicNumber.format(selected)} من ${arabicNumber.format(total)}`
          }
          formatPage={(page, count) =>
            `صفحة ${arabicNumber.format(page)} من ${arabicNumber.format(count)}`
          }
        />
      </DataTable>
    </div>
  )
}
```

## Interactions

- Click a sortable header to sort ascending, again for descending, and a third time to clear. Shift-click another header to add a secondary sort.
- Shift-click a row checkbox to select every row between it and the last one you clicked. Hold Shift and drag across checkboxes to select or clear a range in one motion.
- Searching matches every column except the selection column and jumps back to the first page.
- Hiding columns from the View menu keeps the menu open, so you can toggle several at once.

## Keyboard

| Key | Action |
| --- | --- |
| `Tab` | Moves through the search, View menu, sortable headers, row checkboxes and pagination. |
| `Enter` `Space` | Sorts by the focused header. |
| `Space` | Toggles the focused checkbox. |
| `Esc` | Clears the search when it has text. |

## Accessibility

- Sortable headers carry `aria-sort`, and a polite live region announces the new sort and, shortly after typing, the number of results.
- The page indicator is a live region, so screen readers hear the new page after pressing next or previous.
- Checkboxes have labels by default. Override them with `aria-label` on `<DataTableSelectAll />` and `<DataTableSelectRow />`.

## API reference

Every part below has to be rendered inside `<DataTable />`, which shares the table with them.

### useDataTable

Takes the TanStack Table options and returns the table. Pages hold 10 rows unless `initialState.pagination` says otherwise.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `data` | `TData[]` | – |  |
| `columns` | `ColumnDef[]` | – | Build them with createDataTableColumns. |
| `getRowId` | `(row: TData) => string` | – | Keeps selection stable when rows move. Defaults to the row index. |
| `initialState` | `Partial<TableState>` | `{ pagination: { pageIndex: 0, pageSize: 10 } }` |  |
| `state` | `Partial<TableState>` | – | Control sorting, rowSelection, globalFilter, pagination or columnVisibility. |
| `onSortingChange` | `OnChangeFn<SortingState>` | – | Each controllable state has a matching handler, like onRowSelectionChange. |
| `enableRowSelection` | `boolean \| (row) => boolean` | `true` |  |

### createDataTableColumns

Returns a typed column helper with `accessor`, `display` and `columns`. Set `meta: { align: "end" }` on numeric columns to align the header and cells.

### DataTable

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `table` | `DataTableInstance<TData>` | – | The table returned by useDataTable. |
| `className` | `string` | – |  |

| Attribute | Description |
| --- | --- |
| `data-slot="data-table"` | The wrapper around every part. |
| `data-slot="data-table-announcer"` | The visually hidden live region. |

### DataTableContent

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `emptyMessage` | `ReactNode` | `"No results."` |  |
| `loading` | `boolean` | `false` |  |
| `loadingRows` | `number` | `5` | Number of skeleton rows while loading. |
| `pinStart` | `string[]` | `["select", firstColumnId]` | Column ids to pin to the start edge. |
| `stickyHeader` | `boolean` | `false` | Needs a max height on the container. |
| `containerClassName` | `string` | – | Applied to the scroll container. |
| `swipeSelect` | `boolean` | `true` | Shift-drag across checkboxes to select a range. |
| `className` | `string` | – | Applied to the table element. |

| Attribute | Description |
| --- | --- |
| `data-slot="table-container"` | The scroll container. |
| `data-scrolled-start` | Present on the container when it has scrolled away from the start edge. |
| `data-scrolled-end` | Present while there is more to scroll toward the end edge. |
| `data-scrolled-top` | Present once the rows scroll vertically. |
| `data-swipe-selecting` | Present on the container during a shift-drag. |
| `data-state="selected"` | Present on selected rows. |
| `data-row-id` | The row id from getRowId. |
| `data-slot="data-table-loading-row"` | Each skeleton row. |
| `data-slot="data-table-empty"` | The empty row. |

### DataTableToolbar

A wrapping row for the search, the View menu and your own filters. Accepts every `div` prop and carries `data-table-toolbar` as its `data-slot`.

### DataTableSearch

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `placeholder` | `string` | `"Search…"` |  |
| `aria-label` | `string` | `"Search table"` |  |
| `clearLabel` | `string` | `"Clear search"` | Accessible name of the clear button. |

| Attribute | Description |
| --- | --- |
| `data-slot="data-table-search"` | The search field wrapper. |

### DataTableViewOptions

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `label` | `string` | `"View"` |  |
| `groupLabel` | `string` | `"Toggle columns"` |  |
| `getLabel` | `(column) => string` | – | Defaults to the column’s string header, or its id capitalized. |

Lists every column that can hide. Set `enableHiding: false` on a column to leave it out.

| Attribute | Description |
| --- | --- |
| `data-slot="data-table-view-options"` | The menu popup. |

### DataTableColumnHeader

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `column` | `Column` | – |  |
| `title` | `string` | – |  |

Renders a sort button for sortable columns and plain text for the rest.

| Attribute | Description |
| --- | --- |
| `data-slot="data-table-column-header"` | The header wrapper. |
| `data-sorted` | Present on the sort button while the column is sorted. |
| `aria-sort` | On the header cell: ascending, descending or none. |

### DataTableSelectAll

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `table` | `Table` | – |  |
| `aria-label` | `string` | `"Select all rows on this page"` |  |

Selects the rows on the current page and shows an indeterminate state when only some are selected.

### DataTableSelectRow

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `row` | `Row` | – |  |
| `aria-label` | `string` | `"Select row"` |  |

### DataTablePagination

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `pageSizes` | `number[]` | `[10, 20, 50, 100]` |  |
| `showSelection` | `boolean` | `true` | Show the selected count instead of the row count. |
| `labels` | `Partial<DataTablePaginationLabels>` | – | rowsPerPage, firstPage, previousPage, nextPage and lastPage. |
| `formatSelection` | `(selected: number, total: number) => ReactNode` | `"2 of 42 rows selected"` |  |
| `formatRows` | `(total: number) => ReactNode` | `"42 rows"` |  |
| `formatPage` | `(page: number, pageCount: number) => ReactNode` | `"Page 1 of 5"` |  |

| Attribute | Description |
| --- | --- |
| `data-slot="data-table-pagination"` | The pagination bar. |

### useDataTableContext

Returns the table from the nearest `<DataTable />`. Use it to build your own toolbar controls, such as a status filter.

### Table

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `stickyHeader` | `boolean` | `false` | Pins the header row inside a scrolling container. |
| `containerClassName` | `string` | – | Applied to the scroll container. |
| `containerRef` | `Ref<HTMLDivElement>` | – |  |

| Attribute | Description |
| --- | --- |
| `data-slot="table"` | The table element. |
| `data-sticky-header` | Present on the container when stickyHeader is on. |
| `--table-bg` | Row and pinned-cell background. Follows the card or popover it sits in. |

### TableHead and TableCell

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `align` | `"start" \| "center" \| "end"` | `"start"` | End-aligned cells also use tabular numbers. |
| `pinned` | `"start" \| "end"` | – | Keeps the cell in place while the table scrolls sideways. Offset with --pin-offset. |
| `pinnedEdge` | `boolean` | `false` | Draws a soft shadow on the last pinned column while scrolled. |

| Attribute | Description |
| --- | --- |
| `data-align` | The current alignment. |
| `data-pinned` | start or end when pinned. |
| `data-pinned-edge` | Present on the last pinned cell of a side. |
| `--pin-offset` | Distance from the pinned edge, set for you. |

### Other table parts

`TableHeader`, `TableBody`, `TableFooter`, `TableRow` and `TableCaption` render the matching table elements and accept all of their props.

## Notes for AI assistants

- Install a component with the shadcn CLI: `npx shadcn@latest add https://hextaui.com/r/<name>.json`. It adds the source, the HextaUI theme tokens and any HextaUI components it depends on. `https://hextaui.com/r/all.json` installs every component.
- The code is then owned by the project, like shadcn/ui. There is no HextaUI npm package. HextaUI is MIT licensed and free for personal and commercial use.
- Behavior and accessibility come from Base UI (`@base-ui/react`). Compose with the `render` prop, not `asChild`.
- Styling uses Tailwind CSS v4 with theme tokens. Merge classes with `cn` from the `cn` package.
- Icons come from `@tabler/icons-react`.
- Import components from `@/components/ui/<name>`, hooks from `@/hooks/<name>` and utilities from `@/lib/<name>`.

Every HextaUI doc: https://hextaui.com/llms.txt
