HextaUI

Data table: 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.

MethodCountry
[email protected]paidCardUnited States2026-01-01$5.00
[email protected]pendingPayPalJapan2026-02-02$84.20
[email protected]failedBankGermany2026-03-03$163.40
[email protected]paidApple PayBrazil2026-04-04$242.60
[email protected]pendingCardIndia2026-05-05$321.80
[email protected]failedPayPalUnited States2026-06-06$401.00
[email protected]paidBankJapan2026-07-07$480.10
[email protected]pendingApple PayGermany2026-08-08$559.30
[email protected]failedCardBrazil2026-09-09$638.50
[email protected]paidPayPalIndia2026-01-10$717.70
[email protected]pendingBankUnited States2026-02-11$796.90
[email protected]failedApple PayJapan2026-03-12$876.10
[email protected]paidCardGermany2026-04-13$955.30
[email protected]pendingPayPalBrazil2026-05-14$34.50
[email protected]failedBankIndia2026-06-15$113.70
[email protected]paidApple PayUnited States2026-07-16$192.90
[email protected]pendingCardJapan2026-08-17$272.00
[email protected]failedPayPalGermany2026-09-18$351.20
[email protected]paidBankBrazil2026-01-19$430.40
[email protected]pendingApple PayIndia2026-02-20$509.60
[email protected]failedCardUnited States2026-03-21$588.80
[email protected]paidPayPalJapan2026-04-22$668.00
[email protected]pendingBankGermany2026-05-23$747.20
[email protected]failedApple PayBrazil2026-06-24$826.40
[email protected]paidCardIndia2026-07-25$905.60
[email protected]pendingPayPalUnited States2026-08-26$984.80
[email protected]failedBankJapan2026-09-27$63.90
[email protected]paidApple PayGermany2026-01-01$143.10
[email protected]pendingCardBrazil2026-02-02$222.30
[email protected]failedPayPalIndia2026-03-03$301.50
[email protected]paidBankUnited States2026-04-04$380.70
[email protected]pendingApple PayJapan2026-05-05$459.90
[email protected]failedCardGermany2026-06-06$539.10
[email protected]paidPayPalBrazil2026-07-07$618.30
[email protected]pendingBankIndia2026-08-08$697.50
[email protected]failedApple PayUnited States2026-09-09$776.70
[email protected]paidCardJapan2026-01-10$855.80
[email protected]pendingPayPalGermany2026-02-11$935.00
[email protected]failedBankBrazil2026-03-12$14.20
[email protected]paidApple PayIndia2026-04-13$93.40
[email protected]pendingCardUnited States2026-05-14$172.60
[email protected]failedPayPalJapan2026-06-15$251.80
[email protected]paidBankGermany2026-07-16$331.00
[email protected]pendingApple PayBrazil2026-08-17$410.20
[email protected]failedCardIndia2026-09-18$489.40
[email protected]paidPayPalUnited States2026-01-19$568.60
[email protected]pendingBankJapan2026-02-20$647.70
[email protected]failedApple PayGermany2026-03-21$726.90
[email protected]paidCardBrazil2026-04-22$806.10
[email protected]pendingPayPalIndia2026-05-23$885.30
[email protected]failedBankUnited States2026-06-24$964.50
[email protected]paidApple PayJapan2026-07-25$43.70
[email protected]pendingCardGermany2026-08-26$122.90
[email protected]failedPayPalBrazil2026-09-27$202.10
[email protected]paidBankIndia2026-01-01$281.30
[email protected]pendingApple PayUnited States2026-02-02$360.50
[email protected]failedCardJapan2026-03-03$439.60
[email protected]paidPayPalGermany2026-04-04$518.80
[email protected]pendingBankBrazil2026-05-05$598.00
[email protected]failedApple PayIndia2026-06-06$677.20
[email protected]paidCardUnited States2026-07-07$756.40
[email protected]pendingPayPalJapan2026-08-08$835.60
[email protected]failedBankGermany2026-09-09$914.80
[email protected]paidApple PayBrazil2026-01-10$994.00
[email protected]pendingCardIndia2026-02-11$73.20
[email protected]failedPayPalUnited States2026-03-12$152.40
[email protected]paidBankJapan2026-04-13$231.50
[email protected]pendingApple PayGermany2026-05-14$310.70
[email protected]failedCardBrazil2026-06-15$389.90
[email protected]paidPayPalIndia2026-07-16$469.10
[email protected]pendingBankUnited States2026-08-17$548.30
[email protected]failedApple PayJapan2026-09-18$627.50
[email protected]paidCardGermany2026-01-19$706.70
[email protected]pendingPayPalBrazil2026-02-20$785.90
[email protected]failedBankIndia2026-03-21$865.10
[email protected]paidApple PayUnited States2026-04-22$944.30
[email protected]pendingCardJapan2026-05-23$23.40
[email protected]failedPayPalGermany2026-06-24$102.60
[email protected]paidBankBrazil2026-07-25$181.80
[email protected]pendingApple PayIndia2026-08-26$261.00
[email protected]failedCardUnited States2026-09-27$340.20
[email protected]paidPayPalJapan2026-01-01$419.40
[email protected]pendingBankGermany2026-02-02$498.60
[email protected]failedApple PayBrazil2026-03-03$577.80
[email protected]paidCardIndia2026-04-04$657.00
[email protected]pendingPayPalUnited States2026-05-05$736.20
[email protected]failedBankJapan2026-06-06$815.30
[email protected]paidApple PayGermany2026-07-07$894.50
[email protected]pendingCardBrazil2026-08-08$973.70
[email protected]failedPayPalIndia2026-09-09$52.90
[email protected]paidBankUnited States2026-01-10$132.10
[email protected]pendingApple PayJapan2026-02-11$211.30
[email protected]failedCardGermany2026-03-12$290.50
[email protected]paidPayPalBrazil2026-04-13$369.70
[email protected]pendingBankIndia2026-05-14$448.90
[email protected]failedApple PayUnited States2026-06-15$528.10
[email protected]paidCardJapan2026-07-16$607.20
[email protected]pendingPayPalGermany2026-08-17$686.40
[email protected]failedBankBrazil2026-09-18$765.60
[email protected]paidApple PayIndia2026-01-19$844.80
0 of 1,000 rows selected
Page 1 of 10
"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>
  )
}

This adds the example as components/examples/data-table/sticky-header.tsx, together with the Data table component and the HextaUI theme tokens it needs.

pnpm dlx shadcn@latest add https://hextaui.com/r/data-table-sticky-header.json

To add only the component, follow the Data table installation.

Props in this example

The Data table props this example sets, from the API reference.

PropTypeDefault
tableThe table returned by useDataTable.
DataTableInstance<TData>–
PropTypeDefault
stickyHeaderNeeds a max height on the container.
booleanfalse
containerClassNameApplied to the scroll container.
string–
PropTypeDefault
placeholder
string"Search…"
PropTypeDefault
column
Column–
title
string–
PropTypeDefault
table
Table–
PropTypeDefault
row
Row–

Every prop is in the Data table API reference.

See them all, with keyboard support and accessibility notes, on the Data table page.