# Ready-made modules for your markup

**Status:** current · **Verified:** 2026-10-26, storefront runtime and level-1 sanitiser tests; every html snippet below is checked by `TestModulesSnippetsSurviveLevel1`
**Owner:** Vizen API documentation · **Serves:** `GET /docs/modules`
**Related:** `/docs/own-markup`, `/docs/vz-keys`, `/docs/webcoding`, `/docs/forms`, `/docs/guide-family-cards`

## 1. How to use this page

A module is platform behaviour that you attach to **your own** markup with a `vz-`
key: a lightbox, a product window, a cart button. You copy a snippet, restyle it
with your CSS and do not write the behaviour yourself — the platform runs it, keeps
it accessible and updates it for everyone.

- Snippets work in HTML documents and components at **level 1 and level 3**.
  Level 2 (a sandboxed frame) has no keys; where a module can be called from it,
  the section shows the message to post.
- Modules run on the **storefront**. The editor canvas shows the markup but does
  not open windows or lightboxes.
- Keys are listed in `/docs/vz-keys` (machine form `/docs/vz-keys.json`); markup
  rules (levels, sanitising, images) are in `/docs/own-markup`. Never invent a key:
  an unknown `vz-` attribute is removed.

| Module | Keys | Section |
|---|---|---|
| Lightbox | `vz-lightbox`, `vz-lightbox-item` | 2 |
| Product or group window | `vz-modal`, `href="modal:…"` | 3 |
| Product gallery with zoom | `vz-gallery` + `vz-lightbox` | 4 |
| Variant picker | `vz-variants` | 5 |
| Cart and favourites | `vz-add-to-cart`, `vz-cart-count`, `vz-cart-open`, `vz-fav`, `vz-fav-count` | 6 |
| Search with suggestions | `vz-search` | 7 |
| Shop menu | `vz-menu` | 8 |
| Your own toggle | `vz-toggle` | 9 |
| Lead form button | `href="form:<id>"` | 10 |
| Variant chips on tiles | `vz-panes`, `vz-pane`, `vz-pick` | 11 |
| Sign-in state and live events | `vz-auth`, `vz-user-initial`, `vz:cart` … | 12 |

## 2. Lightbox

**The shopper gets** the platform viewer: full-screen frame, thumbnail rail,
arrows and swipes, keyboard (Esc, ←/→), zoom up to 4× (pinch, wheel, double tap),
lazy loading of large frames, focus returned to the opener.

### Copy this: a photo grid

Every `<img>` inside `vz-lightbox` becomes a frame; a click opens the lightbox on it.

```html
<div class="photos" vz-lightbox>
  <img src="/files/sofa-1.jpg" sizes="(min-width: 768px) 320px, 50vw" alt="Sofa, front">
  <img src="/files/sofa-2.jpg" sizes="(min-width: 768px) 320px, 50vw" alt="Sofa, side">
  <img src="/files/sofa-3.jpg" sizes="(min-width: 768px) 320px, 50vw" alt="Sofa, fabric">
</div>
```

Set `sizes` to your tile width (`/docs/own-markup` §6): without it the page
loads a full-width step for every tile. The large frame is built from the
original file at 1024/1600/2560 — you do not link a big version. A picture inside
a link (`<a href="/catalog/sofa"><img></a>`) or inside a button with its own
action (`vz-add-to-cart`, `vz-fav`, `vz-modal` …) is not a frame: the click stays
theirs. Pictures in a hidden node (`hidden`, a closed `vz-pane`) do not take part.
For keyboard access wrap frames in links, as in the next recipe — a plain
`<img>` cannot receive focus.

### Copy this: explicit frames, a big file and captions

Mark frames with `vz-lightbox-item` when only some pictures should open, when the
large file differs from the thumbnail, or when the frame is a link that must work
without JavaScript. The caption comes from the nearest `<figcaption>`, otherwise
from `title`.

```html
<div vz-lightbox="thumbs=none&transition=fade">
  <figure>
    <a href="/files/room-big.jpg" vz-lightbox-item><img src="/files/room.jpg" alt="Living room"></a>
    <figcaption>Living room, oak finish</figcaption>
  </figure>
  <img src="/files/plan.jpg" vz-lightbox-item="/files/plan-big.jpg" title="Floor plan" alt="Plan">
  <img src="/files/logo.svg" alt="Brand">
</div>
```

Here the logo is not a frame: once explicit frames exist, plain pictures of the
set do not take part. A single `vz-lightbox-item` outside any set opens on its own.
In a loop, bind the big file: `vz-lightbox-item="{{ i.zoom_url }}"`.

