---
name: vizen-chrome
description: Build a Vizen shop's header, menu and footer — configure the platform chrome widgets or replace them with your own markup, assemble a second chrome group, bind it to one section or to a LAYOUT (theme) without touching the rest of the shop, dress the system pages (cart, checkout, account), make a custom header stick, and replace the bundled product card.
---

# Header, menu, footer, layouts

## 1. Two paths — decide before writing

| | Platform widgets | Own markup |
|---|---|---|
| look | bounded by props (`siteHeader` has ~30) — **plus your own class on its band, see §4a** | anything |
| sticky header | `siteHeader.sticky` — works | needs the sticky key, with a caveat below |
| dropdown menu | `siteMenu.level2` — works, opens **on click** | you build it |
| effort | minutes | hours |

**⚠️ The single most expensive omission we have measured:** a chrome **group**
takes **any block, including `kind:"html"`** (as did `kit.top`/`kit.bottom`
before it). That is how a fully custom header is built. An agent spent a probe —
a write to a live shop — to discover this, because no guide said it.

## 2. How chrome is assembled

```
GROUP (a content block, group_type:"chrome")   props.widgets = [block ids]
   ↓ bound by
PUT /design/bindings/{resource_type}/{id}   { "slots": { "header": <group id> } }
```

Four slots make a page — `header` · `feed` · `body` · `footer` — and each has its
own ladder: this resource → the category chain → **the layout** (§2a) → the shop →
the earliest group of that label. `resource_type` is `shop` (id `0`), `category`,
`category_children`, `product`, `article`, plus `theme` and `system` (§2a).
`category_children` inherits down the whole subtree.

A slot has three answers, not two: absent from the map = inherit, `0` = **nothing
here** (the ladder stops — this is how a header is switched off, there is no
flag), an id = that group.

Read the current state first: `GET /v1/storefronts/resolve?slug=<shop>` → `chrome`,
and `GET /design/bindings/category/<id>` → `resolved` with the source of each slot.

⚠️ Shops built before 2026-08-21 still run on the legacy kit — a `kind:"chromeKit"`
block in the slot `chrome`, `props.kit = {top:[…], bottom:[…]}`. It keeps working;
write groups for anything new. Full model: `GET /docs/chrome` §3.1–3.5.

## 2a. Layouts and system pages

A **layout** ("Лейаут" in the admin, `theme` in the API) sits above the chrome: a
group-container `group_type:"theme"` whose "content" is not widgets but
**bindings** — `PUT /design/bindings/theme/{id}` with `header`, `feed`, `body`,
`footer`, plus a body per page kind (`body_product`, `body_category`,
`body_news`, `body_article`). Which layout a page uses is its own short ladder on
the slot `theme`: this resource → the category chain → the shop → the earliest
layout. So one call gives a whole section its own design:

```bash
PUT /design/bindings/category/648   {"slots": {"theme": 1837}}   # a section
PUT /design/bindings/shop/0         {"slots": {"theme": 1837}}   # the whole shop
```

**A layout also owns a folder** — the site's shared CSS, fonts, scripts and
favicon. That is where global styles belong: CSS left inside the header's html
widget is seen by the header alone. `vizen-own-markup` §6.2 has the folder,
`/themes/{id}/site/*` and the rules. Files of the **active** layout also answer
on a second, unversioned address — `GET /_site/<path>` (`/_site/logo.svg`,
`/_site/favicon.ico`) — for links that outlive a release: an email to a buyer, a
mail signature, an organisation card, someone else's site. It needs the tenant
host (without it "the active layout" is undefined → 404) and is cached
`public, max-age=300` with an ETag rather than forever. Inside the site keep
using `/_html/{doc}/{rid}/…`.

**An article is a design resource too** (2026-09-13):

```bash
PUT /design/bindings/article/<id>   {"slots": {"theme": 1837}}   # one article, another design
```

The ladder is `the article → its rubric and the rubric's ancestors → the layout →
the shop → the default` — the rubric branch is `category`, so a binding made on
the rubric itself does reach its articles. Read it back with the article:
`GET /articles/by-slug/{slug}` answers with `design` (the admin
`GET /articles/{id}` never resolves design). `body_article` stays a slot of the
**layout**, not of the article — asking for it on an article answers
`DESIGN_SLOT_NOT_FOR_RESOURCE`.

**System pages** — cart, checkout, account, orders, wishlist, search, deals,
dashboard, coupons, notfound — are design resources too: `resource_type:"system"`,
`resource_id` from the registry `cart=1 · checkout=2 · account=3 · orders=4 ·
wishlist=5 · search=6 · deals=7 · dashboard=8 · coupons=9 · notfound=10`, slots
`theme|header|footer` only (the storefront owns their body). Unbound, they
inherit the shop's layout and chrome:

```bash
PUT /design/bindings/system/1  {"slots": {"header": 1975}}   # 1 = cart
GET /design/system/cart?company_id=<N>                        # what actually applies
```

⚠️ In the guarded zones (`/cart`, `/checkout`, `/account`, `/wishlist`,
`/orders`, `/deals`, `/dashboard`) an author-owned (level 3) header renders
**without its scripts** and without its relative `<link rel="stylesheet">`
(measured 2026-09-12), and the layout's code does not run at all. Markup,
inline `<style>` and the layout folder's stylesheet stay — keep the header's
CSS there, and never promise the owner a script in the cart.

