{datatable}
Rows as a complete HTML table - formatted numbers, a totals row, sorting and paging on the server.
Syntax
{datatable data='name', columns='a, b, c', labels='A, B, C', totals='c'}
{datatable data='name', format="c:currency('EUR')", sortable, striped, caption='Caption'}
{datatable data='name', rows=10, query='prefix_'}
{datatable totals='amount'} month,amount ... {/datatable}
Options
-
data -
The rows: a
{data}store, a page array or a_datafile. As a pair the content is the rows instead - JSON, YAML, XML or CSV, told apart on sight. -
columns - The fields shown, in their order. Every field of the first row when not given.
-
labels -
The header of each column, in the order of
columns. The field name as a headline when not given (order_dateis Order Date). -
format -
A pipe per column,
column:pipeseparated by commas -total:money, date:date('j M'). -
totals - Columns summed in a totals row - over every row, not only the page shown.
-
sortable -
Bare option: every header is a link that sorts on the server,
?sort=total&dir=desc. -
rows -
Rows per page, with page links below (
?page=2). -
query -
A prefix for the request names
sort,dirandpage- for two tables on one page. -
striped - Bare option: every other row shaded.
-
caption -
The table's
<caption>.
Description
{datatable} writes a whole table from rows of data: a header from the field names, a body, an optional totals row, header links that sort and page links that page - all on the server, without JavaScript. The rows come from data= (a {data} store, a page array or a _data file, as for {chart}), or, as a pair, from the content: JSON, YAML, XML or CSV, told apart on sight. The tag writes the table once - it does not iterate the rows like a data tag does.
Columns and numbers
columns= picks the fields and their order, labels= gives their headers; otherwise every field of the first row shows, its name made a headline. A column whose values are all numbers is right-aligned, written with the most decimals any of its values has and grouped by thousands - unless its name says it identifies (id, number, code, year ...), then the digits stand as they are. format= gives a column a pipe instead, run as {echo $value | pipe}. totals= sums columns in a last row, over all rows. Everything from the data is escaped.
Sorting and paging
With sortable each header is a link to the same page with ?sort=column&dir=asc or desc. Only a column the table shows is accepted - anything else leaves the rows as they came - and the sort is stable; the sorted column carries aria-sort. rows= shows that many rows a page with page links below, built from the {pager}'s items. The links keep the request's other values. query= prefixes the three request names, so two tables on one page sort and page apart.
A table without rows answers nothing, and its @else@ part shows. Under the strict check these are errors: no data at all, a column no row has, a totals= or format= naming a column the table does not show, a total of a column that is not numbers, a number of labels that differs from the number of columns, and rows= below 1.
Examples
Columns, a format and totals
Columns picked, a date pipe on one, the numbers aligned and summed in a totals row.
{datatable data='orders', columns='number, date, customer, items, total', totals='items, total', format="date:date('j M Y')", striped, caption='Orders this week'}
<?php
$orders = [
[ 'number' => 10412, 'date' => '2026-10-05', 'customer' => 'Atelier Graphique', 'items' => 3, 'total' => 1245.5 ],
[ 'number' => 10413, 'date' => '2026-10-05', 'customer' => 'Signal Gift Stores', 'items' => 1, 'total' => 89.9 ],
[ 'number' => 10414, 'date' => '2026-10-06', 'customer' => 'Mini Wheels Co.', 'items' => 12, 'total' => 4310 ],
[ 'number' => 10415, 'date' => '2026-10-07', 'customer' => 'Land of Toys Inc.', 'items' => 5, 'total' => 642.25 ] ];
?>
| Number | Date | Customer | Items | Total |
|---|---|---|---|---|
| 10412 | 5 Oct 2026 | Atelier Graphique | 3 | 1,245.50 |
| 10413 | 5 Oct 2026 | Signal Gift Stores | 1 | 89.90 |
| 10414 | 6 Oct 2026 | Mini Wheels Co. | 12 | 4,310.00 |
| 10415 | 7 Oct 2026 | Land of Toys Inc. | 5 | 642.25 |
| Total | 21 | 6,287.65 |
Rows as content
As a pair the content is the data - here CSV - with a currency pipe and a totals row.
{datatable totals='budget, spent', format="budget:currency('EUR'), spent:currency('EUR')", caption='Marketing budget'}
channel,budget,spent
Search ads,12000,11380.40
Social media,8500,9120.15
Newsletter,1500,960
{/datatable}
| Channel | Budget | Spent |
|---|---|---|
| Search ads | €12,000.00 | €11,380.40 |
| Social media | €8,500.00 | €9,120.15 |
| Newsletter | €1,500.00 | €960.00 |
| Total | €22,000.00 | €21,460.55 |
Sortable and paged
Four rows a page with page links, headers that sort - plain links, under their own prefix.
{datatable data='cities', labels='City, Country, Population', rows=4, sortable, query='city_'}
<?php
$cities = [
[ 'city' => 'Shanghai', 'country' => 'China', 'population' => 24870895 ],
[ 'city' => 'Beijing', 'country' => 'China', 'population' => 21893095 ],
[ 'city' => 'Kinshasa', 'country' => 'DR Congo', 'population' => 16315534 ],
[ 'city' => 'Istanbul', 'country' => 'Türkiye', 'population' => 15655924 ],
[ 'city' => 'Tokyo', 'country' => 'Japan', 'population' => 14094034 ],
[ 'city' => 'Moscow', 'country' => 'Russia', 'population' => 13010112 ],
[ 'city' => 'Mumbai', 'country' => 'India', 'population' => 12442373 ],
[ 'city' => 'Jakarta', 'country' => 'Indonesia', 'population' => 10562088 ] ];
?>
| City | Country | Population |
|---|---|---|
| Tokyo | Japan | 14,094,034 |
| Moscow | Russia | 13,010,112 |
| Mumbai | India | 12,442,373 |
| Jakarta | Indonesia | 10,562,088 |
See also
- {pager} Page links for a tag written with the page option: first and last, a window round the current page, previous and next.
- {chart} Draws a chart as inline SVG on the server - 46 kinds, from bars and lines to sankeys, maps of a hierarchy and org charts.
- {data} Turns its content - JSON, XML, YAML, CSV, a list or a range - into a named data set for a later tag to iterate.