### Copy this: video

```html
<a href="/files/tour.mp4" vz-lightbox-item><img src="/files/tour-poster.jpg" alt="Tour"></a>
```

A `.mp4`, `.webm` or `.mov` address opens as video. At level 3 a
`<video src poster vz-lightbox-item>` works too; level 1 removes `<video>`, so
there use a link like the one above.

### Settings

The value of `vz-lightbox`, written like a `modal:` query: `key=value&key=value`.

| Setting | Values | Default |
|---|---|---|
| `thumbs` | `rail` · `none` | `rail` (hidden by itself for one frame or for video) |
| `transition` | `slide` · `fade` | `slide` |
| `layout` | `fit` · `coverflow` | `fit` (`coverflow` shows dimmed neighbours) |
| `captions` | `1` · `0` | `1` |

All four at once: `vz-lightbox="thumbs=none&transition=fade&layout=coverflow&captions=0"`.
The counter, arrows, zoom and backdrop are the same everywhere and are not
configurable. An unknown setting is ignored; the browser console shows a
`vz-lightbox:` warning (also on the live shop), as it does for a relative address.

### From a script (level 3)

```js
document.querySelector('.open-gallery').addEventListener('click', () => {
  document.dispatchEvent(new CustomEvent('vz:lightbox-open', { detail: {
    start: 0,
    options: { thumbs: 'rail', transition: 'fade' },
    items: [
      { src: '/files/a.jpg', alt: 'Front', caption: 'Front' },
      { src: '/files/b.jpg', alt: 'Side' },
      { kind: 'video', src: '/files/tour.mp4', poster: '/files/tour.jpg' },
      { src: '/files/c.jpg', cta: { label: 'Request a call', href: 'form:12' } },
    ],
  } }))
})
```

Item fields: `src` (required), `kind` (`image` · `video`), `alt`, `caption`,
`poster` (video), `cta` (`label` + `href`; a click closes the lightbox first, so a
`form:` popup opens on top). Up to 100 items; invalid items are skipped. Call it
from a shopper action: a call during page load, before the storefront runtime has
started, is lost.

### From a sandbox (level 2)

```js
parent.postMessage({ type: 'vizen-lightbox-open', start: 0, items: [{ src: '/files/a.jpg' }] }, '*')
```

The same fields; the platform accepts the message only from your own frame.

### Rules and limits

- **A window opener wins.** A node with `vz-modal`, `vz-quickview` or
  `href="modal:…"` opens the window (section 3), not the lightbox.
- **Addresses.** Use absolute or root (`/…`) addresses in the key value and in
  the link `href`. A relative path there is not rewritten by the server (level 3
  project files): a picture then falls back to its own `src`, a link or video
  without a picture is not a frame.
- Files of an HTML project (`/_html/…`), svg and foreign hosts are shown as they
  are, without resizing.
- Inside the product quick view, the lightbox opens over the whole page.

## 3. Product or group window (quick view)

**The shopper gets** the live product page — or a ready design group — inside the
shop's own window, without leaving the page.

```html
<!-- keep the real URL: SEO, new tab, Cmd/Ctrl+click and no-JS still work -->
<a href="/catalog/chair" vz-modal="modal:product:42?size=full">Chair</a>

<!-- no real URL to keep: put the command in href -->
<a href="modal:product:42">Chair</a>

<!-- any element; foot=1 adds arrows, the counter and the page link -->
<button type="button" vz-modal="modal:product:42?foot=1&nav=ids:42,43">Quick view</button>
```

Everything the window needs rides in the one `modal:` string: size, header,
footer, padding, CSS hook, body group, neighbours, variant. The parameter table
and the window's behaviour (keyboard, an opener inside the window, CSS hooks of
the frame) live in `/docs/webcoding#14-3-modal-window-from-html-a-product-or-a-ready-design-group`.
`vz-modal-size`, `vz-modal-tpl` and similar are not keys — the sanitiser removes
them. A design group (`modal:group:<id>`) must also be declared in
`props.html.modalTargets` of the document.

From a sandbox (level 2):
`parent.postMessage({ type: 'vizen-content-modal-open', href: 'modal:product:42' }, '*')`.

## 4. Product gallery with zoom

On a product page: thumbnails switch the large frame; with `vz-lightbox` on the
same root, a click on the large frame opens the lightbox on the current picture.

