Tables
A repeating structure inside an object: slides in a slider, product specs, team members. A table is described once, then attached to a block through a Table field — and the editor fills in its rows from the block's own form.
PageBlocks → Constructor → Tables.
The table constructor
| Setting | What it does |
|---|---|
| Table name | How the table is labelled when picking one. Required |
| Model | Where the rows live — see below. Required |
| Pagination | Break the list into pages. On by default |
| Search | A search box above the list — and the same box makes the table a source of the global search |
| Moderation | New rows wait for review — see Moderation. The box is absent while the pageblocks_moderation setting is off |
Tabs: Fields, Tabs, Columns, Search, Filtering, Actions and API.
The split is worth getting straight early: fields are what an editor fills in, columns are what they see in the list. They are separate sets and need not match — a field can be searchable or filterable without having a column at all.
No separate database table is created
Rows from every table live in one pb_table_data, with the values as JSON in the data column. Ownership is three references: model_type / model_id for the owning block, constructor_id for the table, field_id for the field.
What follows from that
A table costs nothing to create: no migration, no schema change. The flip side is that searching by a "column" of such a table means JSON_EXTRACT — there is no ordinary index on it. For dozens of rows per block that is irrelevant; for tens of thousands it is a reason to consider a model of your own.
Columns
| Setting | What it does |
|---|---|
| Column type | A field, a group of fields, or the first non-empty of several |
| Field | Which field to show |
| Caption | Overrides the field's label |
| Width | In pixels, up to 600. 0 means fit automatically |
| Renderer | How to present the value |
| Default value | What to show when the value is empty |
| Sortable | Allow sorting by this column |
Renderers: text (long text is truncated), html, image, video, date, boolean, button, color. Columns holding an icon or a status get their renderer automatically.
The group type packs several fields into one cell; first non-empty takes whichever of the listed fields has a value. Both exist for the case of many columns and little room.
Output in a template
The value of a table field is an array of rows:
<div class="slides">
{foreach $slides as $slide}
<div class="slide">
<h3>{$slide.title}</h3>
<img loading="lazy"
src="{$slide.image.url}"
width="{$slide.image.width}"
height="{$slide.image.height}"
alt="{$slide.image.title}">
</div>
{/foreach}
</div>Check an optional field before printing it:
{if $description}
<p>{$description}</p>
{/if}Not every row reaches the template
Only published ones: the publish date is set and not in the future, and the row is not deleted. The order is the one set by dragging. A row that is "missing from the site" is usually just unpublished.
Model: an ordinary table, or a resource
Model in the settings picks the storage.
PbTableData— the above: the row lives in shared storage and exists inside its block.PbResource— the row goes topb_resourcesand gains resource columns of its own:pagetitle,alias,uri,template,content,menuindex,hidemenuand the rest. Such a row can have its own address and its own template.
The second is for when a row is really a page: a product card, a catalogue article, a directory entry. Ordinary MODX resources are untouched — pb_resources is its own table and does not replace site_content.
A list like that can be filtered and sorted as a full catalogue:

