HTML <table> reference
Jump to this section
Common attributes Jump to this section
Basic table structure Jump to this section
A <table> element represents tabular data. It consists of rows (<tr>), header cells (<th>), and data cells (<td>). Use semantic structure with <thead>, <tbody>, and optionally <tfoot>.
| Name | Age |
|---|---|
| Alice | 28 |
| Bob | 34 |
<table>
<thead>
<tr>
<th>Name</th>
<th>Age</th>
</tr>
</thead>
<tbody>
<tr>
<td>Alice</td>
<td>28</td>
</tr>
<tr>
<td>Bob</td>
<td>34</td>
</tr>
</tbody>
</table>Table elements hierarchy Jump to this section
<table>
├── <caption> (optional: table title)
├── <colgroup> (optional: column groups)
│ └── <col> (column styling)
├── <thead> (table header)
│ └── <tr> (table row)
│ ├── <th> (header cell)
│ └── <th>
├── <tbody> (table body)
│ └── <tr>
│ ├── <td> (data cell)
│ └── <td>
└── <tfoot> (optional: table footer)
└── <tr>
├── <td>
└── <td>
Table attributes Jump to this section
border (deprecated) Jump to this section
Note: This attribute is deprecated. Use CSS border instead.
<!-- Don't use -->
<table border="1">
<!-- Use CSS instead -->
<table style="border: 1px solid #ddd;">
Caption element Jump to this section
<caption>
Jump to this section
Provides a title or description for the table. Should be the first child of <table>.
| Name | Role |
|---|---|
| Alice | Developer |
<table>
<caption>Employee Data</caption>
<thead>
<tr>
<th>Name</th>
<th>Role</th>
</tr>
</thead>
<tbody>
<tr>
<td>Alice</td>
<td>Developer</td>
</tr>
</tbody>
</table>Accessibility: Always use <caption> to describe the table's purpose for screen readers.
Header cell attributes (<th>)
Jump to this section
scope Jump to this section
Specifies which cells the header applies to.
Values:
row: Header for a rowcol: Header for a column (default)rowgroup: Header for a row groupcolgroup: Header for a column group
| Product | Price |
|---|---|
| Apple | $1.00 |
| Banana | $0.50 |
<table>
<thead>
<tr>
<th scope="col">Product</th>
<th scope="col">Price</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Apple</th>
<td>$1.00</td>
</tr>
</tbody>
</table>Best practice: Always use scope on <th> elements for accessibility.
abbr Jump to this section
Provides an abbreviated version of the header content for screen readers.
<th scope="col" abbr="Emp ID">Employee Identification Number</th>
Cell attributes (<td> and <th>)
Jump to this section
colspan Jump to this section
Spans a cell across multiple columns.
| Name | Age | |
|---|---|---|
| First | Last | |
| Alice | Smith | 28 |
<table>
<thead>
<tr>
<th colspan="2">Name</th>
<th>Age</th>
</tr>
<tr>
<th>First</th>
<th>Last</th>
<th></th>
</tr>
</thead>
</table>rowspan Jump to this section
Spans a cell across multiple rows.
| Alice | alice@example.com | |
| Phone | 555-1234 |
<table>
<tbody>
<tr>
<td rowspan="2">Alice</td>
<td>Email</td>
<td>alice@example.com</td>
</tr>
<tr>
<td>Phone</td>
<td>555-1234</td>
</tr>
</tbody>
</table>headers Jump to this section
References header cells by their id for complex tables. Improves accessibility.
<table>
<thead>
<tr>
<th id="name">Name</th>
<th id="email">Email</th>
</tr>
</thead>
<tbody>
<tr>
<td headers="name">Alice</td>
<td headers="email">alice@example.com</td>
</tr>
</tbody>
</table>
Column elements Jump to this section
<colgroup> and <col>
Jump to this section
Group and style columns without affecting content structure.
| Name | Age | City |
|---|---|---|
| Alice | 28 | NYC |
<table>
<colgroup>
<col style="background: #f9fafb;">
<col style="background: #eff6ff;">
<col style="background: #f0fdf4;">
</colgroup>
<thead>
<tr>
<th>Name</th>
<th>Age</th>
<th>City</th>
</tr>
</thead>
</table>span (col) Jump to this section
Specifies how many columns the <col> element spans.
<colgroup>
<col span="2" style="background: #f9fafb;">
<col style="background: #eff6ff;">
</colgroup>
Accessibility attributes Jump to this section
role Jump to this section
Tables have an implicit table role. Usually not needed unless changing semantics.
<!-- Presentation table (layout, not data) -->
<table role="presentation">
<!-- Content -->
</table>
aria-label Jump to this section
Provides an accessible label for the table.
<table aria-label="Employee contact information">
<!-- Content -->
</table>
aria-describedby Jump to this section
References additional descriptive text.
<table aria-describedby="table-description">
<!-- Content -->
</table>
<p id="table-description">
This table shows quarterly sales data for 2023.
</p>
aria-sort Jump to this section
Indicates the current sort order on a column header.
Values: ascending, descending, none, other
<th scope="col" aria-sort="ascending">Name ↑</th>
<th scope="col" aria-sort="none">Age</th>
Common attributes Jump to this section
id Jump to this section
Unique identifier for the table or cells.
<table id="employee-table">
<!-- Content -->
</table>
<th id="name-header">Name</th>
class Jump to this section
CSS class names for styling.
<table class="data-table striped bordered">
<!-- Content -->
</table>
<tr class="highlight">
<td>Important row</td>
</tr>
data-* attributes Jump to this section
Custom data attributes.
<table data-table-id="12345" data-sortable="true">
<!-- Content -->
</table>
<tr data-user-id="101" data-status="active">
<td>Alice</td>
</tr>
<script>
const table = document.querySelector('table');
const tableId = table.dataset.tableId; // "12345"
</script>
title Jump to this section
Tooltip on hover (not accessible on mobile).
<td title="Click to sort">Name</td>
Table sections Jump to this section
<thead>
Jump to this section
Groups header rows. Should contain <tr> with <th> cells.
<thead>
<tr>
<th>Column 1</th>
<th>Column 2</th>
</tr>
</thead>
Benefits:
- Semantic structure
- Can be styled separately
- Repeats on printed pages in some browsers
- Fixed header positioning with CSS
<tbody>
Jump to this section
Groups body rows. Contains the main table data.
<tbody>
<tr>
<td>Data 1</td>
<td>Data 2</td>
</tr>
</tbody>
Note: If <tbody> is not explicitly included, the browser will create it automatically.
<tfoot>
Jump to this section
Groups footer rows. Typically for totals or summaries.
| Item | Price |
|---|---|
| Apple | $1.00 |
| Banana | $0.50 |
| Total | $1.50 |
<table>
<thead>
<tr>
<th>Item</th>
<th>Price</th>
</tr>
</thead>
<tbody>
<tr>
<td>Apple</td>
<td>$1.00</td>
</tr>
</tbody>
<tfoot>
<tr>
<th>Total</th>
<td>$1.50</td>
</tr>
</tfoot>
</table>Note: <tfoot> can be placed before <tbody> in HTML but will render at the bottom.
Best practices Jump to this section
Use semantic structure Jump to this section
Always use <thead>, <tbody>, and when appropriate <tfoot>.
<!-- Good -->
<table>
<thead>
<tr><th>Name</th><th>Age</th></tr>
</thead>
<tbody>
<tr><td>Alice</td><td>28</td></tr>
</tbody>
</table>
<!-- Avoid -->
<table>
<tr><th>Name</th><th>Age</th></tr>
<tr><td>Alice</td><td>28</td></tr>
</table>
Use <th> for headers
Jump to this section
Use <th> for header cells, not <td> with bold styling.
<!-- Good -->
<tr>
<th scope="col">Name</th>
<th scope="col">Age</th>
</tr>
<!-- Avoid -->
<tr>
<td><strong>Name</strong></td>
<td><strong>Age</strong></td>
</tr>
Include <caption> or aria-label
Jump to this section
Provide a title or description for accessibility.
<table>
<caption>Monthly Sales Report - Q4 2023</caption>
<!-- Content -->
</table>
<!-- Or -->
<table aria-label="Monthly Sales Report">
<!-- Content -->
</table>
Use scope attribute
Jump to this section
Always add scope to <th> elements.
<th scope="col">Column Header</th>
<th scope="row">Row Header</th>
Don't use tables for layout Jump to this section
Tables are for tabular data only. Use CSS Grid or Flexbox for page layout.
<!-- Bad: Using table for layout -->
<table>
<tr>
<td>Sidebar</td>
<td>Main content</td>
</tr>
</table>
<!-- Good: Use CSS Grid -->
<div class="layout">
<aside>Sidebar</aside>
<main>Main content</main>
</div>
Complex table examples Jump to this section
Multi-level headers Jump to this section
| Name | Contact | |
|---|---|---|
| Phone | ||
| Alice | alice@example.com | 555-1234 |
<table>
<thead>
<tr>
<th rowspan="2">Name</th>
<th colspan="2">Contact</th>
</tr>
<tr>
<th>Email</th>
<th>Phone</th>
</tr>
</thead>
<tbody>
<tr>
<td>Alice</td>
<td>alice@example.com</td>
<td>555-1234</td>
</tr>
</tbody>
</table>With row headers Jump to this section
<table>
<thead>
<tr>
<th></th>
<th scope="col">2021</th>
<th scope="col">2022</th>
<th scope="col">2023</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Sales</th>
<td>$100K</td>
<td>$150K</td>
<td>$200K</td>
</tr>
<tr>
<th scope="row">Profit</th>
<td>$20K</td>
<td>$35K</td>
<td>$50K</td>
</tr>
</tbody>
</table>
Grouped rows Jump to this section
<table>
<thead>
<tr>
<th>Department</th>
<th>Employee</th>
<th>Role</th>
</tr>
</thead>
<tbody>
<tr>
<th rowspan="2" scope="rowgroup">Engineering</th>
<td>Alice</td>
<td>Developer</td>
</tr>
<tr>
<td>Bob</td>
<td>Designer</td>
</tr>
</tbody>
<tbody>
<tr>
<th rowspan="2" scope="rowgroup">Marketing</th>
<td>Carol</td>
<td>Manager</td>
</tr>
<tr>
<td>Dave</td>
<td>Analyst</td>
</tr>
</tbody>
</table>
Styling basics Jump to this section
Tables can be styled extensively with CSS to create readable, attractive data presentations.
Basic table styling Jump to this section
| Name | Age |
|---|---|
| Alice | 28 |
| Bob | 34 |
table {
border-collapse: collapse;
width: 100%;
}
th, td {
padding: 12px;
border: 1px solid #e5e7eb;
text-align: left;
}
th {
background: #f9fafb;
font-weight: 600;
}
Striped rows (zebra) Jump to this section
| Name | Role |
|---|---|
| Alice | Developer |
| Bob | Designer |
| Carol | Manager |
.table-striped tbody tr:nth-child(even) {
background: #f9fafb;
}
/* Or odd rows */
.table-striped tbody tr:nth-child(odd) {
background: #f9fafb;
}
Hover effect Jump to this section
| Name | City |
|---|---|
| Alice | NYC |
| Bob | LA |
.table-hover tbody tr {
transition: background 0.2s;
}
.table-hover tbody tr:hover {
background: #eff6ff;
}
Borderless table Jump to this section
| Name | Status |
|---|---|
| Alice | Active |
| Bob | Away |
.table-borderless {
border-collapse: collapse;
}
.table-borderless thead tr {
border-bottom: 2px solid #e5e7eb;
}
.table-borderless tbody tr {
border-bottom: 1px solid #f3f4f6;
}
.table-borderless th,
.table-borderless td {
padding: 12px;
border: none;
}
Compact table Jump to this section
| ID | Name |
|---|---|
| 001 | Alice |
| 002 | Bob |
.table-compact {
font-size: 13px;
}
.table-compact th,
.table-compact td {
padding: 6px 8px;
}
Rounded corners Jump to this section
.table-rounded {
border-collapse: separate;
border-spacing: 0;
border: 1px solid #e5e7eb;
border-radius: 8px;
overflow: hidden;
}
With shadow Jump to this section
.table-shadow {
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
border-radius: 8px;
overflow: hidden;
}
Colored headers Jump to this section
| Name | Status |
|---|---|
| Alice | Active |
| Bob | Away |
.table-colored thead {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
/* Or solid colors */
.table-blue thead {
background: #3b82f6;
color: white;
}
.table-green thead {
background: #10b981;
color: white;
}
Sticky header Jump to this section
| Name | Age |
|---|---|
| Alice | 28 |
| Bob | 34 |
| Carol | 25 |
| Dave | 42 |
| Eve | 31 |
.table-sticky thead {
position: sticky;
top: 0;
z-index: 10;
background: white;
}
/* With container */
.table-container {
max-height: 400px;
overflow-y: auto;
}
Responsive horizontal scroll Jump to this section
| Name | Phone | City | |
|---|---|---|---|
| Alice | alice@example.com | 555-1234 | NYC |
.table-responsive {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.table-responsive table {
min-width: 600px;
}
Card-style table Jump to this section
| Name | Role |
|---|---|
| Alice | Developer |
| Bob | Designer |
.table-cards {
border-collapse: separate;
border-spacing: 0 12px;
}
.table-cards tbody tr {
background: white;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
border-radius: 8px;
}
.table-cards td {
padding: 16px;
}
.table-cards td:first-child {
border-radius: 8px 0 0 8px;
}
.table-cards td:last-child {
border-radius: 0 8px 8px 0;
}
With status badges Jump to this section
| Name | Status |
|---|---|
| Alice | Active |
| Bob | Away |
| Carol | Offline |
.badge {
display: inline-block;
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
}
.badge-success {
background: #dcfce7;
color: #166534;
}
.badge-warning {
background: #fef3c7;
color: #92400e;
}
.badge-danger {
background: #fee2e2;
color: #991b1b;
}
Alternating column colors Jump to this section
| Q1 | Q2 | Q3 | Q4 |
|---|---|---|---|
| $100K | $120K | $150K | $180K |
/* Using colgroup */
col:nth-child(odd) {
background: #f9fafb;
}
/* Or using nth-child on cells */
td:nth-child(odd),
th:nth-child(odd) {
background: #f9fafb;
}
Mobile responsive (stack) Jump to this section
| Name: Alice | Age: 28 | City: NYC |
| Name: Bob | Age: 34 | City: LA |
@media (max-width: 768px) {
.table-mobile {
display: block;
}
.table-mobile thead {
display: none;
}
.table-mobile tbody {
display: block;
}
.table-mobile tr {
display: flex;
flex-direction: column;
margin-bottom: 12px;
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 12px;
}
.table-mobile td {
display: block;
padding: 4px 0;
}
.table-mobile td::before {
content: attr(data-label) ': ';
font-weight: 600;
}
}
JavaScript patterns Jump to this section
Enhance tables with sorting, filtering, pagination, and interactive features using JavaScript.
Sort table columns Jump to this section
| Name ↕ | Age ↕ |
|---|---|
| Alice | 28 |
| Charlie | 22 |
| Bob | 34 |
function sortTable(columnIndex) {
const table = document.getElementById('sortable-table');
const tbody = table.querySelector('tbody');
const rows = Array.from(tbody.querySelectorAll('tr'));
// Toggle sort direction
const isAscending = tbody.dataset.sortDir !== 'asc';
tbody.dataset.sortDir = isAscending ? 'asc' : 'desc';
rows.sort((a, b) => {
const aText = a.cells[columnIndex].textContent;
const bText = b.cells[columnIndex].textContent;
// Try numeric comparison
const aNum = parseFloat(aText);
const bNum = parseFloat(bText);
if (!isNaN(aNum) && !isNaN(bNum)) {
return isAscending ? aNum - bNum : bNum - aNum;
}
// String comparison
return isAscending
? aText.localeCompare(bText)
: bText.localeCompare(aText);
});
// Re-append sorted rows
rows.forEach(row => tbody.appendChild(row));
}
Filter table rows Jump to this section
| Name | Role |
|---|---|
| Alice | Developer |
| Bob | Designer |
| Carol | Manager |
const filterInput = document.getElementById('filter-input');
const table = document.getElementById('filter-table');
const rows = table.querySelectorAll('tbody tr');
filterInput.addEventListener('input', (e) => {
const searchTerm = e.target.value.toLowerCase();
rows.forEach(row => {
const text = row.textContent.toLowerCase();
if (text.includes(searchTerm)) {
row.style.display = '';
} else {
row.style.display = 'none';
}
});
});
Add row dynamically Jump to this section
| Name | Age |
|---|---|
| Alice | 28 |
function addRow() {
const table = document.getElementById('add-row-table');
const tbody = table.querySelector('tbody');
const newRow = tbody.insertRow();
const cell1 = newRow.insertCell(0);
const cell2 = newRow.insertCell(1);
cell1.textContent = 'New Person';
cell2.textContent = '25';
// Or with innerHTML
// newRow.innerHTML = 'New Person 25 ';
}
Delete row Jump to this section
| Name | Action |
|---|---|
| Alice | |
| Bob |
function deleteRow(button) {
const row = button.closest('tr');
row.remove();
}
// Or by index
function deleteRowByIndex(tableId, rowIndex) {
const table = document.getElementById(tableId);
table.deleteRow(rowIndex);
}
Row selection Jump to this section
| Name | |
|---|---|
| Alice | |
| Bob |
function toggleRowSelection(row) {
const checkbox = row.querySelector('.row-checkbox');
checkbox.checked = !checkbox.checked;
if (checkbox.checked) {
row.style.background = '#eff6ff';
} else {
row.style.background = '';
}
updateSelectionCount();
}
function updateSelectionCount() {
const checked = document.querySelectorAll('.row-checkbox:checked');
const count = document.getElementById('selection-count');
count.textContent = `${checked.length} selected`;
}
// Select all
const selectAll = document.getElementById('select-all');
selectAll.addEventListener('change', (e) => {
const checkboxes = document.querySelectorAll('.row-checkbox');
checkboxes.forEach(cb => {
cb.checked = e.target.checked;
const row = cb.closest('tr');
row.style.background = cb.checked ? '#eff6ff' : '';
});
updateSelectionCount();
});
Pagination Jump to this section
| Name | ID |
|---|---|
| Alice | 1 |
| Bob | 2 |
| Carol | 3 |
| Dave | 4 |
| Eve | 5 |
let currentPage = 1;
const rowsPerPage = 2;
function showPage(page) {
const table = document.getElementById('paginated-table');
const tbody = table.querySelector('tbody');
const rows = tbody.querySelectorAll('tr');
const start = (page - 1) * rowsPerPage;
const end = start + rowsPerPage;
rows.forEach((row, index) => {
if (index >= start && index < end) {
row.style.display = '';
} else {
row.style.display = 'none';
}
});
const totalPages = Math.ceil(rows.length / rowsPerPage);
document.getElementById('page-info').textContent =
`Page ${page} of ${totalPages}`;
}
function changePage(direction) {
const table = document.getElementById('paginated-table');
const rows = table.querySelectorAll('tbody tr');
const totalPages = Math.ceil(rows.length / rowsPerPage);
currentPage += direction;
currentPage = Math.max(1, Math.min(currentPage, totalPages));
showPage(currentPage);
}
showPage(1); // Initial load
Editable cells Jump to this section
| Name | Age |
|---|---|
| Alice | 28 |
| Bob | 34 |
// Make cells editable with contenteditable
const cells = document.querySelectorAll('#editable-table td');
cells.forEach(cell => {
cell.setAttribute('contenteditable', 'true');
// Save on blur
cell.addEventListener('blur', () => {
console.log('Saved:', cell.textContent);
// Send to server, update state, etc.
});
// Highlight on focus
cell.addEventListener('focus', () => {
cell.style.background = '#eff6ff';
});
cell.addEventListener('blur', () => {
cell.style.background = '';
});
});
Export to CSV Jump to this section
| Name | Age | City |
|---|---|---|
| Alice | 28 | NYC |
| Bob | 34 | LA |
function exportToCSV() {
const table = document.getElementById('export-table');
const rows = table.querySelectorAll('tr');
let csv = [];
rows.forEach(row => {
const cells = row.querySelectorAll('td, th');
const rowData = Array.from(cells)
.map(cell => `"${cell.textContent}"`)
.join(',');
csv.push(rowData);
});
const csvContent = csv.join('\n');
// Download
const blob = new Blob([csvContent], { type: 'text/csv' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'table-export.csv';
link.click();
URL.revokeObjectURL(url);
}
Calculate totals Jump to this section
| Item | Price |
|---|---|
| Apple | 10 |
| Banana | 5 |
| Orange | 8 |
| Total | $0 |
function calculateTotal() {
const table = document.getElementById('total-table');
const rows = table.querySelectorAll('tbody tr');
let total = 0;
rows.forEach(row => {
const priceCell = row.cells[1];
const price = parseFloat(priceCell.textContent);
if (!isNaN(price)) {
total += price;
}
});
document.getElementById('total-cell').textContent =
'$' + total.toFixed(2);
}
Highlight row on click Jump to this section
| Name | Role |
|---|---|
| Alice | Developer |
| Bob | Designer |
function highlightRow(row) {
// Remove highlight from all rows
const table = row.closest('table');
const rows = table.querySelectorAll('tbody tr');
rows.forEach(r => {
r.style.background = '';
r.classList.remove('selected');
});
// Highlight clicked row
row.style.background = '#dbeafe';
row.classList.add('selected');
}
Load data from JSON Jump to this section
| Name | |
|---|---|
| No data | |
function loadJSON() {
// Sample data (could be from API)
const data = [
{ name: 'Alice', email: 'alice@example.com' },
{ name: 'Bob', email: 'bob@example.com' },
{ name: 'Carol', email: 'carol@example.com' }
];
const table = document.getElementById('json-table');
const tbody = table.querySelector('tbody');
tbody.innerHTML = '';
data.forEach(item => {
const row = tbody.insertRow();
const cell1 = row.insertCell(0);
const cell2 = row.insertCell(1);
cell1.textContent = item.name;
cell2.textContent = item.email;
});
}
// Or from API
async function loadFromAPI() {
const response = await fetch('/api/users');
const data = await response.json();
// Populate table with data
}
