What are Bootstrap tables?
Everything starts from .table, and every modifier stacks on top of it. The opt-in design is deliberate. The Bootstrap 5.3 documentation (v5.3.8 at the time of writing) points out that tables show up inside third-party widgets such as calendars and date pickers, so the Bootstrap framework leaves unclassed tables alone.
An unclassed table still picks up Reboot, the framework’s baseline layer. Reboot styles captions, collapses borders and sets a consistent text alignment, and that’s all it does.
Padding, dividers and every visual state come from classes. Skip them and you are back to writing table CSS by hand.
At minimum you want a table element with .table on it, a thead whose th scope=”col” cells label the columns, and a tbody. Where a row has its own label, use th scope=”row” for it. A tfoot is optional, handy for totals.
With .table applied, cells get .5rem of padding and light horizontal dividers.
Markup is where a lot of sites slip. WebAIM’s 2026 Million report counted 948,225 tables across 143,575 home pages, and only 19% had valid data table markup.
The classes won’t repair that. Bootstrap’s accessibility docs say a project’s accessibility depends largely on the author’s markup, which makes the scope attributes above worth the typing.
Table style classes: stripes, hover, borders and size
Most styling comes down to a handful of classes you add next to .table: striped rows, striped columns, hover, borders and compact size.
| Class | Effect | Targets | Default detail |
|---|
| .table-striped | Zebra stripes | tbody rows | Odd rows shaded |
| .table-striped-columns | Zebra stripes | Columns | Even columns shaded |
| .table-hover | Hover state | tbody rows | Works with stripes |
| .table-bordered / .table-borderless | Adds or removes borders | Table and cells | Recolor with border utilities |
| .table-sm | Compact spacing | All cells | Padding cut from .5rem to .25rem |
Stacking modifiers
Modifiers combine freely. The Bootstrap docs show .table-striped next to .table-hover, .table-dark with either .table-striped or .table-striped-columns, and .table-success with .table-striped-columns.
.table-active is the odd one out. It highlights one row or one cell, so it goes on a tr, td or th instead of the table.
Choosing between them
Stripes earn their place on long tables, where the eye loses its row. On short ones they just add noise, so I’d skip them.
For dense data, .table-sm with .table-borderless fits more rows on screen.
Wide numeric grids are different. .table-bordered earns its heavier look when readers track values across many columns.
How do contextual colors work on Bootstrap tables?
Contextual colors use the .table- prefix, as in .table-danger, and the names are primary, secondary, success, danger, warning, info, light and dark.
Put .table-primary on the table element and every cell picks it up. On a tr, .table-warning colors that row only. And .table-danger on a td or th colors a single cell.
Head and foot colors
.table-light or .table-dark on the thead turns the header row light or dark gray. In Bootstrap 4 that job belonged to .thead-light and .thead-dark, which Bootstrap 5.0.0 dropped.
The variants also work on tfoot, so a totals row can carry its own color.
Where colored tables fall short
Dark mode catches people off guard. Per the Bootstrap 5.3 docs, the contextual variants most likely will not get color mode adaptive styling until v6, so they will not follow a dark color mode.
Contrast is the next problem. The Bootstrap accessibility docs warn that some pairings in the default palette fall below the WCAG 2.2 text ratio of 4.5:1, and the table variants are generated from those same theme colors.
Bootstrap picks each variant’s text color with its color-contrast() function, but that is no substitute for a color contrast check on every variant you ship.
WebAIM’s 2026 data shows how common the problem is. Low contrast text appeared on 83.9% of the million home pages it scanned. Pages running Bootstrap (240,869 of them) averaged 63.3 detected errors, against 56.1 across the full sample, though WebAIM cautions that extra errors cannot always be attributed to the technology itself.
Then there’s meaning. Color alone does not reach screen reader users, so the docs ask for the meaning to sit in visible text or in a .visually-hidden span.
My own rule is to use a row variant for status only when the row’s text also states the status.
Captions, group dividers, alignment and nesting in Bootstrap tables
The structural side of a table (caption, group dividers, vertical alignment, nesting) takes small classes or markup choices, not a custom stylesheet.
Footers and captions
A caption works as a heading for the table. The docs note that it helps screen reader users find a table and decide whether to read it, which is the main reason to add one to any accessible table.
The caption sits below the table by default. Add .caption-top to move it above. It renders in the secondary text color, set by $table-caption-color.
A tfoot works like thead and tbody. It holds totals or summary rows, and the same .table rules style its cells.
Group dividers
.table-group-divider adds a thicker, darker border between the thead, tbody and tfoot groups. To change its color, set border-top-color on the group, since Bootstrap ships no utility class for it.
It’s opt-in as of Bootstrap 5.2.0, which removed the thicker default border between groups.
Vertical alignment
Header cells sit at the bottom and body cells at the top. The top default comes from the $table-cell-vertical-align Sass variable.
.align-middle on the table recenters the body cells (header cells stay at the bottom). To move one row, put .align-bottom on its tr, and .align-top on a td fixes a single cell.
Nested tables
Borders, active styles and variants do not pass down to a nested table. Bootstrap 5 targets cells with the child-combinator selector .table > :not(caption) > * > *, so only the outer table’s own cells match.
Put the inner table in a td with a colspan, and add .mb-0 to drop its bottom margin.
How to make a Bootstrap table responsive
Wrap the table in a div with .table-responsive. The table then scrolls horizontally inside the wrapper whenever it is wider than the screen, at every viewport width.
- Add .table to the table element.
- Wrap the table in a div carrying .table-responsive, or a breakpoint version such as .table-responsive-md.
- Pick the breakpoint suffix: -sm, -md, -lg, -xl or -xxl.
- Test below that width, because breakpoint tables can look broken until their responsive styles apply.
Breakpoint widths
A breakpoint class scrolls the table up to its width. From the breakpoint and above, the table behaves normally.
| Wrapper class | Scrolls below |
|---|
| .table-responsive-sm | 576px |
| .table-responsive-md | 768px |
| .table-responsive-lg | 992px |
| .table-responsive-xl | 1200px |
| .table-responsive-xxl | 1400px |
Those are the same widths Bootstrap uses in its media queries for the grid and components, taken from the $grid-breakpoints Sass map in the Bootstrap 5.3 breakpoints docs.
The -xxl option exists because Bootstrap 5.0.0 added the 1400px breakpoint. Bootstrap 4 stops at -xl.
Bootstrap’s CSS is built mobile first, so the scroll behavior applies at the small end and releases as the screen grows.
There’s one side effect. The Bootstrap 5.3 docs say responsive wrappers use overflow-y: hidden, which clips dropdown menus and other widgets placed inside cells. The compiled 5.3.8 CSS only sets overflow-x: auto, but that turns vertical overflow into auto as well, so the clipping is the same.
Why does .table-responsive clip dropdown menus?
The wrapper that scrolls the table also cuts off anything poking out past its top or bottom edge, and a menu opening from a cell does exactly that.
Row-level actions built as a Bootstrap dropdown are the usual victim. The Bootstrap 4.6 docs carry the same warning as the 5.3 docs, so the limit is old, not a version 5 regression.
Stop scrolling at wide widths
Swap .table-responsive for a breakpoint class such as .table-responsive-lg. From that breakpoint up, the table behaves normally, so no scroll container exists to clip the menu.
Menus then open fully on laptops and desktops. Below the breakpoint they are still clipped, because the wrapper scrolls there.
Community CSS overrides
A snippet shared on DC Blog sets .dropdown-menu inside .table-responsive to position: static below 768px. From 768px up, it sets .table-responsive to overflow: visible.
That is a community workaround, not Bootstrap guidance.
It comes with a cost. A static menu opens inside the cell and pushes the row taller, and overflow: visible removes scrolling at that width, so a wide table can run past the page edge.
When the clipping does not apply
Tables with plain text cells never hit it.
The trouble starts only when a cell holds a menu or any widget that draws outside the cell box.
How to customize Bootstrap tables with CSS variables and Sass
Use CSS variables to restyle one table at runtime and Sass variables to change the defaults for a whole build. Both routes feed the same –bs-table-* properties that Bootstrap 5 assigns at the .table base class.
How the accent color is built
Each cell takes its background from –bs-table-bg. An inset box shadow of 0 0 0 9999px then paints the accent color on top, which is why stripes and hover states work over any variant, semi-transparent backgrounds included.
–bs-table-accent-bg is transparent by default, so a plain table casts no shadow at all.
.table-striped and .table-striped-columns point –bs-table-bg-type at –bs-table-striped-bg. .table-hover and .table-active point –bs-table-bg-state at –bs-table-hover-bg or –bs-table-active-bg.
The shadow reads var(–bs-table-bg-state, var(–bs-table-bg-type, var(–bs-table-accent-bg))), so state outranks type, and a hovered row beats a striped one regardless of CSS specificity.
Overriding one table with CSS variables
Bootstrap assigns its component variables at the base class, so a custom class on the table can reset them without recompiling Sass. Load that class after Bootstrap’s stylesheet, or give it higher specificity.
The variables you’ll reach for most are –bs-table-striped-bg for stripe color, –bs-table-hover-bg for the hover shade, –bs-table-border-color for dividers and –bs-table-color for text.
Wider changes follow the same logic as any guide to overriding Bootstrap CSS: scope the rule, load it late, keep it small.
Changing defaults in Sass
Sass overrides go after the functions import and before the variables import. The Bootstrap 5.2.0 migration notes explain why: once a default has been used, Sass cannot update it.
The factor variables set how strong each accent looks in every variant. In Bootstrap 5.3.8, $table-striped-bg-factor is .05, $table-hover-bg-factor is .075, $table-active-bg-factor is .1 and $table-border-factor is .2. $table-bg-scale sits at -80%, which lightens theme colors for variant backgrounds (light and dark excepted).
New variants come from the $table-variants map, which feeds a Sass loop that outputs one .table-* class per entry.
It stops short in places, though. The docs list stripe order only among the Sass variables ($table-striped-order and $table-striped-columns-order), and the 5.2.0 notes say CSS variable coverage stays incomplete until Bootstrap 6.
What changed in Bootstrap tables from version 4 to version 5?
The big shift came in Bootstrap 5.0.0, when tables were rebuilt on CSS variables. Nested tables also stopped inheriting parent styles, and the cell padding variable was split in two. Striped columns came later, in 5.2.0.
The class-level changes (head colors, the -xxl breakpoint, group dividers) sit in the sections above. The rows below cover what changed underneath.
| Change | Bootstrap 4 | Bootstrap 5 (release) |
|---|
| Nested tables | Inherit parent styles | Do not inherit (5.0.0) |
| Style source | Not built on CSS variables | Rebuilt on CSS variables (5.0.0) |
| Cell padding variables | Single variable | Split into -y and -x (5.0.0) |
| Striped columns | Not in the 4.6 docs | .table-striped-columns (5.2.0) |
| Variant mixin | table-row-variant() | table-variant(), two parameters (5.0.0) |
What to check when upgrading
Nested tables that leaned on inherited borders or variants will look different. Custom Sass calling table-row-variant() needs updating, and so do overrides of the old single padding variable.
Bootstrap 4 has reached end of life, according to the banner on its own docs. A version 4 project is better migrated than patched.
Where the docs disagree
The Bootstrap 4.6 tables page puts the -xl scroll cutoff at 1120px, but Bootstrap 4’s own $grid-breakpoints map sets xl at 1200px (and its widest container at 1140px), the same 1200px the 5.3 breakpoints page lists. The 1120px figure looks like a docs typo, so read the compiled CSS of your own build if you need the exact number.
One more dated detail from inside version 5: the 5.3.0-alpha2 release reverted the border-width utilities to set their property directly, which avoids inheritance problems across nested elements, tables included.
Sorting, search and pagination: when Bootstrap tables need a plugin
Bootstrap’s table classes only style markup, and the table docs describe no sorting, filtering or paging. Those features need a plugin, and the two common choices are DataTables and the Bootstrap Table plugin.
Because the classes are CSS only, every interactive behavior has to come from JavaScript loaded next to them.
DataTables
DataTables adds pagination, instant search and multi-column ordering to an existing HTML table, and it ships Bootstrap 3, 4 and 5 styling integrations.
jQuery is no longer required as of DataTables 3.0.0, which its release post describes as having no dependencies. DataTables 3 was released on 24 July 2026, per the DataTables blog, and the core is MIT licensed.
The Bootstrap Table plugin
The Bootstrap Table plugin (v1.28.0 at the time of writing) switches features on through data attributes such as data-toggle=”table”, data-pagination, data-search and data-sortable. It can also load rows from a remote JSON file with data-url.
It targets Bootstrap v5 and uses Bootstrap Icons, and jQuery must load before it, per its script order. Extensions include Sticky Header, Export, Fixed Columns and Multiple Sort.
How to choose
Stay with plain classes for display-only data. Add a plugin only when readers must sort, search or page.
If you’re starting fresh with no jQuery on the page, DataTables is the better fit. Bootstrap Table makes more sense when jQuery is already loaded and you like setting things up through attributes.
In WebAIM’s 2026 Million report, the 6,724 home pages running DataTables averaged 69.6 detected errors, against 56.1 across the full sample.
WebAIM cautions that extra errors cannot always be pinned on the technology itself. Either way, give a plugin-rendered table a full web accessibility pass, and confirm the .table classes and the .table-responsive wrapper survive initialization.
Bootstrap Tables FAQ
How do I set column widths in a Bootstrap table?
Put a sizing utility on the header cell of that column. Bootstrap’s width utilities cover 25%, 50%, 75%, 100% and auto (.w-25, .w-50, .w-75, .w-100, .w-auto), and the tables docs list no dedicated column width class.
Can I center or right-align text in table cells?
Yes. Add .text-center or .text-end to a td or th, and .text-start to restore left alignment.
Responsive forms such as .text-md-end apply from that breakpoint up. Bootstrap deliberately ships no justified-text class, because justification makes word spacing irregular.
Is there a sticky header class for Bootstrap tables?
No. The Bootstrap docs describe .sticky-top for elements in general, not for table headers.
Community examples set position: sticky; top: 0 on the th cells, and give the wrapper a fixed height with vertical scrolling when the table sits inside one. The Bootstrap Table plugin also offers a Sticky Header extension.
How can I hide a column on small screens?
Add .d-none and .d-md-table-cell to every th and td in that column. The column disappears below the md breakpoint (768px) and returns from md up.
Use the table-cell value, not block, so the cell stays a table cell.
What to Check Before a Bootstrap Table Goes Live
Before shipping, go through the markup first, then the scroll wrapper, then the variant colors, in that order. Make sure th scope and a caption are in place, that the wrapper has been tested with menus open, and that each variant clears a contrast check.
Markup goes first because scope attributes and a caption cost no CSS and serve every screen reader user.
The wrapper comes second because it decides whether widgets in cells render at all. Colors come last, since a variant swaps in without touching structure.
The contrast check has a price. A failing variant sometimes needs overriding, which gives up the one-class convenience of .table-success and its siblings and adds a stylesheet to maintain.
This order was verified against Bootstrap 5.3.8 in October 2026. A stable Bootstrap 6 release would reopen the last check, and the sequence should be re-run against its table docs.