Render any JSON array into a fully interactive HTML table- sortable, searchable, paginated, and exportable.

Plain JavaScript · No dependencies

Drop in two files, pass in your data, and you're done.

This is what you get

A fully interactive table - sort, search, paginate, toggle columns, export CSV.

The real problem isn't building a table.

It's the 20 minutes you spend before you write a single line of code:

Pick a library

DataTables, Simple-DataTables, Grid.js? Compare docs, stars, bundle size…

Read the API

Learn how this specific library wants your data formatted

Fight the defaults

Override styles, disable features you don't need, dig through GitHub issues

Hit a wall

Need one small custom behavior - not supported, or locked behind a paid tier

→ And you still don't own any of it.

"It's just a table.
I don't want to spend an afternoon on this."

Get Json Table Mate - $9

How it compares

DataTables Simple-DataTables Grid.js Json Table Mate
Bundle size (minified) ~180–210KB total
(includes jQuery)
~70–90KB ~50–70KB ~13KB
Bundle size (gzipped) ~50–70KB total
(includes jQuery)
~20–28KB ~12–22KB ~5KB
Columns auto-detected from data
Simple initialization
Modern / readable codebase

Sizes are approximate and vary by build and configuration. Gzipped size reflects typical network transfer size. DataTables requires jQuery, which increases total payload.

What works out of the box

Column Sorting

Click any header to sort. Handles strings, numbers, and nulls correctly.

🔍
Live Search

Filters rows as you type. Searches across all columns simultaneously.

📄
Pagination

Configurable page size. Handles hundreds of rows cleanly.

👁
Column Toggle

Users can show/hide individual columns. Useful for wide datasets.

Export to CSV

Downloads the current filtered, sorted view.

🎨
Custom Cell Rendering

Pass a render function per column. Return any HTML you like.

📱
Responsive

Horizontally scrollable on small screens.

Search Highlighting

Matching text is highlighted as you type.

Row Click Callback

Pass an onRowClick handler and get the full row object on click.

🔧
Themeable

Override 7 CSS variables to restyle everything - no library changes needed.

How to use it

Two files. Drop them in and go. Every option has a default. Nothing is required.

1
Add these three lines to your HTML
<html>
<head>
  <meta charset="UTF-8">  <!-- 1 of 3: charset (omitting this breaks icons) -->
  <link rel="stylesheet" href="json-table-mate-minified.css">  <!-- 2 of 3: CSS -->
</head>
<body>

  <!-- your HTML and script go here (steps 2 & 3) -->

  <script src="json-table-mate-minified.js"></script>  <!-- 3 of 3: JavaScript -->
</body>
</html>
2
Add the HTML elements
<!-- textarea for JSON input -->
<textarea id="jsonInput"></textarea>

<!-- button calls your render function -->
<button onclick="renderTable()">Render Table</button>

<!-- optional: shows JSON parse errors -->
<span id="inputError"></span>

<!-- container where the table renders -->
<div id="my-table"></div>
3
Write the render function
<script>
function renderTable() {
  JsonTableMate.renderTable(
    document.getElementById('jsonInput'),     // textarea (input)
    document.getElementById('my-table'),      // container (output)
    {
      // every option has a default — nothing is required
      isSortable:          true,
      isSearchable:        true,
      isExportable:        true,
      hasColumnToggle:     true,
      defaultPageSize:     25,
      defaultSortBy:       'name',
      onRowClick:          (row) => console.log(row),
      columns: {
        first_name: { label: 'First Name' },
        internal_id: { hidden: true },
        price: { render: (val) => '$' + val.toFixed(2) },
      },
    },
    document.getElementById('inputError')   // error display (optional)
  );
}
</script>
Already have data? Use JsonTableMate.render() directly

If your data comes from a fetch or a JS variable, skip the textarea and button entirely:

<script>
JsonTableMate.render(
  document.getElementById('my-table'),
  yourDataArray,                  // plain JS array of objects
  { isSortable: true, isSearchable: true }
);
</script>
All options
Option Type Default Description
Sorting & Search
isSortable boolean true Click column headers to sort ascending/descending
defaultSortBy string null Column name to sort by on load; table still renders if the column doesn't exist (logs a console warning)
defaultSortDirection string 'asc' Initial sort direction - 'asc' or 'desc'
isSearchable boolean true Show a search box that filters all columns as you type
Pagination
isPaginated boolean true Set to false to show all rows at once with no pagination controls
defaultPageSize number 10 Number of rows per page on initial load
paginationStyle string 'full' 'full' shows numbered page buttons with ellipsis; 'simple' shows Prev / Next only
hasPageSizeSelector boolean true Show a dropdown letting users change the number of rows per page
pageSizeOptions number[] [10, 25, 50, 100] Choices in the page-size dropdown (requires hasPageSizeSelector)
Toolbar
hasColumnToggle boolean true Show a dropdown letting users hide/show individual columns
isExportable boolean true Show an Export CSV button; downloads the current filtered, sorted view
exportFilename string 'export.csv' Filename for the downloaded CSV file
Interaction and Formatting
onRowClick function null Called with the full row object when a row is clicked; adds a pointer cursor to rows
tableClass string 'jtm-table' CSS class(es) on the <table> element - swap in Bootstrap or any other framework's classes
columns object {} Per-column config: label, hidden, render - see below
Per-column configuration

