Craft CMS 5 plugin
Build a table once, use it anywhere
Authors edit tables in a spreadsheet-style grid. Visitors get a real, accessible <table> that sorts, searches and pages — without you writing table markup or wiring up DataTables.
Drop the same table into CKEditor, into Redactor, into a field, or into a template. Edit it in one place and every one of them changes.
Free Lite edition · Craft CMS 5.3+ · PHP 8.2+ · no runtime dependencies
Tables are the thing Craft leaves to you
Every content site ends up needing one. A price list. A spec sheet. Shipping rates. Opening hours. And every time, the same four bad options.
Let them write HTML
A <table> typed into a rich-text field survives exactly until someone pastes over it. No sorting, no responsive behaviour, and a source-code view is not an editing experience.
Use the editor’s table button
Better to edit, but the table now lives inside one entry. The same price list on three pages is three tables, and they will disagree within a month.
Build a Matrix field
A row per block, a field per column — and a new column means a schema change. Authors paginate through rows in a stack of cards instead of seeing their data as a grid.
Wire up DataTables yourself
Now you own a jQuery dependency, a sorting configuration, and a table that sorts “$1,200.00” before “$9.99” because it is comparing strings.
Legs gives tables their own home: a library, a proper editor, and one way to place them that works in every editor at once.
One grid, two audiences
On the left, what an author edits. On the right, what a visitor gets. Nothing in between is your job.
In the control panel
| A | B | C | |
|---|---|---|---|
| 1 | Plan | Price | Seats |
| 2 | Basic | $10.00 | 1 |
| 3 | Team | $1,200.00 | 25 |
| 4 | Enterprise | $4,000.00 | 250 |
| 5 | Total | =SUM(B2:B4) | =SUM(C2:C4) |
Keyboard navigation, paste a whole spreadsheet into any cell, undo and redo. Cells starting with = are calculated when the table renders.
On the page
| Plan ↕ | Price ↑ | Seats ↕ |
|---|---|---|
| Basic | $10.00 | 1 |
| Team | $1,200.00 | 25 |
| Enterprise | $4,000.00 | 250 |
| Total | $5,210.00 | 276 |
Sorted by price — numerically, because the server worked out that $1,200.00 is 1200 and wrote it onto the cell. With JavaScript off, the visitor still gets every row.
And to put it on a page, one tag — in CKEditor, in Redactor, in any rich-text field:
Four ways in, one table
However it gets onto the page, it is the same table — so editing it once changes all of them.
In CKEditor
A toolbar button opens the table picker and drops in a card an author can see. Set options for this one appearance behind the gear — compact here, paginated there — without touching the table itself.
{legs:prices:render(compact,perPage=10)}
In Redactor
The same button, the same picker, the same block. Content moves between a Redactor field and a CKEditor field with nothing to migrate, because neither of them is what renders it.
<div class="legs-embed">{legs:prices:render}</div>
As a field
A Legs field holds either its own inline table — a spec sheet that belongs to one product — or a reference to one or more tables from the library, arranged in the author’s order.
{{ entry.spec.render() }}
In a template
A variable, a filter and a function, all landing on the same renderer. Pass overrides for one appearance, or copy the template into your site and take the markup over entirely.
{{ craft.legs.render('prices') }}
How It Works
Four decisions, each of which earns its place.
Craft Already Knew How to Do This
Craft parses reference tags over every rich-text value it renders. Make tables real elements and {legs:prices:render} works in CKEditor, in Redactor, and in any other field built on Craft’s HTML field — with no template change, no output filter, and no per-editor rendering code. Delete the editor plugin and every embed keeps rendering.
The Sorting Is Decided on the Server
A column of “$1,200.00” and “$9.99” is not a column of strings. Legs looks at the values in each column, works out whether they are numbers, dates or text, and writes the comparable value onto every cell before the page is sent. The browser sorts what it is given — it never has to guess what a currency symbol means.
The Table Is Complete Before the JavaScript Runs
The server renders every row, with a real <caption>, real <th scope>, and a real <tfoot>. The runtime is enhancement: it adds sorting, a search box and pagination to a table that already worked. Turn JavaScript off and a visitor still gets all the data; a crawler always does.
A Table Can Be a Query
Point a table at an element query — entries, categories, Commerce products — and give each column an attribute, a field or an object template. The grid is materialised, not resolved at render, so a table on a busy page never runs a query per visitor. Saving a matching element rebuilds it on a short debounce, so a resave of 400 entries costs one rebuild.
What’s in the box
The parts you would otherwise assemble yourself, already agreeing with each other.
A spreadsheet editor
Keyboard navigation, insert and delete rows and columns, header and footer rows, transpose, undo and redo. Tab off the last cell and the table grows.
Paste from anywhere
Paste a block of cells from Excel, Numbers or Google Sheets into any cell and the grid fills out from there, growing to fit.
Import and export
CSV and TSV with the delimiter sniffed, JSON, an HTML table with its merged cells intact, and XLSX when you want it.
Sorting, search, pagination
One dependency-free ES module, loaded only on pages that need it. Sort values are computed on the server so formatted numbers and dates sort correctly.
Responsive by decision
Too wide for the screen? Scroll sideways, or stack each row into a labelled card. The labels come from the header row, so they are always right.
Formulas
=SUM(B2:B9), =AVERAGE, =ROUND and friends, evaluated at render. The grid keeps what the author typed; only the page sees the number.
Element-query tables
Build a table from entries, categories or products, rebuilt on a debounce when a matching element is saved — or on a schedule from cron.
Per-site content
Every table exists on every site. Each one chooses whether it says the same thing everywhere or gets translated, with Craft’s own site switcher.
Your markup, if you want it
Copy one Twig template into your site and it takes over. Everything that needs a decision has already been made and handed to you.
Installing it
Two commands, no build step, no runtime dependencies.
composer require justinholtweb/craft-legs
php craft plugin/install legs
Then Legs → Tables → New table. If you use CKEditor, drag the Table button into the field’s toolbar; Redactor gets its button automatically.
Pricing
Lite is free and genuinely useful — the editor and the embedding are both in it, because a table you cannot put on a page is not a table.
Lite
Free
- ✓ Three tables in the library
- ✓ The full grid editor
- ✓ Embedding in CKEditor and Redactor
- ✓ The field type, both modes
- ✓ CSV, JSON and HTML in and out
- ✓ Click-to-sort on the front end
- ✓ Per-site content and per-embed options
Pro
$29
one-off · $19/year for updates
- ✓ Unlimited tables
- ✓ Search and pagination
- ✓ Responsive stacking
- ✓ Merged cells
- ✓ Formulas
- ✓ Element-query tables and scheduled refresh
- ✓ XLSX import and export
If a Pro licence lapses, Legs downgrades rather than breaks: your tables and their settings are left exactly as they are, the control panel keeps showing them, and the front end serves the Lite feature set until you renew.
Frequently Asked Questions
The ones worth answering before you install it.
That table lives inside one entry. The same price list on three pages is three tables, and they diverge. A Legs table lives in a library, is embedded by reference, and is edited in one place — and it arrives on the page as markup that sorts, searches and stacks, which an editor-built table never does.
No. Rendering is Craft’s own reference-tag parsing, which knows nothing about either editor — an embed keeps working if you remove the editor plugin, move the content to a different rich-text field, or type the tag by hand. The toolbar buttons are a convenience for authors, not a dependency.
It renders a real table: a <caption>, <thead>/<tbody>/<tfoot>, scope on every header cell, and aria-sort maintained as a visitor sorts. If you would rather write your own, copy one Twig template into your site and Legs uses yours.
Turn its stylesheet off in the settings and nothing of Legs’ own styling loads — the markup and the runtime work without it. Or keep it and retheme with custom properties: set --legs-accent, --legs-border and friends on .legs and you are done.
On the server. Legs looks at the values in each column, decides whether they are numbers, dates or text, and writes a comparable value onto each cell. “$1,200.00”, “1.234,56” and “(450)” all sort as the numbers they are, and empty cells sort last in both directions rather than pretending to be zero.
Comfortably into the thousands of rows — the grid is one JSON document, read whole, and pagination keeps the DOM small. Tens of thousands of rows is not what this is for; that is a listing with filters, and Caffeine is the plugin for it.
Yes — point it at an element query and give each column an attribute, a custom field or an object template. It is materialised rather than resolved at render, so the page never runs a query, and it rebuilds on a debounce when a matching element is saved. That is a Pro feature.
Every table exists on every site, so an embed always resolves. Each table then chooses whether it says the same thing everywhere or is translated per site, and the editor gets Craft’s usual site switcher. A new translation starts as a copy, so an author opens something to edit rather than an empty grid.
Yes. A reference tag can carry options for that one appearance — {legs:prices:render(compact,perPage=10)} — and anything you leave out follows the table’s own settings. The toolbar buttons write the syntax for you.
Supported, via PhpSpreadsheet, which Legs suggests rather than requires: it is a ~10 MB dependency and a free install that only ever pastes from Google Sheets should not pay for it. Run composer require phpoffice/phpspreadsheet and the option appears.
Legs downgrades rather than breaks. Your tables and their settings are untouched and the control panel keeps showing them; the front end serves the Lite feature set — search and pagination off, stacking falls back to scrolling — until you renew, and then everything is exactly as it was.
Lite gives you three tables with the whole editor, the whole embedding story and click-to-sort. That is a real price list, a real spec table and a real shipping table, properly done. Pro is for when three is not enough or you want search, formulas and query-backed tables.
Give your tables somewhere to live
Free Lite edition, or $29 for everything.
Craft CMS 5.3+ · PHP 8.2+ · no runtime dependencies