Skip to content
TopClass UI

Table

A real HTML table rendered on the server: sorting is a link per header, the first column can stick, and it ships no JavaScript.

split - size not measured yet

Table

split - size not measured yet

A real HTML table rendered on the server: sorting is a link per header, the first column can stick, and it ships no JavaScript.

Install
Quarterly revenue by region
RegionQ1Q2Q3Q4Full year
Middle East$412,000$455,300$498,120$530,900$1,896,320
North Africa$208,400$214,950$240,010$262,700$926,060
Western Europe$890,150$874,600$910,480$955,230$3,630,460
North America$1,204,800$1,260,340$1,315,900$1,402,650$5,183,690

Try it

captionVisible
striped
stickyFirstColumn
visibleRows
loadMoreThreshold

The code

table.tsx
import { Table, type TableDesignProps } from 'topclassui/table'

<Table

  caption="Quarterly revenue by region"
  items={regions}
  getKey={(region) => region.name}
  columns={[
    { key: 'name', header: 'Region', rowHeader: true, cell: (region) => region.name },
    { key: 'q1', header: 'Q1', align: 'end', cell: (region) => region.q1 },
    { key: 'q2', header: 'Q2', align: 'end', cell: (region) => region.q2 },
    { key: 'q3', header: 'Q3', align: 'end', cell: (region) => region.q3 },
    { key: 'q4', header: 'Q4', align: 'end', cell: (region) => region.q4 },
    { key: 'total', header: 'Full year', align: 'end', cell: (region) => region.total },
  ]}
/>

Sorting is a link the server answers with the rows in their new order, so a sorted table costs no JavaScript.

Every state

7 states
Deploys
DeploySort by BranchAuthorSort by Duration
dpl_7f3amainLayla Haddad48 s
dpl_7f39fix/checkout-totalOmar Abed51 s
dpl_7f38mainSara Nasser47 s
Deploys, caption drawn
DeploySort by BranchAuthorSort by Duration
dpl_7f3amainLayla Haddad48 s
dpl_7f39fix/checkout-totalOmar Abed51 s
dpl_7f38mainSara Nasser47 s
Deploys, striped
DeploySort by BranchAuthorSort by Duration
dpl_7f3amainLayla Haddad48 s
dpl_7f39fix/checkout-totalOmar Abed51 s
dpl_7f38mainSara Nasser47 s
Deploys, slowest first
DeploySort by BranchAuthorSort by Duration
dpl_7f3amainLayla Haddad48 s
dpl_7f39fix/checkout-totalOmar Abed51 s
dpl_7f38mainSara Nasser47 s
Deploys, first column stuck
DeploySort by BranchAuthorSort by Duration
dpl_7f3amainLayla Haddad48 s
dpl_7f39fix/checkout-totalOmar Abed51 s
dpl_7f38mainSara Nasser47 s
Deploys, none yet
DeploySort by BranchAuthorSort by Duration
Nothing here yet

Import from the package

topclassui/table
import { Table } from 'topclassui/table'

Install once:npm i topclassui@beta zod

Examples

  • caption is required, in real words: it is the only name of the table. It is read out but not drawn; captionVisible draws it.
  • A column with sortHref makes its header a link. Sort the rows on the server and pass sort to mark the column; in Next.js pass link={<Link href="" />}.
  • Table renders the items it is given, already sorted and paged; Pagination moves between pages.
  • Cells stay on one line, so a narrow screen scrolls the table sideways instead of stacking words. A column of prose opts out with its own className.
  • stickyFirstColumn keeps the first column in view, cut with an ellipsis past --tc-table-sticky-width.
  • columns and getKey hold functions, so build the columns where you render the table, not in a Server Component that passes them to a Client Component.
