---
url: /list.md
---
# Listings

## List-container

Renders a product listing using the *list-item* template. If no products are found, the message defined in *list-empty* is displayed.

A listing wrapped in `i:dynamic` with `lazy` is not part of the first HTML response; give it a [lazy-load skeleton](/skeleton) so it does not shift the layout when it arrives.

The **optional** *data-role="pagination"* element is populated with content defined via *pagination* in the `_listing.tpl` (listing definitions) file (`_listing_article.tpl` for articles, etc.). Different templates can be specified using the *data-name* attribute.

**WARNING!** You must not wrap the pagination in a conditional display check. Such code would break when filtering via AJAX. If you need to hide pagination for single-page listings, add the condition inside `<i:list-pagination>` instead.

```twig
{# uses the "default" (or unnamed) template from _listing.tpl #}
<i:list-container type="product">
    <div data-role="pagination"></div>
    <div data-role="empty"></div>
    <div data-role="list"></div>
</i:list-container>
```

```twig
<i:list-container type="product">
    <div data-role="pagination"></div>
    <div data-role="empty"></div>
    <div data-role="list" data-name="short"></div>
    <div data-role="pagination" data-name="simple" ></div>
</i:list-container>
```

```twig
{# wrap boxes in groups of three into .wrap, insert <hr> between groups, and add class "third" #}
<i:list-container type="product" wrap="3:div.wrap" append="3:hr" box-class="3:third">
    <div data-role="empty"></div>
    <div data-role="list"></div>
</i:list-container>
```

```twig
{# wrap boxes in groups of three into .wrap and .www-rap, insert <hr> between groups, and add class "third" #}
<i:list-container type="product" wrap="3:div.wrap.www-rap" append="3:hr" box-class="3:third">
    <div data-role="empty"></div>
    <div data-role="list"></div>
</i:list-container>
```

```twig
{# uses the "news" template from _listing.tpl #}
<i:list-container type="product" name="news">
    <div data-role="empty"></div>
    <div data-role="list"></div>
</i:list-container>
```

**Attributes of list-container**

| Attribute | Required | Description |
|---|---|---|
| **type** | **yes** | listing type |
| source | *no* | Twig object with listing parameters (cannot be combined with *collection*) |
| collection | *no* | list of items to render (cannot be combined with *source* and does not support pagination) |
| name | *no* | item template name (as defined by *list-item*) |
| analytics-name | *no* | free-text placement name for analytics — says where on the page the strip sits, see "Placement names" below |
| wrap | *no* | wrap groups of boxes |
| append | *no* | append a tag after a group of boxes |
| box-class | *no* | add a CSS class |
| hide-empty | *no* | hide the entire container when it has no items (no value needed) |
| scroll-offset | *no* | CSS selector for accounting for a fixed header |
| insert-before | *no* | name of a listing-insert template to insert before each item |
| insert-after | *no* | name of a listing-insert template to insert after each item |
| insert-arguments | *no* | arguments to pass to the insert template - Twig written as {{ {abc: product.id} }} is processed via json\_encode, so only simple values (numbers, arrays, simple objects) are supported |
| class | --- | CSS class |

### Placement names (`analytics-name`)

