Skip to content

Listing layout

Every listing in Mateu renders in one of four layouts. The framework picks one automatically based on the columns you define and the available screen width. You can also force a specific layout when the automatic choice is not right for your use case.

Listing with search bar, filters and paginated table


LayoutWhen it appearsBest for
tableAll columns fit comfortablyDense data with many comparable fields
listColumns fit in two lines (title + secondary info)Navigation-heavy UIs on medium screens
cardsImage/rich content present, or many columns without a clear primary oneProduct catalogues, media galleries
masterDetailToo many or too wide columns to summariseComplex entities that need side-by-side browsing

A fifth layout, tree, is never auto-selected: force it with GridLayout.tree when your rows are hierarchical (each row exposes a self-referential children list) and you want an expandable tree grid — see Tree layout below.


The framework assigns a weight to every column (roughly, how many pixels wide it needs to be), then compares the total column weight to the available container width. 1 unit ≈ 76 px.

Column type / stereotypeWeight (units)
bool, icon1.0
status, integer1.5
combobox, select, number, date, money2.0
link, dateTime, dateRange2.5
plain String3.0
image4.0
html, richText, markdown, textarea5.0

The weight is estimated server-side from the row field’s real Java type (a long counter weighs 1.5, a LocalDate 2.0, …) and travels with each column on the wire, so the selection is accurate even though the coarse column dataType collapses most types to string for rendering purposes. @Weight / @ColumnWidth still override it.

density ratio = totalColumnWeight / (containerWidthPx / 76)
ratio ≤ 1.1 → table
ratio > 1.6 OR columns > 10 → masterDetail
otherwise:
compact columns exist AND their weight ≤ 8 → list
has image or html stereotype → cards
no compact columns AND 4–8 columns → cards
fallback → masterDetail

The table branch tolerates up to ~10% of estimated overweight: the estimate is conservative (auto-width columns take what their content needs, usually less), so a slightly tight table beats flipping a scannable listing into cards.

Compact columns are columns marked as the identifier (@Priority(identifier = true)) or with a priority value ≤ 2 (@Priority(1), @Priority(2)). They are the fields shown in the condensed views (list row title, master-detail panel header).


Influencing auto-selection via annotations

Section titled “Influencing auto-selection via annotations”
public record ProductRow(
@Priority(value = 1, identifier = true)
String name,
String category,
BigDecimal price,
ProductStatus status
) {}

identifier = true pins this column as the row title in list and master-detail layouts. Columns with priority ≤ 2 are included in the compact summary line.

public record ProductRow(
@Weight(5.0) // treat as very wide — pushes density ratio up
String description,
@ColumnWidth("60px") // narrow fixed column — weight derived from px
boolean active
) {}

@Weight takes precedence over the type-based default. @ColumnWidth also feeds into the weight calculation when @Weight is absent.


return Listing.builder()
.gridLayout(GridLayout.cards)
// ...
.build();

Available values: auto (default), table, list, cards, masterDetail, tree.

return Listing.builder()
.listingType(ListingType.card)
// ...
.build();

listingType(ListingType.card) is equivalent to gridLayout(GridLayout.cards). It exists as a semantic alias for CRUDs whose primary presentation is always cards regardless of screen width.


GridLayout.tree renders the listing as an expandable tree grid instead of a flat table. It is the only layout that is never auto-selected — you opt in explicitly, because it changes the data contract: the rows must be hierarchical.

Two requirements:

  1. The row type exposes a self-referential children collection. A row with a non-empty children list gets an expand/collapse toggle; leaves leave it empty or null.
  2. gridLayout() returns GridLayout.tree.
public record CategoryRow(
@Priority(value = 1, identifier = true) // becomes the expandable tree column
String name,
int products,
List<CategoryRow> children // sub-categories → the tree branches
) {}
@UI("/catalog")
@Title("Catalog")
public class CatalogTree implements Listing<CategoryRow>, Searchable {
@Override
public GridLayout gridLayout() {
return GridLayout.tree;
}
@Override
public ListingData<CategoryRow> search(SearchRequest request, HttpRequest httpRequest) {
return ListingData.of(rootCategories); // roots only — each carries its children
}
}

search() returns only the root rows; the framework expands each branch lazily from the children list as the user opens it. The identifier column becomes the tree column, with the expand toggle.

To let users edit a node in place, make the CRUD @SplitCrud so selecting a row opens its editor in a detail pane. That full pattern — including heterogeneous trees whose nodes edit with different forms, and grouping nodes that are not openable (viewable = false) — is documented in Tree CRUD.


Fixing the content height (cards and table)

Section titled “Fixing the content height (cards and table)”

By default the listing expands to fill the container, which scrolls the whole page. Set contentHeight to constrain the card or table area to a fixed height with its own internal scrollbar:

return Listing.builder()
.gridLayout(GridLayout.cards)
.contentHeight("400px")
.build();

SituationExpected layout
3 short columns (name, status, date) on desktoptable
6 columns total, 1 marked identifier, compact weight ≤ 8list
Any column with image or html stereotypecards
More than 10 columnsmasterDetail
Density ratio > 1.6 after adding many wide columnsmasterDetail
gridLayout(GridLayout.cards) set explicitlyalways cards
Rows carry a children list and gridLayout(GridLayout.tree) settree (expandable)