basic
Invoices
InvoiceCustomerStatusAmountOpen
INV-1042Northwind TradersPaid$1,250.00View
INV-1043Contoso LtdDue in 3 days$480.00View
INV-1044Fabrikam IncOverdue$9,120.50View
INV-1045Adventure WorksPaid$76.00View
sorted
Customers, by name
Sort by CustomerPlanSort by Seats
Adventure WorksTeam12
Contoso LtdEnterprise240
Fabrikam IncFree1
Northwind TradersTeam38
Customers, by seats
Sort by CustomerPlanSort by Seats
Contoso LtdEnterprise240
Northwind TradersTeam38
Adventure WorksTeam12
Fabrikam IncFree1
sticky-first-column
Revenue by region
RegionReportSort by Q1Sort by Q2Sort by Q3Sort by Q4Sort by Full year
Middle East$412,000$455,300$498,120$530,900$1,896,320
North Africa$208,400$214,950$240,010$262,700$926,060
Western Europe$890,150$874,600$910,480$955,230$3,630,460
North America$1,204,800$1,260,340$1,315,900$1,402,650$5,183,690
Revenue by region, striped
RegionReportSort by Q1Sort by Q2Sort by Q3Sort by Q4Sort by Full year
Middle East$412,000$455,300$498,120$530,900$1,896,320
North Africa$208,400$214,950$240,010$262,700$926,060
Western Europe$890,150$874,600$910,480$955,230$3,630,460
North America$1,204,800$1,260,340$1,315,900$1,402,650$5,183,690
row-actions
Orders
Sort by OrderSort by CustomerStatusSort by PlacedSort by TotalActions
#1042Northwind TradersShipped28 Mar 2026$120.00
#1041Contoso LtdAwaiting payment27 Mar 2026$240.00
#1040Fabrikam IncDelivered26 Mar 2026$360.00
#1039Adventure WorksShipped25 Mar 2026$480.00
#1038Tailspin ToysAwaiting payment24 Mar 2026$600.00
#1037Northwind TradersDelivered23 Mar 2026$720.00
#1036Contoso LtdShipped22 Mar 2026$840.00
#1035Fabrikam IncAwaiting payment21 Mar 2026$960.00
#1034Adventure WorksDelivered20 Mar 2026$1080.00
#1033Tailspin ToysShipped19 Mar 2026$1200.00
#1032Northwind TradersAwaiting payment18 Mar 2026$1320.00
#1031Contoso LtdDelivered17 Mar 2026$1440.00
#1030Fabrikam IncShipped16 Mar 2026$1560.00
#1029Adventure WorksAwaiting payment15 Mar 2026$1680.00
#1028Tailspin ToysDelivered14 Mar 2026$1800.00
#1027Northwind TradersShipped13 Mar 2026$1920.00
#1026Contoso LtdAwaiting payment12 Mar 2026$2040.00
#1025Fabrikam IncDelivered11 Mar 2026$2160.00
#1024Adventure WorksShipped10 Mar 2026$2280.00
#1023Tailspin ToysAwaiting payment9 Mar 2026$2400.00
#1022Northwind TradersDelivered8 Mar 2026$2520.00
#1021Contoso LtdShipped7 Mar 2026$2640.00
#1020Fabrikam IncAwaiting payment6 Mar 2026$2760.00
#1019Adventure WorksDelivered5 Mar 2026$2880.00
#1018Tailspin ToysShipped4 Mar 2026$3000.00
striped
Recent deploys
DeployBranchAuthorDuration
dpl_7f3amainLayla Haddad48 s
dpl_7f39fix/checkout-totalOmar Abed51 s
dpl_7f38mainSara Nasser47 s
dpl_7f37feat/tableOmar Abed1 min 12 s
dpl_7f36mainLayla Haddad46 s
dpl_7f35chore/depsYousef Karim2 min 3 s
caption
API keys
NameKeyCreatedLast used
Productionkey_live_a81f12 Mar 20262 minutes ago
Stagingkey_test_33c04 Jan 2026Yesterday
Local developmentkey_test_9d2e19 Nov 2025Never
column-classes
Open tickets
TicketSubjectLast noteReplies
T-2041Invoice total is wrongThe customer was charged twice for the March renewal. Finance has refunded one payment and is waiting for the bank.14
T-2038Cannot invite a memberThe invitation email never arrives at addresses on the company domain.3
T-2030Export is emptyFixed in the last release. Waiting for the customer to confirm.120
empty
Pending invitations
NameRoleMember number
Nothing here yet
Team members
NameRoleMember number

Nobody has joined this team yet.

Invite a member
skeleton
Latest deploys
DeployBranchAuthorDuration
dpl_7f3amainLayla Haddad48 s
dpl_7f39fix/checkout-totalOmar Abed51 s
dpl_7f38mainSara Nasser47 s
dpl_7f37feat/tableOmar Abed1 min 12 s
in-a-layout

Recent orders

Recent orders
OrderCustomerPlacedStatusItemsTotal
ORD-5521Northwind Traders12 Mar 2026Awaiting payment14$1,250.00
ORD-5520Contoso Ltd11 Mar 2026Shipped3$480.00
ORD-5519Fabrikam Inc11 Mar 2026Awaiting payment120$9,120.50
ORD-5518Adventure Works9 Mar 2026Delivered1$76.00

This week

128 orders and 4 refunds.