```html
<div class="gallery" vz-gallery vz-lightbox>
  <img vz-gallery-main vz-src="product.preview_url" alt="{{ product.name }}">
  <div vz-gallery-thumbs>
    <img vz-for="i in product.gallery" vz-src="i.thumb_url" srcset="{{ i.thumb_srcset }}" sizes="64px" alt="">
  </div>
</div>
```

The lightbox takes its frames from the thumbnails, in order. The selected
thumbnail gets `vz-state="active"` — style it yourself. The gallery reads
`product.gallery` only: an empty gallery draws nothing. Match `sizes` to your
thumbnail width; image fields and their steps: `/docs/own-markup` §6. To put this
markup on the product page instead of the platform gallery, replace the page's
blocks as in `/docs/webcoding#16-page-layers-your-markup-instead-of-someone-else-s-not-on-top-of-it`
(the same safe write-and-rollback recipe as `/docs/components` §4.8).

## 5. Variant picker

On a product page the platform renders the size and colour choices into the node
and binds them to price, stock and cart. It brings a base style you override from
your own container.

```html
<div class="my-variants" vz-variants></div>
<button vz-add-to-cart>Add to cart</button>
```

## 6. Cart and favourites

```html
<button vz-add-to-cart>Add to cart</button>
<button vz-fav>♡ Save</button>
<a href="/cart" vz-cart-open>Cart <span vz-cart-count></span></a>
<a href="/wishlist">♥ <span vz-fav-count></span></a>
```

Without a value the keys act on the product of the page and honour the chosen
variant. In a feed tile pass the id: `vz-add-to-cart="{{ p.id }}"`,
`vz-fav="{{ p.id }}"` (in a group pane — `{{ m.id }}`). The buttons get
`vz-state="adding"`, `"in-cart"`, `"in-fav"` while and after acting. Counters are
empty at zero: `[vz-cart-count]:empty{display:none}`.

## 7. Search with suggestions

```html
<div class="search" vz-search>Search products</div>
```

The server inserts the input inside the container; its text becomes the
placeholder. While the shopper types, the same live suggestions as the platform
header drop down; Enter opens the results. Use a container, not an `<input>`:
level 1 removes author form controls.

## 8. Shop menu

```html
<nav class="menu" vz-menu>
  <a vz-for="m in menu" vz-href="m.url">{{ m.name }}</a>
</nav>
```

`vz-menu` brings the shop's section tree into the page data as `menu`; you print
it yourself. Nested loops go three levels deep (`c in m.children`). The current
section gets `aria-current` from the server — style `a[aria-current]`.

## 9. Your own toggle: dropdown, accordion, drawer

```html
<button vz-toggle="#specs">Specifications</button>
<div id="specs" class="panel">…</div>
<style>.panel{display:none}.panel[vz-state="open"]{display:block}</style>
```

A click toggles `vz-state="open"` on the target (a CSS selector in the same
document); without a value it toggles the button itself. How "open" looks is your CSS.

## 10. Lead form button

```html
<a href="form:12">Request a call</a>
```

Opens the shop's lead form 12 in the platform popup. Creating forms, prefill and
the level 2 bridge: `/docs/forms`. `vz-lead` is reserved and does nothing — use
the `form:` link.

## 11. Variant chips on product tiles

One tile per product family: a size or colour chip switches the whole tile
without leaving the page; the link still works without JavaScript. The insertion
needs the feed setting `"productsMode": "collapse"` — without it there are no
`p.members`.

```html
<article vz-for="p in products" vz-panes>
  <div vz-for="m in p.members" vz-pane="{{ m.id }}" vz-hidden="!m.selected">
    <a vz-href="m.url">{{ m.name }}</a>
    <div vz-for="ax in m.variants">
      <a vz-for="o in ax.options" vz-href="o.url" vz-pick="{{ o.member_id }}" aria-current="{{ o.current }}">{{ o.label }}</a>
    </div>
  </div>
</article>
```

The full tutorial — feed settings, combo prices, release and rollback:
`/docs/guide-family-cards`.

## 12. Sign-in state and live events

```html
<div class="account" vz-auth>
  <a class="login" href="/account">Sign in</a>
  <span class="avatar" vz-user-initial></span>
</div>
<style>.account[vz-state="out"] .avatar,.account[vz-state="in"] .login{display:none}</style>
```

Your scripts listen instead of polling: `vz:navigate`, `vz:cart`, `vz:fav`,
`vz:auth` on `document`. The table of live keys and the event contract:
`/docs/own-markup#4-0-live-data-without-your-own-script`.
