table
Tables display tabular data with full-width layout, collapsed borders, tabular numerals, and automatic row hover states. Includes variants for striped, compact, and bordered styles plus data attributes for density, sticky headers, and row states.
Description
The <table> element displays tabular data arranged in rows and columns. Vanilla Breeze styles tables with full-width layout (inline-size: 100%), collapsed borders, and font-variant-numeric: tabular-nums so digits align properly in columns. Header cells get a raised surface background with medium-weight bottom borders, and body rows include automatic hover states for improved scannability.
Footer rows (<tfoot>) are styled with bold text and a raised background to visually separate summary data from detail rows.
When to Use
- Data comparison: when users need to compare values across rows or columns
- Structured records: data with clear header-to-value relationships (reports, statistics, inventories)
- Schedules and timetables: time slots related to activities or resources
When Not to Use
- Key-value pairs: use
<dl>for definition/description lists - Card grids: use
<layout-grid>for responsive card layouts - Page layout: never use tables for visual layout; use CSS Grid or Flexbox
- Interactive data grids: for sorting, filtering, and pagination use the
<data-table>web component
Basic Usage
A well-structured table includes a <caption> for accessibility, <thead> for column headers with scope attributes, <tbody> for data rows, and optionally a <tfoot> for summary rows. Use data-numeric on cells containing numbers to right-align and enable tabular numerals.
<table> <caption>Quarterly Sales Report</caption> <thead> <tr> <th scope="col">Product</th> <th scope="col" data-numeric>Q1</th> <th scope="col" data-numeric>Q2</th> <th scope="col" data-numeric>Q3</th> <th scope="col" data-numeric>Q4</th> </tr> </thead> <tbody> <tr> <th scope="row">Widgets</th> <td data-numeric>$12,400</td> <td data-numeric>$14,200</td> <td data-numeric>$13,800</td> <td data-numeric>$16,100</td> </tr> <tr> <th scope="row">Gadgets</th> <td data-numeric>$8,300</td> <td data-numeric>$9,100</td> <td data-numeric>$10,500</td> <td data-numeric>$11,200</td> </tr> </tbody> <tfoot> <tr> <th scope="row">Total</th> <td data-numeric>$26,300</td> <td data-numeric>$29,500</td> <td data-numeric>$30,200</td> <td data-numeric>$34,700</td> </tr> </tfoot></table>
Variants
Three class-based variants modify the table appearance. They can be combined freely.
<table class="striped"> <!-- Alternating row backgrounds --></table> <table class="compact"> <!-- Reduced cell padding --></table> <table class="bordered"> <!-- Full borders on all cells --></table> <table class="striped compact bordered"> <!-- Variants can be combined --></table>
| Class | Effect |
|---|---|
.striped |
Alternating odd-row backgrounds using var(--color-surface-raised) |
.compact |
Reduced cell padding (size-xs / size-s) |
.bordered |
Full borders on every cell using var(--border-width-thin) |
Data Attributes
data-density
Controls cell padding independently of the .compact class. Useful when you want density control via attributes rather than classes.
<table data-density="compact"> <!-- Tighter padding: size-2xs / size-xs --></table> <table data-density="comfortable"> <!-- Roomier padding: size-m / size-l --></table>
| Value | Padding |
|---|---|
compact |
size-2xs / size-xs |
comfortable |
size-m / size-l |
data-sticky
Makes the header row or first column sticky during scroll.
<table data-sticky="header"> <!-- thead sticks to viewport top on scroll --></table> <table data-sticky="column"> <!-- First column sticks on horizontal scroll --></table>
| Value | Effect |
|---|---|
header |
thead th becomes position: sticky at top |
column |
First td/th in each row sticks at inline-start |
data-numeric
Applied to individual <th> and <td> cells to right-align content and enforce font-variant-numeric: tabular-nums for proper digit alignment.
<th scope="col" data-numeric>Revenue</th><td data-numeric>$150,000</td>
data-sort
Applied to <th> elements to indicate sortable columns. Adds a pointer cursor and appends a sort indicator character.
<th data-sort>Name</th><!-- Renders cursor: pointer and appends ⇅ indicator -->
Row States
Data attributes on <tr> elements control visual row states without JavaScript class toggling.
<tr data-selected> <!-- Interactive highlight background --></tr> <tr data-highlight> <!-- Warning/attention background --></tr> <tr data-disabled> <!-- Reduced opacity, no pointer events --></tr>
| Attribute | Effect |
|---|---|
data-selected |
Interactive-tinted background at 15% opacity |
data-highlight |
Warning-tinted background at 15% opacity |
data-disabled |
50% opacity with pointer-events: none |
CSS Reference
The complete set of table styles applied by Vanilla Breeze. All values use design tokens, so tables adapt automatically to any theme.
table { inline-size: 100%; border-collapse: collapse; font-variant-numeric: tabular-nums; text-align: start;} th { padding: var(--size-s) var(--size-m); font-weight: 600; background: var(--color-surface-raised); border-block-end: var(--border-width-medium) solid var(--color-border);} td { padding: var(--size-s) var(--size-m); border-block-end: var(--border-width-thin) solid var(--color-border); vertical-align: top;} tbody tr:hover { background: var(--color-surface-raised);} tfoot td { font-weight: 600; background: var(--color-surface-raised); border-block-start: var(--border-width-medium) solid var(--color-border); border-block-end: none;} /* Variants */table.striped tbody tr:nth-child(odd) { background: var(--color-surface-raised); }table.compact :is(th, td) { padding: var(--size-xs) var(--size-s); }table.bordered :is(th, td) { border: var(--border-width-thin) solid var(--color-border); } /* Density */table[data-density="compact"] :is(th, td) { padding: var(--size-2xs) var(--size-xs); }table[data-density="comfortable"] :is(th, td) { padding: var(--size-m) var(--size-l); } /* Sticky */table[data-sticky="header"] thead th { position: sticky; inset-block-start: 0; z-index: 1; }table[data-sticky="column"] :is(td, th):first-child { position: sticky; inset-inline-start: 0; z-index: 1; } /* Numeric alignment */:is(td, th)[data-numeric] { text-align: end; font-variant-numeric: tabular-nums; } /* Row states */tr[data-selected] { background: oklch(from var(--color-interactive) l c h / 0.15); }tr[data-disabled] { opacity: 0.5; pointer-events: none; }tr[data-highlight] { background: oklch(from var(--color-warning) l c h / 0.15); }
Accessibility
- Always include a
<caption>: provides context for screen reader users navigating to the table - Use
<th>withscope: define whether headers apply to columns (scope="col") or rows (scope="row") - Structure with
<thead>,<tbody>,<tfoot>: helps assistive technologies understand table regions - Avoid empty cells: use appropriate content or "N/A" rather than leaving cells blank
- Keep tables simple: avoid complex merged cells when possible; use
scope="colgroup"for multi-level headers
Related
<caption>- table title for accessibility<colgroup>and<col>- column grouping and styling<thead>,<tbody>,<tfoot>- row group sections<tr>- table rows with state attributes<th>and<td>- header and data cells<dl>- use instead for key-value data<data-table>- interactive data grid with sorting, filtering, and pagination