Orders awaiting payment
OrderCustomerPlacedStatusItemsTotal
ORD-5521Northwind Traders12 Mar 2026Awaiting payment14$1,250.00
ORD-5519Fabrikam Inc11 Mar 2026Awaiting payment120$9,120.50
In Arabic, right to left
الفواتير
ترتيب حسب الفاتورةترتيب حسب العميلالحالةتاريخ الاستحقاقترتيب حسب المبلغ
فاتورة-١٠٤٢شركة الشمال للتجارةمدفوعة١٢ مارس ٢٠٢٦١٬٢٥٠٫٠٠ د.أ
فاتورة-١٠٤٣مؤسسة النخيلتستحق بعد ٣ أيام١٨ مارس ٢٠٢٦٤٨٠٫٠٠ د.أ
فاتورة-١٠٤٤دار الياسمين للنشرمتأخرة١ مارس ٢٠٢٦٩٬١٢٠٫٥٠ د.أ
فاتورة-١٠٤٥مخبز الأملمدفوعة٢٥ فبراير ٢٠٢٦٧٦٫٠٠ د.أ
الفواتير في مساحة ضيقة
ترتيب حسب الفاتورةترتيب حسب العميلالحالةتاريخ الاستحقاقترتيب حسب المبلغ
فاتورة-١٠٤٢شركة الشمال للتجارةمدفوعة١٢ مارس ٢٠٢٦١٬٢٥٠٫٠٠ د.أ
فاتورة-١٠٤٣مؤسسة النخيلتستحق بعد ٣ أيام١٨ مارس ٢٠٢٦٤٨٠٫٠٠ د.أ
فاتورة-١٠٤٤دار الياسمين للنشرمتأخرة١ مارس ٢٠٢٦٩٬١٢٠٫٥٠ د.أ
فاتورة-١٠٤٥مخبز الأملمدفوعة٢٥ فبراير ٢٠٢٦٧٦٫٠٠ د.أ
فواتير الشهر القادم
ترتيب حسب الفاتورةترتيب حسب العميلالحالةتاريخ الاستحقاقترتيب حسب المبلغ
لا يوجد شيء بعد

Props

The props of Table
PropTypeDefaultLimitsDescription
caption*NonEmpty<Caption>--What the table holds, in real words. It is the only name of the table and of its scrolling region.
columns*TableColumns<T>--The columns, in reading order. A false, null or undefined entry is skipped, so cond && {...} hides one.
getKey*(item: T) => string--A stable identity for a row, as the shared collection contract has it. It becomes the React key.
items*readonly T[]--The rows, already sorted and paged: Table renders what it is given.
captionVisiblebooleanfalse-Draws the caption above the header row; when false it is still read out, only not drawn.
emptyFallbackReactNode--Shown in one row across every column when there are no items. null also gives labels.state.empty.
hasMoreboolean--
labelsLabels--The built-in strings: the empty row and the words of the sort links. Defaults to English.
linkReactElement--The element every sort link is cloned from, such as link={<Link href="" />}; its href is replaced with the column's sortHref.
loadMore(() => void | Promise<void>)--Client-only, or a Server Action. Called once when the window nears the end.
loadMoreThresholdinteger, min 0, max 20010min 0, max 200Rows from the end at which loadMore is called, clamped to 0 to 200.
localestring--Accepted so {...getLabels(locale)} spreads cleanly. Nothing here formats a number, so it is not used and never reaches the element.
sortTableSort--The column the rows are sorted by, which gets aria-sort and a caret.
stickyFirstColumnbooleanfalse-Keeps the first column in view while the others scroll sideways under it. It is cut with an ellipsis past --tc-table-sticky-width.
stripedbooleanfalse-Tints every second row, so the eye can follow a wide row across.
virtualizeunknown"auto"-Windowing: auto mounts only the rows in view above the provider threshold (200); false keeps every row in the HTML.
visibleRowsinteger, min 1, max 207min 1, max 20Body rows the box shows before it scrolls under its sticky header, clamped to 1 to 20; left out, the --tc-list-rows knob.

Keyboard and accessibility

The keyboard of Table
KeysWhat it does
TabReaches the scrolling box, then each sort link and each control in the cells.
ArrowLeftArrowRightScroll the focused box sideways.
EnterFollows a sort link.
  • A real <table>: a screen reader moves through it by row and by column. Mark the column that identifies a row with rowHeader: true.
  • The sorted column carries aria-sort, and its caret says the direction in words, because aria-sort is not read out everywhere.
  • Name a row action by its row, such as "Refund order 1042", never a bare "Refund" repeated on every row.

Right to left

The first column sticks to the right edge, an end-aligned amount sits on the left, and the built-in words come from labels.

Server and client

Renders on the server; the interactive part is a client component that ships only when it is used.

Files: index.tsx, client.tsx.

On the chip: split - size not measured yet.