Components
INFO
Automatically generated from PHP attributes by the shop:template:documentation command. CSS must not rely on the generated HTML output of components - it may change in future versions.
i:gdpr-form
GDPR consent checkbox/text inserted into parent forms
| Attribute | Type | Required | Default | Description |
|---|---|---|---|---|
| codes | string | no | GDPR form codes to display (comma-separated) | |
| context | cart-sign-up | footer | sidebar | no | Override auto-detected context | |
| list | boolean | no | Render as <li> elements without wrapper div |
INFO
Self-closing tag, must have no children. Context is auto-detected from the parent form component (newsletter-signup, user-signup, contact form). Use the codes attribute only when you need specific GDPR forms regardless of context.
Auto-detected context (inside newsletter-signup, user-signup, or contact form)
<i:gdpr-form />Explicit GDPR form codes
<i:gdpr-form codes="newsletter_consent,marketing" />Generated output:
<div>gdprForm({"codes":["newsletter_consent","marketing"]})</div>i:newsletter-signup
Newsletter subscription form
| Attribute | Type | Required | Default | Description |
|---|---|---|---|---|
| redirect | string | no | Redirect URL after successful signup | |
| autohide | boolean | no | Hide form for already subscribed visitors | |
| ignore-duplicated | boolean | no | Silently ignore duplicate subscriptions | |
| confirm-template | string | no | Email template ID for confirmation (numeric, requires discount) | |
| discount | string | no | Discount set ID(s) for coupon code (comma-separated, requires confirm-template) |
| Role | Tag | Required | Description |
|---|---|---|---|
| input | yes | Email address input | |
| first_name | input | no | First name |
| last_name | input | no | Last name |
| street | input | no | Street address |
| city | input | no | City |
| zip | input | no | Postal code |
| country | input | no | Country code (cz, sk, ...) |
| source | input | select | no |
| gender | input | no | Gender radio buttons (value: m or f) |
| submit | button | yes | Submit button |
INFO
When using discount attribute, the confirm-template attribute is also required. The email template can use {{discountCode}} for a single code or discountCodes array keyed by set ID for multiple codes.
Minimal newsletter form with GDPR consent
<i:newsletter-signup>
<input data-role="email" data-label="Your email">
<i:gdpr-form />
<button data-role="submit">Subscribe</button>
</i:newsletter-signup>Generated output:
<form method="POST" action="/w/~Prijemce_Pridat/save/?uri=%2F" data-component="newsletter-signup">
<label for="email_uid">trans:Your email</label><input id="email_uid" name="email" type="email" required="required">
<div>gdprForm({"context":"newsletter-signup"})</div>
<div class="global-newsletter-url"><label for="newsletter_uid">trans:URL</label><input type="url" id="newsletter_uid" name="url"></div>
<button type="submit">trans:Subscribe</button>
</form>Newsletter form with extra fields and redirect
<i:newsletter-signup redirect="/thank-you">
<input data-role="email" data-label="Your email">
<input data-role="first_name" data-label="First name">
<i:gdpr-form />
<button data-role="submit">Subscribe</button>
</i:newsletter-signup>Generated output:
<form method="POST" action="/w/~Prijemce_Pridat/save/?uri=%2F" data-component="newsletter-signup">
<label for="email_uid">trans:Your email</label><input id="email_uid" name="email" type="email" required="required">
<label for="first_name_uid">trans:First name</label><input id="first_name_uid" name="first_name" type="text">
<div>gdprForm({"context":"newsletter-signup"})</div>
<div class="global-newsletter-url"><label for="newsletter_uid">trans:URL</label><input type="url" id="newsletter_uid" name="url"></div>
<button type="submit">trans:Subscribe</button>
<input type="hidden" name="redirect" value="/thank-you"></form>i:search-form
Product search form with autocomplete support
| Attribute | Type | Required | Default | Description |
|---|---|---|---|---|
| autocomplete | boolean | no | Enable search autocomplete | |
| min-length | string | no | 3 | Minimum characters before autocomplete triggers |
| template | string | no | Template name for search results (alphanumeric/underscore) | |
| img-format | string | no | Image format for autocomplete results (e.g. "60x60") |
| Role | Tag | Required | Description |
|---|---|---|---|
| search | input | yes | Search text input |
| submit | button | yes | Submit button |
INFO
Autocomplete results template is in ajax/search.tpl (or custom file via template attribute). The template receives variable q with the search query. Each result link must have tabindex and data-result-type (product, category, brand, more) attributes. Style the search-current-item CSS class for keyboard navigation highlight.
INFO
Fires autocomplete:loaded event on the form element after each autocomplete results load.
Search form with autocomplete
<i:search-form autocomplete>
<input data-role="search" data-label="Search">
<button data-role="submit">Search</button>
</i:search-form>Generated output:
<form data-min-length="3" action="Vypis/Vyhledavani" method="GET" data-component="product-search" data-autocomplete>
<label for="search_uid">trans:Search</label><input id="search_uid" type="search" name="fraze" required="required" minlength="2" autocomplete="off">
<button type="submit">trans:Search</button>
</form>i:comment-form
Comment/review form for products, articles, and categories with proof-of-work spam protection
| Attribute | Type | Required | Default | Description |
|---|---|---|---|---|
| object | expression | yes | Variable with the commentable object (product, article, or category) |
| Role | Tag | Required | Description |
|---|---|---|---|
| text | textarea | no | Comment text |
| title | input | no | Comment headline |
| rating | select | no | Star rating 1-5 (products only, options auto-generated) |
| author | input | no | Author name |
| input | no | Author email | |
| phone | input | no | Author phone |
| parent | input | no | Parent comment ID for threaded replies (auto-set as hidden) |
| submit | button | yes | Submit button |
INFO
Spam protection uses proof-of-work (no CAPTCHA needed). The form submits via AJAX automatically. Rating select options (stars) are auto-populated. For threaded replies, set parent slot value to the parent comment ID.
Basic product comment
<i:comment-form object="product">
<input data-role="author" data-label="Name">
<textarea data-role="text" data-label="Comment"></textarea>
<button data-role="submit">Submit</button>
</i:comment-form>Generated output:
<form method="POST" action="/api/comments" data-component="comment-form">
<label for="author_uid">trans:Name</label><input id="author_uid" name="author" type="text" autocomplete="name">
<label for="text_uid">trans:Comment</label><textarea id="text_uid" name="text"></textarea>
<button type="submit">trans:Submit</button>
<input name="commentType" type="hidden" value="zbozi">
<input name="objectId" type="hidden" value="123">
</form>Product review with all fields
<i:comment-form object="product">
<input data-role="author" data-label="Name">
<input data-role="email" data-label="Email">
<input data-role="title" data-label="Headline">
<textarea data-role="text" data-label="Review"></textarea>
<button data-role="submit">Submit review</button>
</i:comment-form>Generated output:
<form method="POST" action="/api/comments" data-component="comment-form">
<label for="author_uid">trans:Name</label><input id="author_uid" name="author" type="text" autocomplete="name">
<label for="email_uid">trans:Email</label><input id="email_uid" name="email" type="email" autocomplete="email">
<label for="title_uid">trans:Headline</label><input id="title_uid" name="title" type="text">
<label for="text_uid">trans:Review</label><textarea id="text_uid" name="text"></textarea>
<button type="submit">trans:Submit review</button>
<input name="commentType" type="hidden" value="zbozi">
<input name="objectId" type="hidden" value="456">
</form>i:list-empty
Defines the empty-state template macro shown when listing has no results
| Attribute | Type | Required | Default | Description |
|---|---|---|---|---|
| type | product | article | actuality | yes | Listing type | |
| name | string | no | default | Empty template macro name |
Empty state with separate messages for filtered and default states
<i:list-item type="product">{{product.name}}</i:list-item><i:list-empty type="product"><p data-role="default">No products found.</p><p data-role="filter">No products match your filter.</p></i:list-empty><i:list-container type="product"><div data-role="empty"></div><div data-role="list"></div></i:list-container>Generated output:
<div data-type="product" data-list-config="eyJjb250ZW50R3JvdXBJZCI6bnVsbCwiY3VycmVudFBhZ2UiOm51bGwsInBlclBhZ2UiOm51bGwsInNvcnQiOm51bGx9" data-component="list-container"><div data-role="empty"><p>trans:No products found.</p></div><div data-role="list"></div></div><script>{inline-listing-loaded};</script>i:list-item
Defines the template macro for a single item in a listing
| Attribute | Type | Required | Default | Description |
|---|---|---|---|---|
| type | product | article | actuality | yes | Type of item | |
| tag | string | no | div | HTML tag to wrap each item |
| name | string | no | default | Macro name (must match list-container name) |
Basic product item template
<i:list-item type="product"><h3>{{product.name}}</h3></i:list-item>{{_self.item_product_default(product)}}Generated output:
<div data-track="123|0||" data-oid="p123"><h3>name</h3></div>i:list-container
Main listing container that fetches items from a repository and renders them with pagination
| Attribute | Type | Required | Default | Description |
|---|---|---|---|---|
| type | product | article | actuality | yes | Type of items to display | |
| source | expression | no | Custom repository query parameters as JSON object (e.g. {category: kategorie.id, limit: 8}) | |
| collection | expression | no | Custom Twig collection to iterate over instead of repository | |
| name | string | no | default | Template macro name for the item box |
| wrap | string | no | Wrap items in groups (e.g. "3:div.row" wraps every 3 items in a div.row) | |
| append | string | no | Insert elements at positions (e.g. "3:div.spacer") | |
| insert-before | string | no | Insert macro name to call before each item | |
| insert-after | string | no | Insert macro name to call after each item | |
| hide-empty | boolean | no | Hide the entire container when no items found (instead of showing empty slot) | |
| scroll-offset | string | no | Offset for scroll-to behavior | |
| analytics-name | string | no | Analytics identifier for tracking |
| Role | Tag | Required | Description |
|---|---|---|---|
| list | div | yes | Container for rendered items (must be empty) |
| empty | no | Shown when no items found (required unless hide-empty) | |
| pagination | div | no | Pagination controls (can have multiple) |
Default product listing
<i:list-item type="product">{{product.name}}</i:list-item><i:list-empty type="product">No products found</i:list-empty><i:list-container type="product"><div data-role="list"></div><div data-role="empty"></div></i:list-container>Generated output:
<div data-type="product" data-list-config="eyJjb250ZW50R3JvdXBJZCI6bnVsbCwiY3VycmVudFBhZ2UiOm51bGwsInBlclBhZ2UiOm51bGwsInNvcnQiOm51bGx9" data-component="list-container"><div data-role="list"><div data-track="123|0|1|" data-oid="p123">abc</div></div><div data-role="empty"></div></div><script>{inline-listing-loaded};</script>Custom product listing with category filter
<i:list-container type="product" source="{category: kategorie.id, limit: 4}">
<div data-role="list"></div>
<div data-role="empty">No products</div>
</i:list-container>i:list-pagination
Defines the pagination template macro for listings
| Attribute | Type | Required | Default | Description |
|---|---|---|---|---|
| type | product | article | actuality | yes | Listing type | |
| name | string | no | default | Pagination macro name |
| Role | Tag | Required | Description |
|---|---|---|---|
| add-page | a | no | Load-more button, shown when next page exists |
Simple load-more pagination
<i:list-pagination type="product">
<a data-role="add-page">Load more</a>
</i:list-pagination>i:list-dynamic
Dynamic content area within a listing that renders content based on type (headline, text, subcategories)
| Attribute | Type | Required | Default | Description |
|---|---|---|---|---|
| type | headline | text | subcategories | other | yes | Type of dynamic content | |
| variables | string | yes | Twig variables to pass to the dynamic content |
Category headline in listing
<div i:list-dynamic type="headline" variables="kategorie">{{ kategorie.name }}</div>i:banners
Banner/slider component with responsive images, CTA buttons, and carousel support
| Attribute | Type | Required | Default | Description |
|---|---|---|---|---|
| source | expression | yes | Twig expression returning banner collection | |
| class | string | no | CSS class for the banner wrapper | |
| plugin | string | no | Slider plugin name (e.g. "swiper") | |
| click | cta | banner | off | auto | no | auto | Click behavior - CTA button, whole banner, disabled, or auto |
| visible-slides | string | no | 1 | Number of visible slides |
| priority | string | no | high | Loading priority (high = no lazy load for visible slides) |
| wrap | string | no | Wrapper element (e.g. "div.banner-inner") | |
| arrows | boolean | no | Show navigation arrows | |
| dots | boolean | no | Show pagination dots | |
| thumbnails | boolean | no | Show thumbnail navigation | |
| progress | boolean | no | Show progress bar | |
| transparent | boolean | no | Preserve image transparency | |
| fill | boolean | no | Use object-fit: cover on images |
Responsive hero banner with Swiper plugin
<i:banners source="banners" class="hero-banner" plugin="swiper" arrows dots><source format="1200x400" src="desktop"/><source format="400x300" src="mobile"/></i:banners>Generated output:
<div class="banner hero-banner swiper-container">
<div class="banner-wrap swiper-wrapper">
<div class="banner-item banner--item__1 banner--item__first banner--item__last swiper-slide">
<picture>
<source srcset="https://i00.eu/img/999999/400x300/b70ltmq7/17.webp 1x,https://i00.eu/img/999999/800x600c/14pro84s/17.webp 2x" media="(max-width: 400px)">
<img width="730" height="400" srcset="https://i00.eu/img/999999/1200x400/eb9g3pmd/17.webp 1x,https://i00.eu/img/999999/2400x800c/aergzgi6/17.webp 2x" src="https://i00.eu/img/999999/1200x400/eb9g3pmd/17.webp">
</picture>
</div>
</div>
<div class="pagination swiper-pagination"></div>
<div class="button-next swiper-button-next"></div>
<div class="button-prev swiper-button-prev"></div>
</div>i:menu
Defines a menu macro in _menu.tpl, rendered by i:menu-container
| Attribute | Type | Required | Default | Description |
|---|---|---|---|---|
| name | string | yes | Menu macro name (must match i:menu-container name) |
INFO
Must be defined in _menu.tpl file. The lazy variable is automatically passed by i:menu-container - use it to skip rendering deep submenus that will be loaded via AJAX. Menu output is cached, so use active_menu_placeholder_class('className', category) for active state instead of Twig conditionals. Also accepts a route hash like active_menu_placeholder_class('active', 'Vypis/Slevy') for page type matching.
Two-level category menu with active class and lazy support
<i:menu name="main">
<ul>
{% for category in repository.category.findBy({'parent': 0}) %}
<li class="{{ active_menu_placeholder_class('active', category) }}">
<a href="{{ category.url }}">{{ category.name }}</a>
{% if not lazy and category.subcategories is not empty %}
<ul>
{% for sub in category.subcategories %}
<li class="{{ active_menu_placeholder_class('active', sub) }}">
<a href="{{ sub.url }}">{{ sub.name }}</a>
</li>
{% endfor %}
</ul>
{% endif %}
</li>
{% endfor %}
</ul>
</i:menu>i:menu-container
Category menu container that loads menu from _menu.tpl template with optional lazy loading
| Attribute | Type | Required | Default | Description |
|---|---|---|---|---|
| name | string | yes | Menu macro name defined in _menu.tpl | |
| tag | string | no | div | HTML wrapper tag |
| lazy | boolean | no | Load menu asynchronously via AJAX |
INFO
Must be an empty element (no children). With lazy, the non-lazy part renders server-side and the full menu loads via AJAX, significantly improving page speed. Fires menu:load event on the container element after async load completes.
Lazy-loaded main navigation menu
<div><i:menu-container name="main" tag="div" lazy /></div>Generated output:
<div><div id="js-menu-uid">trans:hello</div><script>(function(){var x=new XMLHttpRequest();x.open("GET","/ajax-menu-url");x.setRequestHeader("X-Requested-With","XMLHttpRequest");x.send();x.onreadystatechange=function(){if(4===x.readyState){var t=x.responseText;var l=JSON.parse(document.body.dataset["activeCategories"]),r=document.body.dataset["routeHash"];for(var i=0;i<l.length;i++)t=t.replace(new RegExp("([a-zA-Z0-9\-_]+)\-menu\-class\-placeholder\-"+l[i]+"([^0-9])","g"),"$1$2");t=t.replace(new RegExp("([a-zA-Z0-9\-_]+)\-menu\-class\-placeholder\-"+r+"([^0-9a-z])","g"),"$1$2");document.getElementById("js-menu-uid").innerHTML=t.replace(new RegExp("[a-zA-Z0-9\-_]+\-menu\-class\-placeholder\-[0-9]+","g"),"");const event = document.createEvent("CustomEvent");event.initCustomEvent("menu:load", true, true, {});document.getElementById("js-menu-uid").dispatchEvent(event);}};})()</script></div>Sidebar menu rendered as list
<div i:menu-container name="sidebar" tag="ul"></div>i:delivery-transports
Container for transport/shipping options in the checkout process
| Attribute | Type | Required | Default | Description |
|---|---|---|---|---|
| tag | string | no | div | HTML wrapper tag |
| reload-targets | string | no | CSS selectors of elements to reload when transport changes |
Transport selection in checkout
<i:delivery-transports>{% for transport in cart.availableTransportMethods %}<i:delivery-transport transport="transport" tag="div"><input name="transport" type="radio" />{{ transport.name }}<i:delivery-branch button-text="Choose branch" /></i:delivery-transport>{% endfor %}</i:delivery-transports>Generated output:
<div data-component="delivery-transports" data-dynamic-content="7eNRw6uOGJUoTV9lG4yoJ7tzbjZVbB5agwTz0Ph+6ks="><div data-component="delivery-transport"><input name="transport" type="radio" value="7">PPL<div data-component="delivery-branch"><input type="hidden" name="pobocka[zasilkovna_stat_1000]" value="5" data-config='{"type":"ppl"}' data-branch='{"code":5,"label":"Drtinova 10"}' data-excluded-places="[]"><span>Drtinova 10</span> <a href="#">trans:Choose branch</a></div></div></div>i:delivery-transport
Single transport option within delivery-transports
| Attribute | Type | Required | Default | Description |
|---|---|---|---|---|
| transport | expression | yes | Twig expression for the transport object from cart.transports loop | |
| tag | string | yes | HTML wrapper tag |
Transport option with branch selection
<i:delivery-transport transport="transport" tag="label"><input name="transport" type="radio" />{{ transport.name }}<i:delivery-branch button-text="Choose pickup point" /></i:delivery-transport>Generated output:
<label data-component="delivery-transport"><input name="transport" type="radio" value="3">PPL<div data-component="delivery-branch"><input type="hidden" name="pobocka[ppl_cz]" data-config='{"type":"ppl"}' data-excluded-places="[]"><span></span> <a href="#">trans:Choose pickup point</a></div></label>i:delivery-payments
Container for payment method options in the checkout process
| Attribute | Type | Required | Default | Description |
|---|---|---|---|---|
| tag | string | no | div | HTML wrapper tag |
| reload-targets | string | no | CSS selectors of elements to reload when payment changes |
Payment method selection in checkout
<i:delivery-payments>{% for payment in cart.availablePaymentMethods %}<i:delivery-payment payment="payment" tag="label"><input name="payment" type="radio" />{{ payment.name }}</i:delivery-payment>{% endfor %}</i:delivery-payments>Generated output:
<div data-component="delivery-payments" data-dynamic-content="7eNRw6uOGJUoTV9lG4yoJ7tzbjZVbB5agwTz0Ph+6ks="><label data-component="delivery-payment"><input name="payment" type="radio" value="7">Credit card</label></div>i:delivery-payment
Single payment method option within delivery-payments
| Attribute | Type | Required | Default | Description |
|---|---|---|---|---|
| payment | expression | yes | Twig expression for the payment object from cart.payments loop | |
| tag | string | yes | HTML wrapper tag |
Payment option with radio button
<i:delivery-payment payment="payment" tag="label"><input name="payment" type="radio" />{{ payment.name }}</i:delivery-payment>Generated output:
<label data-component="delivery-payment"><input name="payment" type="radio" value="5">Credit card</label>i:delivery-branch
Pickup point/branch selector within a delivery-transport (renders branch select widget)
| Attribute | Type | Required | Default | Description |
|---|---|---|---|---|
| transport | expression | yes | Transport object reference (auto-set by parent delivery-transport) | |
| button-text | string | yes | Text for the branch selection button (translatable) |
Branch selector (must be inside i:delivery-transport)
<i:delivery-branch button-text="Choose pickup point" />i:filter-form
Defines a filter form template macro for product/article filtering
| Attribute | Type | Required | Default | Description |
|---|---|---|---|---|
| name | string | yes | Filter form macro name (referenced by filter-placeholder) | |
| auto-scroll | string | no | Scroll to listing after filter change |
| Role | Tag | Required | Description |
|---|---|---|---|
| reset | a | no | Filter reset link/button (hidden when no filter is active) |
Basic filter form with reset button
<i:filter-form name="default">
<a data-role="reset">Reset filters</a>
{% for attr in attributes %}
<i:filter-attribute attribute="attr" />
{% endfor %}
</i:filter-form>i:filter-placeholder
Renders a filter-form macro at a specific location with given configuration
| Attribute | Type | Required | Default | Description |
|---|---|---|---|---|
| name | string | yes | Filter form macro name (must match a filter-form name) | |
| config | string | yes | Filter configuration code | |
| hide-empty | boolean | no | Hide the filter form when no filter attributes are available | |
| always-collapsed | boolean | no | Render all filter groups initially collapsed |
Render default filter form, hidden when empty
<i:filter-placeholder name="default" config="default" hide-empty />i:variant-choice
Product variant selector with step-by-step attribute selection
| Attribute | Type | Required | Default | Description |
|---|---|---|---|---|
| data-product | expression | yes | Product ID expression | |
| reload-targets | string | no | CSS selectors of elements to reload when variant changes |
| Role | Tag | Required | Description |
|---|---|---|---|
| step | yes | Container for each variant selection step |
Variant selector with color/size steps
<div i:variant-choice data-product="{{product.id}}" i:dynamic="product" reload-targets=".product-price,.product-availability">
{% for step in product.variantChoice.steps %}
<div data-role="step">
<strong>{{ step.name }}:</strong>
{% for value in step.values %}
<button>{{ value.name }}</button>
{% endfor %}
</div>
{% endfor %}
</div>i:dynamic
Wraps content for dynamic (AJAX) reloading when server-side state changes
| Attribute | Type | Required | Default | Description |
|---|---|---|---|---|
| variables | string | yes | Comma-separated Twig variables that trigger re-render | |
| lazy | none | auto | manual | no | none | Lazy loading mode - none (server-rendered), auto (lazy on page load), manual |
Cart summary that updates dynamically
<i:dynamic variables="order">Order #{{ order.id }}</i:dynamic>Generated output:
<div data-dynamic-content="+U8r+mkvbR7EJVHWtAmbvIN9KvgnUinu7XHN9oUfE9Nb6OKzPQzr8vs6btDlfyPu1PqQ3yj1hmwFpfxEwO5Lxg==">Order #7</div>i:img
Responsive image with automatic srcset, DPR-aware rendering, and lazy loading
| Attribute | Type | Required | Default | Description |
|---|---|---|---|---|
| src | expression | yes | Twig expression returning an Image object (e.g. product.image) | |
| format | string | yes | Dimensions WxH or comma-separated list (e.g. "359x479" or "800x600,400x300") | |
| sizes | string | no | HTML sizes attribute (required when multiple formats) | |
| fill | boolean | no | Use object-fit: cover | |
| vector | boolean | no | SVG source - skip bitmap optimization | |
| transparent | boolean | no | Preserve transparency (PNG/WebP instead of JPEG) | |
| watermark | boolean | no | Apply watermark overlay | |
| quality | low | normal | high | lossless | no | Compression quality | |
| loading | string | no | Loading strategy (lazy/eager) | |
| lazyload | boolean | no | Enable lazy loading |
Basic product image
<i:img src="product.img" format="100x100" fill />Generated output:
<img alt="description" width="100" height="100" srcset="http://100x100f.jpg 1x,http://200x200fc.jpg 2x" src="http://100x100f.jpg">Lazy-loaded lifestyle image variant
<i:img src="product.image('lifestyle')" format="800x600" fill loading="lazy" />SVG brand logo
<i:img src="brand.image" format="200x100" vector transparent fill />