bootstrap 5 tables, from the simplest markup to sortable columns wired up with vanilla JavaScript.
Key Takeaways
- Bootstrap 5’s
.tablebase class applies consistent spacing and borders with no extra CSS required. - Wrapping a table in
.table-responsiveenables horizontal scrolling on small viewports without breaking your layout. - Striped, bordered, hover, and colour variant classes can be combined freely using Bootstrap’s utility system.
- Sortable tables are achievable with a small vanilla JavaScript function — no jQuery plugin needed.
- Accessibility requires
<thead>,<th scope>, andaria-sortattributes for screen reader compatibility. - CSS custom properties like
--cnvs-themecolormake it straightforward to match table accents to your brand palette.
The Base Table Markup You Should Always Start With
Bootstrap 5 requires only the .table class on a <table> element to apply its default styles. What that class gives you out of the box: consistent cell padding, a bottom border on each row, and inherited text colour from the page’s base typography. No extra stylesheet, no component import beyond Bootstrap’s main CSS bundle.
The semantic structure matters too, particularly for accessibility. Always separate your header row into <thead>, your body rows into <tbody>, and optionally totals or summaries into <tfoot>. Use scope="col" on column headers and scope="row" on row headers. This is not optional if your audience includes screen reader users — and it rarely costs more than a few extra characters.
<table class="table">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Plan</th>
<th scope="col">Price</th>
<th scope="col">Users</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">1</th>
<td>Starter</td>
<td>$9/mo</td>
<td>5</td>
</tr>
<tr>
<th scope="row">2</th>
<td>Pro</td>
<td>$29/mo</td>
<td>25</td>
</tr>
</tbody>
</table>
Striped, Bordered, and Hover: Combining Bootstrap Table Styles
Bootstrap 5 ships modifier classes that you stack on top of .table. Understanding what each does — and when to combine them — prevents you from over-engineering a simple component.
.table-striped— applies a subtle background to alternating<tbody>rows using CSSnth-of-type. In Bootstrap 5.1+,.table-striped-columnsapplies the same pattern vertically instead..table-bordered— adds borders on all four sides of every cell, useful for dense data grids..table-borderless— removes all borders, better suited to card-embedded tables where the card itself provides containment..table-hover— highlights the row under the cursor, improving scannability in long tables..table-sm— halves cell padding, useful when you need to fit more data without horizontal scrolling.
<table class="table table-striped table-hover table-bordered">
<thead class="table-dark">
<tr>
<th scope="col">Project</th>
<th scope="col">Status</th>
<th scope="col">Due Date</th>
</tr>
</thead>
<tbody>
<tr>
<td>Brand Redesign</td>
<td><span class="badge bg-success">Complete</span></td>
<td>Jan 15, 2025</td>
</tr>
<tr>
<td>API Integration</td>
<td><span class="badge bg-warning text-dark">In Progress</span></td>
<td>Feb 28, 2025</td>
</tr>
</tbody>
</table>Colour variants — .table-primary, .table-danger, .table-warning and so on — can be applied at the table, row, or individual cell level, giving you precise control over status highlighting without touching your stylesheet. For consistent visual theming across components, the same approach used in Bootstrap 5’s utility class system applies here: layer classes rather than writing one-off overrides.
Making Tables Responsive With Bootstrap’s Wrapper Classes
A responsive table bootstrap implementation comes down to one wrapper div. Enclose your <table> inside a <div class="table-responsive"> and Bootstrap applies overflow-x: auto, enabling horizontal scrolling on viewports too narrow to show all columns. The table itself retains its full width internally — nothing gets truncated or hidden.
<div class="table-responsive">
<table class="table table-striped">
<!-- wide table content -->
</table>
</div>If you only need responsive behaviour below a specific breakpoint, Bootstrap provides breakpoint-suffixed variants: .table-responsive-sm, .table-responsive-md, .table-responsive-lg, and .table-responsive-xl. These apply the overflow behaviour only when the viewport is narrower than the named breakpoint, leaving the table at its natural width on larger screens.
<!-- Scroll only on screens smaller than 768px -->
<div class="table-responsive-md">
<table class="table">
<!-- content -->
</table>
</div>For tables with many columns — think analytics dashboards or financial reports — consider combining .table-sm with .table-responsive to reduce the point at which scrolling kicks in. If column priority matters, you can hide lower-priority columns on small screens using Bootstrap’s display utilities (d-none d-md-table-cell) to keep the most critical data visible without scrolling at all.