Use the columns object to set a display label, hide a column by default, or override how cells are rendered. All three keys are optional and can be combined.

columns: {
  // label - display name shown in the header (field name used internally)
  first_name: { label: 'First Name' },

  // hidden - start this column hidden (user can re-show via Columns toggle)
  internal_id: { hidden: true },

  // render - custom cell output; return HTML or plain text
  price: { render: (val) => val != null ? '$' + val.toFixed(2) : '-' },
  active: { render: (val) => val ? '✓' : '✗' },
  name: { label: 'Full Name', render: (val, row) => `<a href="/item/${row.id}">${val}</a>` },
}

// render(val, row) - val is the cell value, row is the full data object.
// Returns a string. HTML is rendered directly - see security note below.
Security note: The render function injects its return value as innerHTML. If your data comes from user input or an untrusted source, escape values inside your render function before returning them. Cells without a custom render are always safe - the library escapes them automatically.
API reference
Method / PropertyDescription
JsonTableMate.renderTable(inputEl, containerEl, options, errorEl) Reads JSON from a <textarea>, validates it, and renders the table. options and errorEl are optional.
JsonTableMate.render(containerEl, dataArray, options) Renders directly from a JS array - no textarea needed. Use this when your data comes from a fetch or a variable.
JsonTableMate.destroy(containerEl) Tears down the table, removes event listeners, and clears the container. Use before removing the element from the DOM (ex. in a SPA).
Theming with CSS variables

Override any of these on :root (or a parent element) to restyle the table without touching the library CSS.

VariableDefaultControls
--jtm-accent#0d6efdActive page button, sort icon, focus rings
--jtm-border#dee2e6Table border, toolbar button borders, select borders
--jtm-header-bg#f8f9faColumn header background, button hover background
--jtm-stripe-bg#f8f9faEven-row stripe background
--jtm-hover-bg#e8f0feRow hover background
--jtm-highlight-bg#fff3cdSearch match highlight background
--jtm-muted#6c757dInfo text, empty-state text, ellipsis color
/* example: green theme - place this after your stylesheet link */
:root {
  --jtm-accent:       #198754;
  --jtm-border:       #a3cfbb;
  --jtm-header-bg:    #198754;
  --jtm-stripe-bg:    #f0faf5;
  --jtm-hover-bg:     #a3cfbb;
  --jtm-highlight-bg: #d1fae5;
}

/* header text color - not covered by a variable */
.jtm-table thead th {
  color: #fff;
}

You own the code.

You get the full, readable source. Don't like something? Open the file and change it. No build step, no abstraction layers, no pull requests.

That's what $9 buys you: code you understand and control, permanently.

// All options, readable in one place:

const opts = {
  isSortable:            options.isSortable           !== false,
  isSearchable:          options.isSearchable         !== false,
  defaultSortBy:         options.defaultSortBy        ?? null,
  defaultSortDirection:  options.defaultSortDirection ?? "asc",
  isPaginated:           options.isPaginated          !== false,
  defaultPageSize:       options.defaultPageSize      ?? 10,
  paginationStyle:       options.paginationStyle      ?? "full",
  hasPageSizeSelector:   options.hasPageSizeSelector  ?? true,
  pageSizeOptions:       options.pageSizeOptions      ?? [10, 25, 50, 100],
  hasColumnToggle:       options.hasColumnToggle      ?? true,
  isExportable:          options.isExportable         ?? true,
  exportFilename:        options.exportFilename       ?? "export.csv",
  onRowClick:            options.onRowClick           ?? null,
  tableClass:            options.tableClass           ?? "jtm-table",
  columns:               options.columns              ?? {},
};

// Every line is this readable. All of it.

What's included

One download. Everything you need.

  • ✓  json-table-mate.js - readable source, ~15KB
  • ✓  json-table-mate.css - styles + 7 CSS variables for theming
  • ✓  demo/ - four working demos: default, Bootstrap, custom theme, hardcoded data
  • ✓  Personal and commercial use - no attribution required
$9
One-time. No subscription. No upsell.
Buy Now - $9

Not sure? Try the live demo first.

You could copy the minified file from this demo. I won't stop you. But you'd get 13KB of unreadable soup — no comments, no structure, no way to customize anything. Reverse-engineering it will cost you more than an afternoon. The full, readable source is only $9.