Table fundamentals
Build semantic tables with captions, scoped headers, footers, responsive overflow, sticky headers, sorting controls, and empty states.
Table fundamentals
Build tables from meaningful native structure, contain wide datasets in accessible overflow regions, and expose data states without weakening the row-and-column relationships.
Sticky and sortable columns
Constrain a long result set with the sticky-header wrapper. Sorting remains application behavior: update the rows and move aria-sort to the active column after each sort.
| Commit | Result | ||
|---|---|---|---|
| Aug 11, 14:32 | Production | a81f0d2 | Passed |
| Aug 11, 12:08 | Preview | 7c29be1 | Passed |
| Aug 10, 18:41 | Production | 663d420 | Rolled back |
| Aug 10, 16:05 | Preview | 45fb118 | Passed |
| Aug 9, 09:27 | Production | 31ad902 | Passed |
<div class="bs-table-responsive bs-table-sticky-header" role="region" tabindex="0" aria-label="Deployment history" style="--bs-table-max-height: 16rem;">
<table class="bs-table bs-table-hover">
<caption>Deployment history</caption>
<thead><tr><th scope="col" aria-sort="descending"><button class="bs-table-sort" type="button">Deployed</button></th><th scope="col"><button class="bs-table-sort" type="button">Environment</button></th><th scope="col">Commit</th><th scope="col">Result</th></tr></thead>
<tbody>
<tr><td>Aug 11, 14:32</td><td>Production</td><td><code class="bs-code-inline">a81f0d2</code></td><td>Passed</td></tr>
<tr><td>Aug 11, 12:08</td><td>Preview</td><td><code class="bs-code-inline">7c29be1</code></td><td>Passed</td></tr>
<tr><td>Aug 10, 18:41</td><td>Production</td><td><code class="bs-code-inline">663d420</code></td><td>Rolled back</td></tr>
<tr><td>Aug 10, 16:05</td><td>Preview</td><td><code class="bs-code-inline">45fb118</code></td><td>Passed</td></tr>
<tr><td>Aug 9, 09:27</td><td>Production</td><td><code class="bs-code-inline">31ad902</code></td><td>Passed</td></tr>
</tbody>
</table>
</div>Empty dataset
Keep the table structure and column context while data is empty. The message should explain the absence; place any recovery action in or immediately after the cell.
| Invoice | Customer | Due date | Amount |
|---|---|---|---|
| No invoices match these filters. | |||
<div class="bs-table-responsive" role="region" tabindex="0" aria-label="Filtered invoices">
<table class="bs-table">
<caption>Invoices matching the current filters</caption>
<thead><tr><th scope="col">Invoice</th><th scope="col">Customer</th><th scope="col">Due date</th><th class="bs-table-cell-numeric" scope="col">Amount</th></tr></thead>
<tbody><tr><td class="bs-table-empty" colspan="4">No invoices match these filters.</td></tr></tbody>
</table>
</div>Overflow
Give scrollable wrappers role="region", tabindex="0", and an accessible name. This lets keyboard users reach columns beyond the viewport.
Sorting
Put sorting on real buttons, move aria-sort to the active header, and update it only after the visible rows have been reordered.
Empty states
Keep the caption and headers in place, then span one explanatory .bs-table-empty cell across the body so users retain column context.
Structure and behavior API
Sorting remains application behavior. Use a real button in the header and keep aria-sort synchronized with the rendered row order.
| Class | Purpose | Use it on |
|---|---|---|
.bs-table | Styles native captions, headers, body rows, cells, and footers. | The table. |
.bs-table-responsive | Contains horizontal or vertical overflow. | A wrapper with a role, keyboard target, and accessible name. |
.bs-table-sticky-header | Constrains the scroll region and keeps column headers visible. Override --bs-table-max-height as needed. | The responsive wrapper. |
.bs-table-cell-numeric | Aligns tabular numbers to the right. | Matching numeric header and data cells. |
.bs-table-cell-actions | Right-aligns and keeps row actions on one line. | Matching action header and data cells. |
.bs-table-sort | Styles a real sort button and reflects its parent header’s sort state. | A button inside a column header. |
.bs-table-empty | Creates a centered empty-state cell with generous vertical space. | One cell spanning every column. |