What is a Tailwind table?
Nothing exotic. It’s a regular HTML data table where layout, borders, spacing and hover states come from utility classes instead of a custom stylesheet.
Since Tailwind is a utility-first CSS framework, each class sets one property, and a table gets built by stacking classes on plain elements. The usual elements all apply. The table is the container, caption gives it a visible title, thead, tbody and tfoot split the header, body and summary rows, and tr, th and td handle rows and cells.
Four utilities are specific to tables: border-collapse, border-spacing, table-layout and caption-side (Tailwind CSS v4.3 documentation). Everything else, from divide-y to the odd: and hover: variants, is a general utility that also works on rows and cells.
Bootstrap works the other way around. In Bootstrap tables, one table class switches on a default look. Tailwind makes you pick each piece yourself, which costs a few more classes but removes the overrides.
How to build a basic Tailwind table
The recipe is a wrapper div, w-full and a text size on the table, the same padding on every th and td, and divide-y with an explicit color on tbody. It behaves the same in Tailwind v3 and v4.
- Create a wrapper div with overflow-x-auto, rounded-lg, border and border-gray-200.
- Add the table with w-full, border-collapse, text-left and text-sm.
- Add a thead with bg-gray-50, and give each th scope=”col”, px-4, py-3 and font-semibold.
- Add a tbody with divide-y and divide-gray-200 so each row gets a separator.
- Give each td px-4 and py-3, matching the th padding so columns line up.
The browser default for border-collapse is separate (MDN), but Tailwind’s Preflight already sets border-collapse: collapse on table elements, so the class in step 2 mainly makes the intent explicit. It starts to matter if you disable Preflight or switch the table to border-separate elsewhere. Padding goes on th and td, because rows do not take padding in the default table display.
Skipping the wrapper is the mistake I see most on a first build. A wide table then pushes the whole page sideways instead of scrolling inside its own box.
Typing forty rows by hand gets old fast. An HTML table generator can produce the row and column skeleton, and the classes from the steps go on top.
Table layout utilities: table-auto, table-fixed and border-collapse
table-auto and table-fixed decide how columns get sized, while border-collapse and border-separate decide how borders are drawn.
| Utility | CSS output | Use when | Limit |
|---|
| table-auto | table-layout: auto | Columns should fit their content | Widths shift as cell text changes |
| table-fixed | table-layout: fixed | Widths are set once and must hold | Needs a table width; long content can overflow |
| border-collapse | border-collapse: collapse | Single-line borders and row dividers | border-radius does not apply to the table |
| border-separate | border-collapse: separate | Gaps between cells, rounded table corners | Gap is set with border-spacing-* |
Auto and fixed layout
table-auto sizes each column from its cell content. table-fixed ignores content and reads column widths from the first row, according to Tailwind’s table-layout documentation.
table-fixed has no effect on a table whose width is auto. MDN documents that fixed layout needs an explicit table width, and falls back to the automatic algorithm without one.
Tailwind’s own table-fixed example sets the class alone, so add w-full or another width class when you copy it.
Set widths on the th cells of the first row, such as w-1/2 or w-32. Columns without a width split the remaining space evenly.
Fixed layout renders faster and holds its shape, but later cells can outgrow their column (MDN). I’d use table-fixed for dashboards with predictable columns and keep table-auto for content tables where text length varies.
Both utilities take breakpoint prefixes, so table-auto md:table-fixed changes the algorithm at the md breakpoint.
Collapsed and separate borders
With border-collapse, adjacent cells share one border, so a row divider draws as a single line. border-separate gives every cell its own border, and the gap between them comes from border-spacing-*.
border-spacing-2, border-spacing-x-3 and border-spacing-y-1 only act on a table with separate borders (Tailwind border-spacing documentation). The x and y versions let you tune horizontal and vertical gaps independently.
If you already know how to style tables in plain CSS, nothing here is new. The utilities are the same table-layout, border-collapse and border-spacing properties with shorter names.
Styling rows, cells and borders
Stripes come from odd: and even: variants on tr, hover states from hover:, dividers from divide-y on tbody, and dark mode from dark: color pairs. Tailwind v4 also changed the default border color, which breaks tables that relied on it.
Striped and hoverable rows
Put the stripe classes on each tr: odd:bg-white even:bg-gray-50. Tailwind’s documentation shows this exact pattern for table rows, paired with dark:odd:bg-gray-900/50 and dark:even:bg-gray-950 for dark mode.
Row highlighting is a standard hover effect, written as hover:bg-gray-100 on the tr.
In v4, the hover variant only applies when the primary input device supports hover, through the @media (hover: hover) query (Tailwind v4 upgrade guide). On touch screens the row highlight never fires, so treat it as a bonus and rely on the stripes for readability.
Borders, corners and the v4 color default
Tailwind v4 changed the default color of border-* and divide-* utilities from gray-200 to currentColor.
A v3 table with a bare divide-y now draws text-colored lines after the upgrade. Name the color on every border and divide class, or add the upgrade guide’s base-layer rule that sets border-color to gray-200 for all elements.
Check the browser floor before upgrading. v4 targets Safari 16.4, Chrome 111 and Firefox 128 (Tailwind upgrade guide), so projects that must support older browsers stay on v3.4.
Rounded corners need a decision. MDN states that border-radius does not apply to table elements when border-collapse is collapse.
Keep border-collapse and clip the corners with rounded-lg on a wrapper, as the wrapper in the build steps does. The other route is switching the table to border-separate with border-spacing-0.
Dark mode color pairs
The dark: variant follows prefers-color-scheme by default. To drive it from a class or data attribute, override it with a @custom-variant dark rule (Tailwind dark mode documentation).
- text-gray-900 with dark:text-gray-100 for body text
- divide-gray-200 with dark:divide-gray-700 for the row lines
- bg-gray-50 with dark:bg-gray-800 on the header
- odd:bg-white even:bg-gray-50 plus dark:odd:bg-gray-900/50 and dark:even:bg-gray-950 for stripes
Check each stripe against its text color. A dark stripe at 50% opacity shifts with whatever sits behind the table.
How to make a Tailwind table responsive
A wide table on a phone can scroll sideways or lose a few low-priority columns. A third option, restacking each row as a card, replaces the table layout entirely and carries the most accessibility risk. Scrolling leaves everything intact.
Choose by how much of the table a reader needs at once. This is a responsive design decision more than a styling one.
Horizontal scroll wrapper
A wrapper with overflow-x-auto only scrolls once the table is wider than it is. Add whitespace-nowrap to the cells and min-w-full to the table so columns keep their natural width instead of squeezing.
HyperUI’s table snippets use exactly that pairing inside the wrapper.
Every column and row relationship stays intact, and native table semantics are untouched. The weak spot is discoverability, since nothing tells the reader more columns exist. Leave a column cut off at the right edge, or add a shadow, as the cue.
Hiding columns by breakpoint
Put hidden on a cell and md:table-cell next to it to bring the column back at wider screens. The md variant starts at 48rem, because Tailwind’s breakpoints are min-width media queries (Tailwind variants reference).
The remaining columns get room and nothing scrolls sideways. The price is that hidden data is gone on small screens, so fold it into a visible cell or a detail view.
Apply the classes to the th and to every td in that column together. Hiding only the data cells breaks the column alignment.
Stacked cards on small screens
Stacking sets block on tr and td, hides the thead below md, and prints each label from a data-label attribute with before:content-[attr(data-label)].
On a phone it reads like a list, and nobody needs a sideways gesture. But every row repeats its labels, wide numeric comparisons across rows get hard to scan, and a hidden thead with generated labels can leave screen reader users without column context.
Setting display to block, grid or flex on table elements made browsers drop the native table semantics in Adrian Roselli’s 2018 testing. His own reading of the specs is that dropping them was a browser bug rather than intended behavior. In his October 2023 update, tables with display properties applied work in Chromium (since Chrome 80), Gecko and WebKit (since Safari 17), and only display: contents is still problematic. Test the final markup with a screen reader anyway, because a hidden thead and generated labels are not covered by that fix.
Sticky headers in a Tailwind table
Put sticky and top-0 on the thead, give it a background, and cap the wrapper height so the table scrolls inside its own box. Each of those parts fails in its own way, which is why sticky headers have a reputation for breaking.
| Symptom | Cause | Fix |
|---|
| Header scrolls away | Wrapper has no height limit | Add max-h-96 and overflow-auto to the wrapper |
| Rows show through the header | thead has no background | Add bg-white and dark:bg-gray-900 to the thead |
| Cell borders vanish on the header | Collapsed borders belong to the table, not the cell | Draw the line on the wrapper, or use border-separate with border-spacing-0 |
| A 1px strip of the next row shows above the header | The shared border sits outside the sticky box | Offset top by the border width (-top-px) and clip with overflow-hidden |
HyperUI’s sticky-heading snippet covers the first two rows: a max-height wrapper, sticky top-0 on the thead, and an explicit background with a dark variant.
The last two rows trace back to the same cause, and both show up in public bug trackers. Mozilla’s tracker lists borders disappearing on sticky th cells in a collapsed-border table (Bug 1727594, reported on Firefox 91 and still open and unconfirmed as of October 2026). A Mozilla engineer noted that all browsers behave the same on the test case, so no change is planned unless the spec changes. Bootstrap’s table-sticky-headers branch has a commit titled “close the sticky header 1px gap” that closed the strip with a top offset, because top-0 cannot cover a border the cells share.
daisyUI skips the debugging. Its table-pin-rows modifier pins the thead and tfoot rows, table-pin-cols pins the th columns, and the docs example sits in a fixed-height wrapper.
I prefer a borderless sticky header on a tinted background, since it avoids the whole class of border bugs in the table above.
Making a Tailwind table accessible
Give the table a caption, use th for header cells, keep the scroll wrapper reachable by keyboard, and avoid display overrides on table elements. Tailwind sets none of this for you, because the utilities style markup and the semantics come from the elements you write.
A caption element can be shown with caption-top or hidden with sr-only. Header cells should be th, with scope added when headers run along both rows and columns. Empty header cells get sr-only text, as Flowbite’s markup does for its “Edit” column.
W3C WAI describes the caption as the primary way assistive technology users identify a table in table-browsing mode.
WCAG technique H63 says th elements alone are enough for simple tables with headers in the first row or column. Scope starts to matter when a second header axis appears.
Captions and header markup are the core of accessible tables. The summary attribute is obsolete in the HTML Living Standard (WCAG technique H73), so describe a long table in the caption or a nearby paragraph.
From Chrome 132, scroll containers are keyboard focusable by default when they hold no focusable children (Chrome for Developers, which first announced the change for Chrome 130).
Rows with links or buttons change that. The browser skips the wrapper and focuses the first control, and Chrome’s guidance is to set tabindex=”0″ when the container itself should take focus. Before Chrome 132, the wrapper needed tabindex=”0″ to be reachable at all.
Pair it with focus-visible:outline-2 on the wrapper, so only keyboard users see the focus ring.
Stacked-card layouts set block on table elements, which stripped table semantics in Roselli’s 2018 testing. ARIA roles such as role=”cell” were the workaround, but he found they can be problematic once header cells are hidden. Current browsers keep table semantics for most display values, and display: contents is the one he still flags, so the remaining risk is a hidden thead and generated labels. If screen reader users matter for the page, scroll the table instead.
Which fits: a plain table, a component kit or a headless library?
A plain Tailwind table suits static or lightly interactive data. A component kit gets you a finished look fast, and a headless library earns its place once sorting, filtering or pagination state is needed. The markup stays the same across all of them. What changes is who writes the logic.
Choosing the approach
| Option | Table logic | Markup and styles | Fits when |
|---|
| Plain Tailwind table | None | You write both | Static or lightly interactive data |
| Component kit (HyperUI, Flowbite, daisyUI) | None, or the kit’s JavaScript | Prebuilt classes | A finished look in minutes |
| Headless library (TanStack Table) | Sorting, filtering, pagination, selection | You write both | Tables users can change |
| Grid library (AG Grid) | Full | Ready-made, theme-based | Large grids with little design control |
Start plain. A kit’s styling is ordinary utility classes you could write yourself, and what you take on is its JavaScript. Flowbite marks its search and checkbox-selection examples as requiring Flowbite JS.
A Tailwind UI kit still earns its place when a team wants tables, buttons and forms from one consistent source.
Component sources compared
Ranked for a static Tailwind table, by how little you need to adopt:
- HyperUI ships copy-paste snippets under the MIT license, with sticky-heading and sticky-first-column variants and plain markup with no scripts.
- daisyUI gives you a table class with modifiers for zebra stripes, pinned rows and columns, and five sizes from table-xs to table-xl.
- Flowbite has striped, hover, caption, overflow and sortable-header examples, with a Pro version above the free tier.
- Preline UI comes under the MIT license plus the Preline UI Fair Use License, with a DataTable plugin and a paid Pro tier for premium blocks.
- Material Tailwind offers MIT-licensed HTML and React packages with striped-row and striped-column table examples.
Tailwind Plus is the exception. The official paid kit, formerly Tailwind UI, closed to new sign-ups when Tailwind Labs joined Shopify on September 9, 2026, and existing customers keep access (Tailwind Labs announcement).
Roundups that still present it as purchasable are out of date, and tailwindcss.com/plus resolved to a login form when checked on October 29, 2026. Tailwind CSS itself stays MIT-licensed.
Headless logic with TanStack Table
TanStack Table supplies sorting, filtering, pagination and row selection as state and functions, and it renders no markup or styles. The Tailwind classes stay yours.
The shadcn/ui route has no data-table component to install, because every data table behaves differently. The guide wraps TanStack Table around a Table component and styles it with utilities such as overflow-hidden rounded-md border.
Pagination controls are separate Tailwind pagination markup, driven by the page state the library exposes.
Versions moved recently. TanStack Table v9 left beta with 9.0.0 on August 4, 2026 (npm registry), and shadcn/ui’s data-table guide builds on v9, while many tutorials still show v8 code. Pin the version you install and read the matching docs.
TanStack’s own overview names AG Grid as the pre-built alternative, where you style the result from the outside through props and theme APIs. A headless library costs setup instead, since it ships no markup, styles or themes.
Add TanStack Table when users can change what the table shows. Keep a plain table when the page only displays the data.
Tailwind table FAQ
How do I right-align numbers in a Tailwind table?
Put text-right on the th and on every td in that column, and add tabular-nums so each digit takes the same width. Tailwind’s font-variant-numeric documentation pairs text-right with slashed-zero and tabular-nums in a subtotal, tax and total list.
Use text-center the same way when a column should be centered.
Can I use a Tailwind table in React or Vue?
Yes. The markup and classes are identical, with React writing className where Vue and plain HTML write class.
For sorting and pagination, TanStack Table supports both frameworks, and Flowbite publishes integration guides for each.
Can div elements replace table tags in a Tailwind table?
Visually, yes. Preline’s table docs show table display utilities on div elements for a table-like layout without semantic table tags.
A div gains no table semantics from its display value, according to Adrian Roselli, so keep real table elements for data.
Does the Tailwind Typography prose class style tables?
Yes. The plugin’s prose class styles table, thead, tr, th and td, and modifiers such as prose-table:{utility} and prose-th:{utility} adjust them.
Wrap a hand-styled table in not-prose, the plugin’s sandbox class, so prose styles do not compete with your utilities.
How do I make the first column sticky in a Tailwind table?
Give the first cell of every row sticky, left-0 and a background, inside an overflow-x-auto wrapper. HyperUI’s snippet sets *:first:sticky, *:first:left-0 and *:first:bg-white on each tr.
Its dark variant adds dark:*:first:bg-gray-900, so scrolling cells do not show through the pinned column.
Reusing Tailwind Table Styles Across Pages
A Tailwind table belongs in one component or template partial once a second file needs it. Tailwind’s documentation names that as the best strategy for styles shared across multiple files.
Work down this order and stop at the first step that removes the duplication.
- Render rows in a loop, so the class list exists once
- Edit repeated cells together with multi-cursor editing inside one file
- Extract a component or partial for use across files
A custom table class in the components layer falls short. Tailwind recommends partials for anything beyond a single HTML element, and a table spans table, thead, tbody, th and td.
The trade-off is flexibility. Expose props instead of letting call sites add classes, because a clash between two utilities resolves by stylesheet order, not class attribute order.
Position verified against the Tailwind CSS v4.3 documentation on October 29, 2026.