JSON to HTML Table: Display API Data on Web Pages [2026]
JSON arrays from REST APIs are the most common data source in modern web development. Yet displaying that data clearly for end users almost always means turning it into an HTML table. Whether you are building a dashboard, a report view, or a simple admin page, the pattern is the same: fetch JSON, render a table.
This guide covers the full picture — from understanding JSON array structure, to manually writing a table, to dynamically generating one with JavaScript, handling nested objects, adding proper accessibility attributes, and dealing with large datasets through pagination and sorting.
1. Why Convert JSON to HTML Tables?
JSON is a data interchange format — it is designed for machines to parse, not humans to read. A raw JSON response from an API looks like a wall of nested braces and brackets that is difficult to scan at a glance. An HTML table turns the same data into a grid with clearly labeled columns, aligned values, and visual rows that humans can compare and analyze instantly.
Tables are the right choice when:
- The data is a list of uniform objects (same keys across all items)
- Users need to scan, compare, or sort multiple records simultaneously
- The dataset has more than a handful of rows but is not so large that pagination is complex
- The data will be exported or printed (tables translate naturally to spreadsheets and PDFs)
Tables are the wrong choice when the JSON is a deeply nested configuration object, a single record with many fields, or a graph-like structure with many-to-many relationships. For those cases, a formatted JSON viewer (like the SnapUtils JSON Formatter) or a custom layout is more appropriate.
2. JSON Array Structure for Tables
The ideal JSON structure for a table is a flat array of objects where every object has the same set of keys. Each object becomes a table row; each key becomes a column header.
[
{ "id": 1, "name": "Alice Johnson", "role": "Engineer", "team": "Platform", "active": true },
{ "id": 2, "name": "Bob Smith", "role": "Designer", "team": "Product", "active": true },
{ "id": 3, "name": "Carol White", "role": "Manager", "team": "Platform", "active": false }
]
This structure maps perfectly to a table:
- The array's length determines the number of rows
- The keys of the first object (
id,name,role,team,active) become the column headers - Each object's values fill the corresponding cells
Real API responses often wrap this array in an envelope: { "data": [...], "total": 100, "page": 1 }. Extract the array before passing it to your table renderer.
3. Manual HTML Table from JSON
For static or rarely-changing data, writing the HTML table directly is the simplest approach. Here is the table corresponding to the JSON above:
<table>
<caption>Team Members</caption>
<thead>
<tr>
<th scope="col">ID</th>
<th scope="col">Name</th>
<th scope="col">Role</th>
<th scope="col">Team</th>
<th scope="col">Active</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>Alice Johnson</td>
<td>Engineer</td>
<td>Platform</td>
<td>Yes</td>
</tr>
<tr>
<td>2</td>
<td>Bob Smith</td>
<td>Designer</td>
<td>Product</td>
<td>Yes</td>
</tr>
<tr>
<td>3</td>
<td>Carol White</td>
<td>Manager</td>
<td>Platform</td>
<td>No</td>
</tr>
</tbody>
</table>
Note the structural elements: <caption> describes the table, <thead> groups header rows, <tbody> groups data rows, and scope="col" on each <th> tells screen readers that this is a column header.
Inspect and Format Your JSON First
Before converting JSON to a table, use the SnapUtils JSON Formatter to validate, format, and explore the structure of your JSON data. Identify keys, check for nulls, and understand nesting — all before writing a single line of table code.
Open JSON Formatter4. JavaScript: Dynamically Generating Tables from JSON
For API data that changes at runtime, build the table in JavaScript. The pattern is always the same: extract column names from the first object, create the header row, then iterate all objects to create data rows.
/**
* Renders a JSON array of objects into an HTML table.
* @param {Object[]} data - Array of flat objects with uniform keys
* @param {HTMLElement} container - DOM element to insert the table into
*/
function renderTable(data, container) {
if (!data || data.length === 0) {
container.innerHTML = '<p>No data to display.</p>';
return;
}
const columns = Object.keys(data[0]);
const table = document.createElement('table');
// Caption
const caption = document.createElement('caption');
caption.textContent = `${data.length} records`;
table.appendChild(caption);
// Header row
const thead = document.createElement('thead');
const headerRow = document.createElement('tr');
columns.forEach(col => {
const th = document.createElement('th');
th.scope = 'col';
th.textContent = col.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
headerRow.appendChild(th);
});
thead.appendChild(headerRow);
table.appendChild(thead);
// Data rows
const tbody = document.createElement('tbody');
data.forEach(row => {
const tr = document.createElement('tr');
columns.forEach(col => {
const td = document.createElement('td');
const value = row[col];
td.textContent = value === null || value === undefined ? '—' : String(value);
tr.appendChild(td);
});
tbody.appendChild(tr);
});
table.appendChild(tbody);
container.innerHTML = '';
container.appendChild(table);
}
// Usage: fetch from API and render
fetch('https://api.example.com/users')
.then(res => res.json())
.then(json => renderTable(json.data, document.getElementById('tableContainer')))
.catch(err => console.error('Failed to load data:', err));
This implementation handles null and undefined values gracefully (displaying a dash), formats column names by converting underscores to spaces and capitalizing words, and creates all DOM elements safely to avoid XSS vulnerabilities from user data.
5. Handling Nested JSON Objects
Real-world API responses frequently contain nested objects. A user object might look like:
{
"id": 42,
"name": "Alice Johnson",
"address": {
"city": "Austin",
"state": "TX",
"zip": "78701"
},
"tags": ["admin", "beta-user"]
}
You have three strategies for handling address and tags:
Strategy 1: Flatten the Object
Recursively flatten nested objects into dot-notation keys before rendering. address.city, address.state, address.zip each become their own column.
function flattenObject(obj, prefix = '') {
return Object.entries(obj).reduce((acc, [key, value]) => {
const fullKey = prefix ? `${prefix}.${key}` : key;
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
Object.assign(acc, flattenObject(value, fullKey));
} else {
acc[fullKey] = Array.isArray(value) ? value.join(', ') : value;
}
return acc;
}, {});
}
const flatData = apiData.map(flattenObject);
renderTable(flatData, container);
Strategy 2: Stringify Nested Values
For objects and arrays you do not want to flatten, call JSON.stringify(value) and display the raw JSON string in the cell. This is quick but can produce unreadable output for complex nested structures.
Strategy 3: Nested Tables
For structured nested data (like an array of address objects), render a nested <table> inside the parent cell. This preserves structure but can become visually overwhelming for deep nesting. Use sparingly.
6. Styling Tables for Readability
An unstyled HTML table is functional but hard to read. A few CSS rules transform it significantly:
table {
width: 100%;
border-collapse: collapse;
font-family: system-ui, sans-serif;
font-size: 0.9rem;
}
caption {
text-align: left;
font-weight: 600;
padding: 0.5rem 0;
color: #666;
}
th, td {
padding: 0.75rem 1rem;
text-align: left;
border-bottom: 1px solid #e2e8f0;
}
thead th {
background: #f8fafc;
font-weight: 600;
color: #374151;
white-space: nowrap; /* prevent header wrapping */
}
tbody tr:hover {
background: #f1f5f9; /* subtle hover highlight */
}
tbody tr:last-child td {
border-bottom: none;
}
/* Overflow container for wide tables */
.table-container {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
Key readability principles:
- Use
border-collapse: collapseto eliminate double borders between cells - Apply subtle alternating row colors or hover states to aid row tracking
- Wrap wide tables in an
overflow-x: autocontainer for mobile - Keep header text from wrapping with
white-space: nowrap - Right-align numeric columns for easier visual comparison
7. Accessibility in Data Tables
An HTML table that is visually clear but inaccessible fails a significant portion of users. Screen readers like VoiceOver and NVDA navigate tables by reading cell values along with their associated headers. Getting this right requires a few specific techniques.
Use caption
The <caption> element is the table's title. Screen readers announce it when the user enters the table. Always include one that describes what the table contains: "Q1 2026 Sales Data" rather than "Table 1".
Use scope on th Elements
The scope attribute tells screen readers whether a <th> is a header for its column (scope="col") or its row (scope="row"). Without scope, screen readers may not correctly associate headers with their cells.
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Department</th>
<th scope="col">Start Date</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Alice Johnson</th> <!-- row header -->
<td>Engineering</td>
<td>2023-03-15</td>
</tr>
</tbody>
Use thead, tbody, and tfoot
These semantic grouping elements allow screen readers to correctly identify which rows are headers and which are data. They also allow browsers to repeat the header row when printing multi-page tables.
Sufficient Color Contrast
WCAG 2.1 requires a contrast ratio of at least 4.5:1 for normal text and 3:1 for large text. Use the SnapUtils Color Contrast Checker to verify your table's text and background combinations meet this standard.
sortable Column Announcements
When adding sort functionality, use aria-sort on the currently sorted column header: aria-sort="ascending" or aria-sort="descending". This announces the current sort state to screen reader users.
8. Pagination and Large Datasets
Rendering thousands of rows in a single table is a performance problem — parsing large DOM trees is slow, and scrolling through hundreds of rows is a poor user experience. Pagination is the standard solution.
Client-Side Pagination
When all data is loaded into memory, slice the array and render only the current page's slice:
const PAGE_SIZE = 25;
let currentPage = 0;
let allData = []; // full dataset loaded from API
function renderPage(page) {
const start = page * PAGE_SIZE;
const end = start + PAGE_SIZE;
const pageData = allData.slice(start, end);
renderTable(pageData, document.getElementById('tableContainer'));
updatePaginationControls(page, Math.ceil(allData.length / PAGE_SIZE));
}
function updatePaginationControls(current, total) {
document.getElementById('pageInfo').textContent = `Page ${current + 1} of ${total}`;
document.getElementById('prevBtn').disabled = current === 0;
document.getElementById('nextBtn').disabled = current === total - 1;
}
document.getElementById('prevBtn').addEventListener('click', () => {
if (currentPage > 0) renderPage(--currentPage);
});
document.getElementById('nextBtn').addEventListener('click', () => {
if (currentPage < Math.ceil(allData.length / PAGE_SIZE) - 1) renderPage(++currentPage);
});
Server-Side Pagination
For datasets too large to load entirely, request only the current page from the API using ?page=2&limit=25 query parameters. Update the table with fresh data whenever the user navigates pages.
Virtual Scrolling
For very large datasets that require a continuous scroll experience, virtual scrolling (also called windowed rendering) renders only the rows currently visible in the viewport. Libraries like TanStack Virtual handle this efficiently. The DOM always contains a small fixed number of row elements regardless of total dataset size.
For further reading on working with JSON data structures, see the JSON syntax guide and the common JSON errors guide.
Format and Validate JSON Before Building Your Table
Paste your JSON into the SnapUtils JSON Formatter to pretty-print, validate, and explore the structure — check key names, spot nulls, and understand nesting before writing any table code.
Open JSON Formatter Free9. Frequently Asked Questions
How do I display JSON data as an HTML table?
The most straightforward approach: use JavaScript to extract the column names from the first object in your JSON array, then iterate over all objects to build <tr> and <td> elements. Wrap the result in a <table> with a <caption>, a <thead> with scope="col" on each header, and a <tbody> for data rows. For static data, you can write the table HTML manually from your JSON values without any JavaScript at all.
Can I render nested JSON as a table?
Yes. The most practical approach is to flatten nested objects into dot-notation columns before rendering (address.city, address.zip). For arrays within objects, join the array values with a comma or render them as a comma-separated string. For deeply nested structures, a nested <table> inside a cell works but can become unreadable. When nesting is deeper than two levels, a tree view or the JSON Formatter is usually a better visualization choice than a flat table.
What HTML attributes make tables accessible?
The critical ones are: scope="col" on column header <th> elements, scope="row" on row header <th> elements, a <caption> element describing the table's content, and semantic grouping with <thead>, <tbody>, and optionally <tfoot>. For sortable columns, add aria-sort="ascending" or aria-sort="descending" to the currently sorted header. For complex tables with irregular header relationships, use id and headers attributes to explicitly associate cells with their headers.
How do I sort an HTML table generated from JSON?
Sort the source JSON array before rendering rather than re-ordering DOM nodes, which is slower and more complex. Add a click handler to each column header that (1) sorts the data array by the clicked column key — ascending on first click, descending on second — then (2) re-renders the table body from the newly sorted array. Track sort state (column name and direction) in a variable and update the aria-sort attribute on the header element to reflect the current sort order for screen readers.