## 2b. The chrome is rendered on the server — what that changes for you

Since 2026-09-13 header, menu and footer are printed by the server; only the
pieces that are actually clicked ship JavaScript (the search and burger toggles,
the level-2 toggle). Two consequences, and neither is a new rule to follow:

- **the menu's level-2 panels arrive in the HTML**, closed (`data-open="0"`),
  within a node budget — 60 nodes on the storefront. A root whose panel does not
  fit loads it on the first click from `GET /categories?view=menu`, one request
  per tab, shared with the search suggest. So a panel that opens after a short
  pause on a huge catalogue is **normal**, not a defect to report;
- **nothing of this touches your own header.** The budget and the toggle belong
  to the platform `siteMenu` widget. Your markup in a chrome group is printed
  whole, exactly as before, and the active menu item is now computed from the
  request path on the server instead of after hydration — highlighting appears
  immediately.

## 3. Recipe: a custom header on one section only

1. publish the markup as a folder: `node backend-3D/tools/html-transfer/transfer.mjs ./header --name "Header"`
   (no `--page` — a chrome widget is not mounted on a page; the command creates
   the level-3 document, publishes the release and prints the document id);
2. wrap it in a block, `kind: "html"`;
3. create **two groups** (`group_type: "chrome"`, `props.widgets: [<block id>]`) —
   one for the header, one for the footer;
4. `PUT /design/bindings/category/<id>` with
   `{"slots": {"header": <header group>, "footer": <footer group>}}`.

The rest of the shop keeps what it had. Check that: open the home page and
confirm its `data-vz-kit` still shows the original id. Putting the same pair on
the **layout** instead (`/design/bindings/theme/<id>`) gives them to every page
that layout covers — that is the difference between "this section" and "this
design".

## 4. Sticky headers

`vz-sticky` on the root node of your markup makes the block stick. Two ways in,
both landing in the same place:

- own markup — `vz-sticky` on your root node;
- any block — `props.position: "sticky"`, with `props.stickyTop` in px when
  something above is already sticky.

Either way the platform applies it to the **block wrapper**, which is the only
node with room to travel: a sticky element placed inside the block cannot move,
because its parent is exactly as tall as the block.

⚠️ If you have older notes prescribing `position: fixed` plus a spacer of the
header's height — drop them. That workaround existed because the class used to
land inside the wrapper; it was fixed on 2026-08-19. `position: fixed` is not
the header tool: it reserves no height, `vz-sticky` does. It is fine for a side
rail or a modal in an unboxed level-3 block, where it resolves against the
viewport.

⚠️ The two ways in are **not** interchangeable everywhere. `props.position` and
`vz-sticky` both work for any block in a page zone or a chrome kit — that is the
custom-header case. They do **not** work on the platform `siteHeader` /
`siteMenu` / `siteFooter`, whose band the wrapper never draws: the header uses
its own `props.sticky: true`, and the menu and footer have no switch at all. Full
table by surface: `GET /docs/own-markup` §5.7.

⚠️ `props.overflow: "clip"` breaks stickiness for everything inside the block —
a clipping ancestor is not a scroll container the sticky element can stick to.
Sticky and clip are mutually exclusive; pick one per block.

## 4a. Keeping the widget and restyling it anyway

Before you replace the header with own markup, check whether the third path is
enough: `props.wrapperClass` puts **your** class on the band of `siteHeader`,
`siteMenu` or `siteFooter`, and you restyle that from your own CSS. The widget
keeps its live data, its search island and its cart counter; only the look
changes.

```jsonc
{ "kind": "siteHeader", "props": { "wrapperClass": "shop-hdr", "sticky": true } }
```

```css
.shop-hdr             { background: #0b0b12; border: 0 }
.shop-hdr > .vz-inner { max-width: none; padding-inline: 40px }
```

Two conditions, both hard: that CSS must live in a **level 3** document (level-1
styles are trapped inside a shadow root and never see the band), and you must
scope through your own class — never write bare `.vz-inner`, those names are
internal and have moved before. Full recipe and the anti-path:
`GET /docs/own-markup` §5.

## 5. Replacing the bundled product card

Build a `product` group whose `props.widgets` list is your widgets — your card,
breadcrumbs, whatever, in your order — and bind it to the slot `body`:

```bash
PUT /design/bindings/category_children/<id>   {"slots": {"body": <group id>}}
# one product only: PUT /design/bindings/product/<id>
```

`category_children` reaches every product of the subtree without touching a
single product record. The body is one group and the page is assembled on the
server, so **there is no two-cards problem any more**: a card with an explicit
`card.productId` draws that product anywhere, a card without one takes the page's
product.

⚠️ **Retired recipe, do not use it.** Older notes say to clear `refs: []` in each
product's `__page:top` to remove the second card. Under the group model that does
not fix anything — it wipes the contents of those pages. If you meet such
manifests in an unmigrated shop, they are read for compatibility and nothing
else. `hideSystemBlock` is not the answer either: it hides the whole technical
layer, including your own bound card.

## 6. Verify

- the home page and untouched sections show the original `data-vz-kit`;
- your section shows the new one;
- the menu opens (by click) and the header holds at several widths;
- a product page has exactly one card.

Full checklist: `vizen-troubleshoot`.