`analytics-name` tells the product-lists report *where on the page* a strip
sits. The list source already says *what* it is — the beacon records that
itself as `list_id` (e.g. `recsys:similar`, or a curated source's own name) —
so `analytics-name` shouldn't repeat it: it names the **slot**, not what
feeds it. Reports are per-shop, so there is no cross-shop comparison to keep
a fixed vocabulary for — **`analytics-name` is free text**. The report simply
groups strips by whatever value a container carries, exactly as written; the
one thing that matters is that every listing with a *source* or *collection*
carries a name that says where it sits, so it isn't left blank in the report.

The recommended convention is `<page>.<slot>`, optionally with a third free
segment when two lists share a slot and need to stay distinguishable (e.g.
`detail.middle.parameters` next to `detail.middle`):

| page | meaning |
|---|---|
| `home` | homepage |
| `category` | category listing page |
| `detail` | product detail page |
| `cart` | cart page |
| `search` | search results page |
| `order` | order confirmation page |

| slot | where on the page |
|---|---|
| `top` | above the main content/listing |
| `middle` | inside the content, e.g. under the gallery or between listing pages |
| `bottom` | below the main content |
| `sidebar` | sidebar |
| `dialog` | inside a dialog, e.g. after-add-to-cart or the autocomplete panel |
| `empty` | shown instead of content: empty cart, no results |
| `main` | the page's own primary listing (the category listing itself, the search results list, the cart items) |

Examples following the convention: `detail.middle` (similar products under
the description), `detail.bottom`, `category.top`, `category.main` (the
category listing itself), `cart.dialog`, `cart.empty`, `search.dialog`
(autocomplete panel), `order.bottom`, `home.middle`. Any other value works
just as well — the report has no closed list to reject it from — but sticking
to the convention keeps a shop's own strips readable next to each other.

This applies to every `<i:list-container>` that has a *source* or
*collection* — not only recommendation strips (`recommend*` sources): the
report compares a curated strip (bestseller, a promoted category, recently
viewed, …) against a recsys strip in the same slot, so both need a name. A
container with neither *source* nor *collection* (the page's own default
listing, wired with no attribute at all) needs no `analytics-name`.

The template check reports a warning for a listing (with a *source* or
*collection*) that carries no name at all — an empty or missing
`analytics-name` is the only thing it flags; any non-empty value passes.
Example:

```twig
<i:dynamic variables="product" lazy="auto">
    <i:list-container type="product" source="{'recommendSimilar': product.id, 'limit': 12}"
                      analytics-name="detail.middle" name="listfeedrowmini">
        <div data-role="list"></div>
        <div data-role="empty"></div>
    </i:list-container>
</i:dynamic>
```

### hide-empty - hide listing with no items

When the *hide-empty* attribute is present (specified without a value), the container is not rendered at all if it contains no items. With this attribute, the empty listing definition is not used.

```twig
<i:list-container type="product" source="{kategorie:123,limit:10}" hide-empty>
    <div data-role="list"></div>
</i:list-container>
```

### source - listing parameters

If the *source* attribute is not specified, the default listing for the current page is used. For example, a category page will list products from that category, etc. A listing without a specified *source* updates the URL on changes (page, filter, ...).

Global variables can be used in the parameters, but no local variables are allowed. The expression must work identically even if the rest of the template is deleted.

```twig
<i:list-container type="product" source="{kategorie:123,limit:10}">
    <div data-role="empty"></div>
    <div data-role="list"></div>
</i:list-container>
```

```twig
<i:list-container type="product" source="{kategorie:kategorie.id,limit:10}">
    <div data-role="empty"></div>
    <div data-role="list"></div>
</i:list-container>
```

### Recommendations (recommend\*) must load lazily

A `source` with a recommendation parameter (`recommendSimilar`, `recommendCart`, `recommendHomepage`, `recommendCrossSell`, `recommendDefault`) calls the external recommendation service — 200–300 ms per request, and every requested item is processed and accounted for by the service. Such a listing must therefore never render as part of the main HTML response:

* wrap the container in `i:dynamic` with `lazy="auto"`, so it loads only when it approaches the viewport (crawlers and below-fold visitors never trigger it); reserve its space with the [lazy-load skeleton](/skeleton),
* set `limit` to the number of tiles the strip actually shows (at most 20),
* never probe a recommendation with a bare `repository.product.findBy(...)` emptiness check — that burns a full extra slate whose result nothing renders; render the container and let the `empty` slot (or `hide-empty`) handle the rare empty slate.

```twig
<i:dynamic variables="product" lazy="auto">
    <h3>Similar products</h3>
    <i:list-container type="product" source="{'recommendSimilar': product.id, 'limit': 12}">
        <div data-role="list"></div>
        <div data-role="empty"></div>
    </i:list-container>
</i:dynamic>
```

The template check (Kontrola šablony) reports violations of all three rules. On the tpltest host a synchronous recommendation is a render error with this instruction in the message; in production it only logs a warning, but the strip is still costing first-byte latency on every page view until fixed. Templates in `dialog/` and `ajax/` are exempt from the lazy rule — they already render after an interaction.

`lazy` belongs on `i:dynamic` and nowhere else. Written on the `i:list-container` itself, or next to the `i:dynamic="…"` *attribute* form, it does nothing at all: the listing still renders in the main document and the attribute is copied into the output as an invalid HTML attribute. The template check reports both spellings.

Deferring a strip changes when its markup exists, so **any JavaScript bound to it has to run again**. A slider or carousel initialised on page load never sees the lazily loaded strip and leaves it as a plain row of tiles. Re-initialise it from the [`reload` event](/events#dynamic-content-loaded-reload), scoped to `event.target`:

```javascript
$(document).on('reload', function (event) {
    $(event.target).find('.product-slider:not(.swiper-container-initialized)').each(function () {
        new Swiper(this, { /* … */ });
    });
});
```

### wrap - wrapping boxes into a single tag

The *wrap* attribute defines which tag with which class *(class is optional)* and under what conditions the boxes should be wrapped.

```twig
<i:list-container source="zdroj_zbozi" wrap="3:ul.sublist" class="list">
    <div data-role="empty"></div>
    <div data-role="list"></div>
</i:list-container>
```

```html
<div class="list">
    <ul class="sublist">
        <li>product</li>
        <li>product</li>
        <li>product</li>
    </ul>
    <ul class="sublist">
        <li>product</li>
        <li>product</li>
        <li>product</li>
    </ul>
    <ul class="sublist">
        <li>product</li>
    </ul>
</div>
```

### append - inserting a tag by position

The *append* attribute specifies a tag to insert after rendered boxes according to conditions. Multiple conditions can be specified, separated by commas -- the first matching tag is used.

```twig
<i:list-container append="3:hr.divider,last:hr.last" class="list">
    <div data-role="empty"></div>
    <div data-role="list"></div>
</i:list-container>
```

```html
<div class="list">
    <div class="item">product</div>
    <div class="item">product</div>
    <div class="item">product</div>
    <hr class="divider"/>
    <div class="item">product</div>
    <div class="item">product</div>
    <div class="item">product</div>
    <hr class="divider"/>
    <div class="item">product</div>
    <hr class="last"/>
</div>
```

## List pagination

Pagination definition -- always in the **\_listing.tpl** file. Defines a macro that is subsequently used for product listing in *list-container*.

Typically contains pagination, record count information, etc.

Global variables are not accessible; only the **pagination** and **filterActive** variables are available.

```twig
{# _listing.tpl #}
<i:list-pagination>
    {%if pagination.stran > 1%}
        {%trans%}celkem položek{%endtrans%}: <strong>{{pagination.nsum}}</strong>
        {%trans%}strana{%endtrans%}:  <strong>{{pagination.strany|raw}}</strong>
    {%endif%}
</i:list-pagination>
```

```twig
{# _listing.tpl #}
<i:list-pagination>
    {# appends the next page to the listing instead of changing the page #}
    <button data-role="add-page">{% trans 'listing.show-more-products' %}</button>
</i:list-pagination>
```

**Attributes of pagination**

| Attribute | Default | Description |
|---|---|---|
| **type** | --- | item type *(product, article, ...)* |
| name | *default* | box name for linking with *list-container* |

**Attributes of pagination.strany** (pages)

| Attribute | Values | Description |
|---|---|---|
| spaces | true/false | insert non-breaking spaces between pages |
| size | 1/2/3/4/5/... | number of displayed page links |
| label\_next | 'string' | text for the next page link |
| label\_previous | 'string' | text for the previous page link |
| hash | 'string' | appends a hash fragment after # in links |

```twig
{# _listing.tpl #}
<i:list-pagination>
    {{pagination.strany({spaces: true, label_next: 'next-page'|trans})}}
</i:list-pagination>
```

## List empty

Empty listing message definition -- always in the **\_listing.tpl** file. Defines a macro that is subsequently used for product listing in *list-container*.

Only the *url* variable is available.

```twig
<i:list-empty type="product" name="short">
    {% trans 'listing.no-products-found' %}
</i:list-empty>
```

```twig
<i:list-empty type="product">
    <div data-role="default">{% trans 'listing.no-products-found' %}</div>
    <div data-role="filter">
        {% trans 'listing.filter-no-results' %}
        <a href="{{url.reset}}">{% trans 'listing.reset-filter' %}</a>
        </div>
</i:list-empty>
```

**Attributes of list-empty**

| Attribute | Default | Description |
|---|---|---|
| **type** | --- | item type *(product, article, ...)* |
| name | *default* | name for linking with *list-container* |

**Available variables**

| Name | Value |
|---|---|
| url.reset | URL without filter and page |

## List insert

Template definition that is inserted before (or after) each rendered item -- always in the **\_listing.tpl** file. Defines a macro that is subsequently used for product listing in *list-container*.

```twig
<i:list-insert type="product" name="banner">
    {% if limit == 16 and pagePosition % 5 === 0 %}
        {% set article = repository.article.findOneBy({id:arguments.id}) %}
        {{ article.name }}
    {% endif %}
</i:list-insert>

<i:list-container insert-after="banner" insert-arguments="{{ {id:article.id} }}">
    <div data-role="empty"></div>
    <div data-role="list"></div>
</i:list-container>
```

**Attributes of list-insert**

| Attribute | Description |
|---|---|
| **type** | item type *(product, article, ...)* |
| name | name for linking with *list-container* |

**Available variables**

| Name | Value |
|---|---|
| pagePosition | Position on the page (counted from 1) |
| page | Listing page number (counted from 1) |
| limit | Number of records per page (maximum) |
| recordsOnPage | Number of records on the current page (actual) |
| arguments | input data defined on the listing-container -- scalar only (passed through json\_encode) |

## List Item

Listing item definition -- always in the **\_listing.tpl** file. Defines a macro that is subsequently used for product listing in *list-container*.

Global variables are not accessible and the position within the listing cannot be determined.

```twig
{# _listing.tpl #}
<i:list-item>
    <h3>{{product.name}}</h3>
</i:list-item>
```

```twig
<i:list-item tag="li" name="news">
    <h3>{{product.name}}</h3>
</i:list-item>
```

**Attributes of list-item**

| Attribute | Default | Description |
|---|---|---|
| **type** | --- | item type *(product, article, ...)* |
| tag | *div* | which tag to wrap the resulting box in |
| name | *default* | box name for linking with *list-container* |
| class | --- | CSS class |

## Complete Listing Definition Example

A production `_listing.tpl` defines multiple list-items (default product card, compact card for sliders, search result card), empty states with context-specific messages, and several pagination variants. The examples below are simplified from the buran production template (feature toggles stripped, core patterns preserved).

**Default product card** -- the main listing item used for category pages. Includes product image, labels (discount/new/custom), name, price with optional discount, availability with estimated delivery date, and add-to-cart form:

::: v-pre

```twig
{# _listing.tpl -- default product card for category listing #}
<i:list-item type="product" class="swiper-slide cell cell--product">
    <div class="white-item">
        <div class="white-item__image">
            <a href="{{ product.url }}">
                <i:img src="product.image" format="359x479" fill />
            </a>

            {# Labels: discount percentage, new arrival, custom labels with dynamic color #}
            <div class="labels">
                {% if product.item.price.discountPercent %}
                    <strong class="label label__discount">-{{ product.item.price.discountPercent }}%</strong>
                {% endif %}
                {% if product.flagNew %}
                    <strong class="label label__new">{% trans 'template.label.new' %}</strong>
                {% endif %}
                {% for label in product.labels %}
                    <strong class="label" if:style="label.colorCode ? 'background: ' ~ label.colorCode">
                        {{ label.name }}
                    </strong>
                {% endfor %}
            </div>
        </div>

        <h2><a href="{{ product.url }}">{{ product.name }}</a></h2>

        {# Price with optional strikethrough for discounted items #}
        {% if product.item.price.discount %}
            <span class="u-decoration-through">{{ product.item.price.beforeDiscount.formatted }}</span>
        {% endif %}
        <strong class="{{ product.item.price.discount ? 'u-color-red' }}">
            {{ product.item.price.current.formatted }}
        </strong>

        {# Availability with estimated delivery date #}
        <strong class="availability availability--{{ product.item.availability.hours }}">
            {{ product.item.availability.text }}
        </strong>
        {% if product.item.availability.hours < 999 and product.item.availability.deliveryOption.dateDelivered %}
            <span>
                {% trans 'template.at-your-place' %}
                <strong>{{ product.item.availability.deliveryOption.dateDelivered|date('j. n.') }}</strong>
            </span>
        {% endif %}

        {# Add to cart #}
        <form action="/Koupit" method="post">
            <button type="submit" class="button--order">{% trans 'template.add-to-cart' %}</button>
            <input name="zbozi" value="{{ product.id }}" type="hidden" />
        </form>
    </div>
</i:list-item>
```

:::

**Compact card for sliders** (`name="simple"`) -- a simplified variant used in carousels (related products, homepage banners). Wrapped in a swiper-slide, image + name + price only:

::: v-pre

```twig
{# _listing.tpl -- compact card for product carousels #}
<i:list-item type="product" name="simple" class="swiper-slide cell simple-box">
    <span class="white-item">
        <a href="{{ product.url }}">
            <div class="white-item__image">
                <i:img src="product.image" format="300x376" fill />
                {# Labels (simplified) #}
                <div class="labels">
                    {% if product.item.price.discountPercent %}
                        <strong class="label label__discount">-{{ product.item.price.discountPercent }}%</strong>
                    {% endif %}
                    {% for label in product.labels %}
                        <strong class="label" if:style="label.colorCode ? 'background: ' ~ label.colorCode">{{ label.name }}</strong>
                    {% endfor %}
                </div>
            </div>
            <h3>{{ product.name }}</h3>
            <strong>{{ product.item.price.current.formatted }}</strong>
        </a>
    </span>
</i:list-item>
```

:::

**Empty listing states** -- separate messages for filter results vs. default, plus specialized variants for search and wishlist:

::: v-pre

```twig
{# _listing.tpl -- default empty states with filter/default variants #}
<i:list-empty type="product">
    <p data-role="filter" class="callout">{% trans 'listing.empty.filter' %}</p>
    <p data-role="default" class="callout">{% trans 'listing.empty.default' %}</p>
</i:list-empty>

{# Separate empty state for search results #}
<i:list-empty type="product" name="search">
    <div data-role="filter" class="callout">{% trans 'listing.empty.filter' %}</div>
    <div data-role="default" class="callout">{% trans 'listing.empty.search' %}</div>
</i:list-empty>

{# Empty state for wishlist #}
<i:list-empty type="product" name="wish">
    <div data-role="default" class="callout">{% trans 'listing.empty.wish' %}</div>
</i:list-empty>
```

:::

**Pagination variants** -- buran defines multiple named paginations that are placed independently inside `i:list-container` via `data-name`:

::: v-pre

```twig
{# Sort dropdown -- auto-generated by the system, just place the container #}
<div data-role="pagination" data-name="sort"></div>

{# Page number links #}
<i:list-pagination type="product" name="pages">
    {% if pagination.pageCount > 1 %}
        {{ pagination.html({'spaces': false, 'size': 2})|raw }}
    {% endif %}
</i:list-pagination>

{# Result count summary (e.g. "Showing 1 - 24 of 156 products") #}
<i:list-pagination type="product" name="numbers">
    {% if pagination.totalResults > 0 %}
        {% trans 'listing.show' %} <strong>{{ pagination.resultsFrom }} - {{ pagination.resultsTo }}</strong>
        {% trans 'listing.show-from' %} <strong>{{ pagination.totalResults }}</strong>
        {% trans 'listing.products' %}
    {% endif %}
</i:list-pagination>

{# Total result count (simple) #}
<i:list-pagination type="product" name="qty">
    {{ pagination.totalResults }} {% trans 'listing.results' %}
</i:list-pagination>

{# "Load more" button (appends next page via AJAX instead of navigating) #}
<i:list-pagination type="product" name="add">
    <a href="#" class="button" data-role="add-page">{% trans 'listing.show-more-products' %}</a>
</i:list-pagination>

{# Scroll-to-top anchor #}
<i:list-pagination type="product" name="scroll">
    <a href="#main" class="button js_scrollToAnchor">{% trans 'template.scroll-top' %}</a>
</i:list-pagination>
```

:::

## Common Source Patterns

The `source` attribute on `i:list-container` accepts a Twig object with query parameters. The `collection` attribute accepts a pre-loaded list of items (e.g. from [content grid](content-grid.md)). When neither is specified, the listing uses the default source for the current page (e.g. a category page lists its products automatically).

::: v-pre

```twig
{# Related/relevant products on product detail (uses "simple" card variant) #}
<i:list-container type="product" source="{relevant: product}" name="simple" hide-empty>
    <div data-role="list"></div>
</i:list-container>

{# Products from a specific category by ID #}
<i:list-container type="product" source="{kategorie: 123, limit: 8}" hide-empty>
    <div data-role="list"></div>
</i:list-container>

{# Products from a dynamic category variable #}
<i:list-container type="product" source="{kategorie: kategorie.id, limit: 10}">
    <div data-role="empty"></div>
    <div data-role="list"></div>
</i:list-container>

{# Products from the current category (automatic -- no source needed) #}
{# This is the typical pattern used in vypis.tpl (category listing page) #}
<i:list-container type="product">
    <div data-role="pagination" data-name="sort"></div>
    <div data-role="empty"></div>
    <div data-role="list"></div>
    <div data-role="pagination" data-name="pages"></div>
</i:list-container>

{# Products from a content grid product_list field (e.g. homepage banners) #}
<i:list-container type="product" collection="{{ item.right.products }}" name="simple" hide-empty
                  class="swiper-container" data-navigation="simple-slider-{{ loop.index }}">
    <div data-role="list" class="swiper-wrapper"></div>
</i:list-container>
```

:::

For how listings integrate with [filters](filter.md) (including `i:filter-placeholder` and `i:filter-placeholder-values` inside the container), see the [filter integration example](filter.md#integration-with-listing).