Custom Table Styling With CSS Variables
Bootstrap 5 exposes table-specific CSS custom properties that you can override without touching the source Sass. This is the fastest way to align a table’s appearance with your brand or template theme.
<style>
.table {
--bs-table-striped-bg: rgba(var(--bs-primary-rgb), 0.06);
--bs-table-hover-bg: rgba(var(--bs-primary-rgb), 0.12);
--bs-table-border-color: #e0e0e0;
}
/ Using Canvas Template's theme colour variable /
thead.table-brand {
background-color: var(--cnvs-themecolor);
color: #fff;
}
</style>
<table class="table table-striped table-hover">
<thead class="table-brand">
<tr>
<th scope="col">Feature</th>
<th scope="col">Starter</th>
<th scope="col">Pro</th>
</tr>
</thead>
<tbody>
<tr>
<td>Custom Domain</td>
<td>No</td>
<td>Yes</td>
</tr>
</tbody>
</table>The --cnvs-themecolor variable is available globally in the Canvas HTML Template, making it trivial to pull your primary brand colour into table headers, hover states, or border accents without duplicating hex values across your CSS. For a deeper look at theming with variables, see the guide on using SCSS variables to theme a Bootstrap 5 site.
Building Sortable Tables With Vanilla JavaScript
Sortable tables dramatically improve usability for data-heavy interfaces. You do not need a plugin for basic column sorting — a small vanilla JavaScript function handles the majority of real-world cases.
The approach: attach click listeners to <th> elements, read the column index, collect all <tr> elements from <tbody>, sort them by cell text content, then re-insert them. Toggle an aria-sort attribute to communicate sort direction to assistive technologies.
<table class="table table-hover" id="sortable-table">
<thead>
<tr>
<th scope="col" data-sortable="true" aria-sort="none" style="cursor:pointer">Name</th>
<th scope="col" data-sortable="true" aria-sort="none" style="cursor:pointer">Revenue</th>
<th scope="col" data-sortable="true" aria-sort="none" style="cursor:pointer">Region</th>
</tr>
</thead>
<tbody>
<tr><td>Acme Corp</td><td>84000</td><td>North</td></tr>
<tr><td>Globex</td><td>120000</td><td>East</td></tr>
<tr><td>Initech</td><td>47500</td><td>South</td></tr>
</tbody>
</table>
<script>
document.querySelectorAll('#sortable-table th[data-sortable]').forEach(function(header) {
header.addEventListener('click', function() {
const table = header.closest('table');
const tbody = table.querySelector('tbody');
const index = Array.from(header.parentElement.children).indexOf(header);
const ascending = header.getAttribute('aria-sort') !== 'ascending';
Array.from(tbody.querySelectorAll('tr'))
.sort(function(a, b) {
const aText = a.children[index].textContent.trim();
const bText = b.children[index].textContent.trim();
const aVal = isNaN(aText) ? aText : parseFloat(aText);
const bVal = isNaN(bText) ? bText : parseFloat(bText);
return ascending ? (aVal > bVal ? 1 : -1) : (aVal < bVal ? 1 : -1);
})
.forEach(function(row) { tbody.appendChild(row); });
table.querySelectorAll('th').forEach(function(th) {
th.setAttribute('aria-sort', 'none');
});
header.setAttribute('aria-sort', ascending ? 'ascending' : 'descending');
});
});
</script>This function handles both string and numeric sorting by detecting whether the cell value is a valid number. The aria-sort toggle ensures the sort state is communicated to assistive technologies — a small addition that goes a long way toward Bootstrap 5 accessibility compliance.
Placing Tables Inside Cards and Page Layouts
Tables rarely live in isolation. In most production interfaces they sit inside Bootstrap 5 card components, dashboard panels, or modal dialogs. When embedding a table in a card, use .card-body padding carefully — if you want the table to extend edge-to-edge within the card, skip the .card-body wrapper and place the table directly inside .card, relying on Bootstrap’s built-in .card > .table style rules that remove the outer border and adjust corner radius.
<div class="card">
<div class="card-header">
<strong>Q1 Sales Report</strong>
</div>
<div class="table-responsive">
<table class="table table-striped mb-0">
<thead>
<tr>
<th scope="col">Month</th>
<th scope="col">Sales</th>
<th scope="col">Growth</th>
</tr>
</thead>
<tbody>
<tr><td>January</td><td>$42,000</td><td class="text-success">+12%</td></tr>
<tr><td>February</td><td>$38,500</td><td class="text-danger">-8%</td></tr>
<tr><td>March</td><td><strong>$51,200</strong></td><td class="text-success">+33%</td></tr>
</tbody>
</table>
</div>
</div>Adding .mb-0 to the table removes Bootstrap’s default bottom margin, preventing a gap between the last row and the card’s bottom edge. Combine with .table-responsive wrapping the table — not the entire card — to keep the card header and footer stationary while only the table body scrolls horizontally.
Table Best Practices for Production Projects
A few decisions made at the markup level prevent the most common table-related bugs and accessibility failures:
- Always use
<thead>and<tbody>. Browsers and screen readers rely on this structure for correct rendering and navigation. - Add
scopeattributes to all<th>elements.scope="col"for column headers,scope="row"for row headers. - Use
.mb-0inside card containers to eliminate unwanted spacing at the table’s bottom edge. - Prefer
.table-responsive-{breakpoint}over the generic wrapper when the table is readable on tablet-sized viewports — unnecessary scroll chrome on mid-range screens harms usability. - Avoid inline widths on
<td>or<th>unless you have a specific reason. Let Bootstrap’s box model and content determine column widths unless equal or fixed columns are a design requirement. - Apply colour variants at the row level for status highlighting rather than re-styling entire tables — it is easier to maintain and more semantically clear.
FAQ
Wrap the <table> element in a <div class="table-responsive">. Bootstrap applies overflow-x: auto to this container, allowing the table to scroll horizontally on narrow viewports while maintaining its full column width internally. Use .table-responsive-md or similar breakpoint variants if you only want scroll behaviour below a specific screen size.
Yes. Bootstrap 5 table modifier classes are designed to be combined freely. You can stack .table-striped, .table-bordered, .table-hover, and colour variants like .table-dark on a single <table> element. The classes target distinct CSS properties and do not conflict with each other.
Applying .table-dark to the <table> element makes the entire table dark — headers, body rows, and footer. Applying it only to <thead> gives you a dark header row with a light body, which is a common design pattern for data tables in dashboard interfaces. You can also use .table-light on <thead> for a subtle grey header against a white body.
No. A lightweight vanilla JavaScript function — roughly 20 lines — handles alphanumeric and numeric column sorting without any third-party dependency. For advanced requirements like server-side pagination, complex filtering, or virtual scrolling of thousands of rows, libraries such as DataTables or Tabulator are worth considering, but they add significant weight for simpler use cases.
Use the CSS custom property approach rather than hardcoding a hex value. In a Bootstrap 5 project using the Canvas HTML Template, you can reference var(--cnvs-themecolor) as the background colour on your <thead> element. Alternatively, override the Bootstrap CSS variable --bs-table-bg on a scoped selector to change the table’s base background without touching the global stylesheet.
Looking for a production-ready Bootstrap 5 HTML template? Browse Canvas Template demos and find the perfect starting point for your next project.
If you’re working with the Canvas HTML Template and want to generate production-ready layouts faster, try Canvas Builder free and see how much time you save on every project.
Skip the setup — build it free
Spin up a complete Bootstrap 5 site, blog included, with Canvas Builder. No coding, no cost.
Canvas Team
Tutorials and tips for building beautiful Bootstrap 5 websites with the Canvas HTML Template and Canvas Builder.
More from the Canvas Blog