Table
<ahi-table> | AhiTable
Tables are used to organize and display information from a data set. Provides table styles in addition to features like striped rows and support for narrow viewports.
|
|
Amount | Status | Description | Method | Date | Actions |
|---|---|---|---|---|---|---|
|
|
|
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. | Card |
|
|
|
|
|
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. | Card |
|
|
|
|
|
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. | Card |
|
|
|
|
|
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. | Card |
|
|
<ahi-card title="Recent payments" style="width: 100%">
<div class="ahi-row ahi-gap-2 ahi-align-items-center" slot="header-actions" style="visibility: hidden">
<span class="ahi-text-body message" aria-live="polite" aria-atomic="true">0 items selected</span>
<ahi-button variant="primary" size="small">Export</ahi-button>
</div>
<ahi-table style="margin-top: var(--ahi-space-4)">
<table>
<thead>
<tr>
<th>
<div class="ahi-table-actions">
<ahi-checkbox>
<span class="ahi-visually-hidden">Toggle all</span>
</ahi-checkbox>
</div>
</th>
<th class="ahi-table-align-right">Amount</th>
<th>Status</th>
<th>Description</th>
<th>Method</th>
<th class="ahi-table-align-right">Date</th>
<th class="ahi-table-align-right">Actions</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<div class="ahi-table-actions">
<ahi-checkbox>
<span class="ahi-visually-hidden">Select row</span>
</ahi-checkbox>
</div>
</td>
<td class="ahi-table-align-right">
<ahi-format-number type="currency" value="350.00"></ahi-format-number>
</td>
<td>
<ahi-badge status="success">Success</ahi-badge>
</td>
<td class="ahi-table-ellipsis">
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et
dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex
ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat
nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit
anim id est laborum.
</td>
<td>Card</td>
<td class="ahi-table-align-right">
<ahi-format-date precision="date" date="2021-03-20" time-zone="America/New_York"></ahi-format-date>
</td>
<td>
<div class="ahi-table-actions">
<ahi-button size="small">
<span class="ahi-visually-hidden">Actions</span>
<ahi-icon name="dots"></ahi-icon>
</ahi-button>
</div>
</td>
</tr>
<tr>
<td>
<div class="ahi-table-actions">
<ahi-checkbox>
<span class="ahi-visually-hidden">Select row</span>
</ahi-checkbox>
</div>
</td>
<td class="ahi-table-align-right">
<ahi-format-number type="currency" value="29.99"></ahi-format-number>
</td>
<td>
<ahi-badge status="warning">Warning</ahi-badge>
</td>
<td class="ahi-table-ellipsis">
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et
dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex
ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat
nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit
anim id est laborum.
</td>
<td>Card</td>
<td class="ahi-table-align-right">
<ahi-format-date precision="date" date="2021-03-20" time-zone="America/New_York"></ahi-format-date>
</td>
<td>
<div class="ahi-table-actions">
<ahi-button size="small">
<span class="ahi-visually-hidden">Actions</span>
<ahi-icon name="dots"></ahi-icon>
</ahi-button>
</div>
</td>
</tr>
<tr>
<td>
<div class="ahi-table-actions">
<ahi-checkbox>
<span class="ahi-visually-hidden">Select row</span>
</ahi-checkbox>
</div>
</td>
<td class="ahi-table-align-right">
<ahi-format-number type="currency" value="50.00"></ahi-format-number>
</td>
<td>
<ahi-badge status="success">Success</ahi-badge>
</td>
<td class="ahi-table-ellipsis">
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et
dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex
ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat
nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit
anim id est laborum.
</td>
<td>Card</td>
<td class="ahi-table-align-right">
<ahi-format-date precision="date" date="2021-03-19" time-zone="America/New_York"></ahi-format-date>
</td>
<td>
<div class="ahi-table-actions">
<ahi-button size="small">
<span class="ahi-visually-hidden">Actions</span>
<ahi-icon name="dots"></ahi-icon>
</ahi-button>
</div>
</td>
</tr>
<tr>
<td>
<div class="ahi-table-actions">
<ahi-checkbox>
<span class="ahi-visually-hidden">Select row</span>
</ahi-checkbox>
</div>
</td>
<td class="ahi-table-align-right">
<ahi-format-number type="currency" value="49.00"></ahi-format-number>
</td>
<td>
<ahi-badge status="success">Success</ahi-badge>
</td>
<td class="ahi-table-ellipsis">
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et
dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex
ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat
nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit
anim id est laborum.
</td>
<td>Card</td>
<td class="ahi-table-align-right">
<ahi-format-date precision="date" date="2021-03-17" time-zone="America/New_York"></ahi-format-date>
</td>
<td>
<div class="ahi-table-actions">
<ahi-button size="small">
<span class="ahi-visually-hidden">Actions</span>
<ahi-icon name="dots"></ahi-icon>
</ahi-button>
</div>
</td>
</tr>
</tbody>
</table>
</ahi-table>
</ahi-card>
<script>
const { tHead, tBodies: [tBody] } = document.querySelector("table");
const checkboxes = tBody.querySelectorAll("ahi-checkbox");
const toggleAll = tHead.querySelector("ahi-checkbox");
const actions = document.querySelector("[slot=header-actions]");
const button = actions.querySelector("ahi-button");
const message = actions.querySelector(".message");
function render() {
let count = 0;
checkboxes.forEach(checkbox => {
checkbox.closest("tr").classList.toggle("ahi-row-selected", checkbox.checked);
if (checkbox.checked) count++;
})
toggleAll.indeterminate = count > 0 && count < checkboxes.length;
toggleAll.checked = count === checkboxes.length;
message.innerText = `${count} ${count === 1 ? "item" : "items"} selected`;
actions.style.visibility = count === 0 ? "hidden" : "visible";
}
tBody.addEventListener("ahi-change", render);
toggleAll.addEventListener("ahi-change", () => {
checkboxes.forEach(checkbox => (checkbox.checked = toggleAll.checked));
render();
})
</script>
Row states
Rows highlight on hover and darken while pressed. Both are automatic — there is nothing to opt into.
The hover is behind @media (hover: hover), so it never appears on a touch device. A touch
browser reports a hover on whatever was tapped last and keeps reporting it, which would otherwise leave the
tapped row highlighted long after the finger lifted. The press state is not gated and is the feedback a tap
gets instead.
.ahi-row-selected is a state you set, not a pointer position, so it is outside that gate and
paints on every device. Add and remove it yourself, as the selection example above does.
If your app dims a row — an inactive patient, a cancelled order — note that the hover restores full-strength text for as long as the pointer is over it. That only happens on a device that can hover; the dimming is untouched on a phone.
Narrow viewports
A table’s natural width is the sum of its columns, which on a phone means it scrolls sideways. Three options address that, each independent of the others and all off by default.
Stacked rows
stacked turns each row into a card once the table’s own box is too narrow for columns, so it
reads as a list instead of scrolling. Each value is labelled with its column heading, which the component
copies from the table’s <thead> — your markup needs nothing but the attribute.
The switch is a container query on the table’s own width, not the viewport’s, so a table inside a narrow column stacks even on a wide screen.
| Medication | Dose | Prescriber |
|---|---|---|
| Lisinopril | 10 mg daily | Dr. Jane Doe |
| Metformin | 500 mg twice daily | Dr. Jane Doe |
<div style="max-inline-size: 380px; resize: horizontal; overflow: auto; padding-block-end: 1rem;">
<ahi-table stacked>
<table>
<thead>
<tr>
<th>Medication</th>
<th>Dose</th>
<th>Prescriber</th>
</tr>
</thead>
<tbody>
<tr>
<td>Lisinopril</td>
<td>10 mg daily</td>
<td>Dr. Jane Doe</td>
</tr>
<tr>
<td>Metformin</td>
<td>500 mg twice daily</td>
<td>Dr. Jane Doe</td>
</tr>
</tbody>
</table>
</ahi-table>
</div>
Drag the handle in the corner to widen the container and the cards become columns again.
To label a cell differently from its column heading — spelling out an abbreviation, or naming a column
headed by an icon — set data-label on the cell yourself and the component will leave it alone.
<td data-label="Date of birth">1984-02-11</td>
A cell carrying .ahi-table-ellipsis keeps its full value once the table stacks. The utility
truncates a long value to one line so it does not widen its column, and a card has no column and no sideways
room to save, so there is nothing left for it to do — the value wraps instead. Nothing to change in your
markup: the same cell truncates with columns and wraps as a card.
Sticky first column
sticky-column pins the first column while the rest scrolls, so a row keeps its identifier as
the reader scrolls right to reach a value. It has no effect while a table is stacked, where each card is
already its own identifier.
| Patient | MRN | Last seen | Primary care | Insurer | Next appointment |
|---|---|---|---|---|---|
| Jane Doe | 884120 | 12 Aug 2026 | Dr. Jane Doe | Blue Ridge Health | 04 Sep 2026 |
| John Smith | 884121 | 28 Jul 2026 | Dr. John Smith | Cardinal Mutual | 11 Sep 2026 |
<ahi-table sticky-column>
<table>
<thead>
<tr>
<th>Patient</th>
<th>MRN</th>
<th>Last seen</th>
<th>Primary care</th>
<th>Insurer</th>
<th>Next appointment</th>
</tr>
</thead>
<tbody>
<tr>
<td>Jane Doe</td>
<td>884120</td>
<td>12 Aug 2026</td>
<td>Dr. Jane Doe</td>
<td>Blue Ridge Health</td>
<td>04 Sep 2026</td>
</tr>
<tr>
<td>John Smith</td>
<td>884121</td>
<td>28 Jul 2026</td>
<td>Dr. John Smith</td>
<td>Cardinal Mutual</td>
<td>11 Sep 2026</td>
</tr>
</tbody>
</table>
</ahi-table>
Scroll cue
Any table that overflows its box shows a shadow at the edge with content beyond it, which fades away as the reader reaches that end. It needs no attribute and costs nothing when the table fits — it exists because the platform’s overlay scrollbar is invisible until you are already scrolling, so nothing told the reader the remaining columns were there.
Advanced use-cases
The table component itself does not provide any functionality, only styles. This works well for simple or static tables, but there are instances where functionality such as sorting, resizing, or column dragging are required. This kind of functionality is out of scope for the table component, as the spectrum of possible use-cases is too broad for a single component to cover effectively. Whilst the table component is limited out of the box, these same limitations make it easy to integrate with third-party libraries which offer advanced functionality.
For these advanced use-cases we recommend using
Tanstack Table. Tanstack table offers integrations for many UI frameworks, such as Vue and React. It is headless so it
provides functions, state, utilities and event listeners but does not include any HTML or styles. You can
use it to render a plain, semantic <table> wrapped in an <ahi-table>,
and you will get all the necessary styles.
Best practices
- The table component acts as a lightweight and un-opinionated wrapper component for enhancing tabular data. It is up to the user of this component to make sure that the table markup they use is accessible.
-
Despite the name the table component does not supplement the HTML
<table>element. Please ensure that a<table>element is a direct descendant of the table component. - It’s important to pay close attention to semantics when authoring tables. The markup in the examples can be used as a starting point. However, be aware that HTML tables have a large feature set which cannot be fully covered in this documentation.
Tables should:
- Be used to display tabular data
- Have a
<table>element directly within the component - Be used inside an
<ahi-card>and given a title - Paginate when the current list contains more than 25 items.
Tables should NOT:
- Be used to display list data
- Be used to display basic key and value pairs, consider using a description list instead
- Be used for layout
- Use a primary button in every row
Content guidelines
- Headers in a table should be clear, accurate and predictable. When writing headers, always write them in sentence case, not title case. The first word should be capitalized and the rest lowercase (unless a proper noun)
- Include units of measurement symbols in the table header so they aren’t repeated throughout every single column ie “Temperature °C”
- Avoid unnecessary words and articles in table headers, such as “the”, “an” or “a”
- Keep decimals consistent. For example, don’t use 3 decimals in one row and 2 in others
Importing
To import this component using a script tag:
<script type="module" src="/path/to/azalea-kit-ui/dist/components/table/table.js"></script>
To import this component using a bundler:
import '@azaleahealth/azalea-kit-ui/dist/components/table/table.js';
Slots
| Name | Description |
|---|---|
| (default) | Default slot which holds the HTML <table> element. |
Learn more about using slots.
Properties
| Name | Description | Type | Default | |
|---|---|---|---|---|
density
|
Controls the density of the table’s rows and headers. Relaxed increases space, condensed reduces space. |
'condensed' | 'default' | 'relaxed'
|
'default'
|
|
scrollSnap
scroll-snap
|
Enables scroll-snapping, meaning the scroll position is always column-aligned. |
boolean
|
false
|
|
striped
|
Controls whether to use zebra striping on tables, which can improve readability. |
boolean
|
false
|
|
stacked
|
Stacks each row into a card once the table itself is too narrow for columns, so a phone-width table
reads as a list instead of scrolling sideways. Each cell is labelled with its column heading, taken
from the table’s own <thead>; set data-label on a cell to override
it. The switch is a container query on the table’s width rather than the viewport’s, so a table in a
narrow column stacks even on a wide screen.
|
boolean
|
false
|
|
stickyColumn
sticky-column
|
Pins the first column while the rest of the table scrolls horizontally, so a row keeps its identifier as the reader scrolls to reach a value. Has no effect while the table is stacked, where there are no columns to pin. |
boolean
|
false
|
|
updateComplete |
A read-only promise that resolves when the component has finished updating. |
Learn more about properties and attributes.