---
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

A header or footer from a finished site moves as it is — `vizen-transfer` §3
step 4; the choice below is for building one.

| | Platform widgets | Own markup |
|---|---|---|
| look | configured by props (`siteHeader` has ~30) and **your layout CSS, 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** for shared CSS, fonts and favicon. Use
`vizen-own-markup` §6.2 for upload and `/docs/own-markup` §7.2 for the single
CSS/positioning surface matrix. `/_site/<path>` resolves the shop's active
layout on the tenant host, not a page-specific layout.

**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, thankyou, compare — 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 · thankyou=11 · compare=12`, 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`, `/coupons`, `/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.

Use sticky for a header in the flow. Before choosing a viewport-fixed overlay,
read `/docs/own-markup` §7.2: chrome keeps a separate query container, and actual
fixed anchoring depends on CSS containment/transform and the browser.

⚠️ 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 }
```

The wrapper's CSS must be outside a level-1 shadow root. Use your own wrapper
class to restrict the override; stable names do not make global restyling safe.
Canonical CSS/availability matrix: `/docs/own-markup` §§5.5,7.2.

## 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 page's own blocks: `__page:top` / `__page:bottom`
are the blocks the owner put before and after the inherited body in the page
editor (`/docs/components` §4.7.1). `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`.

For builder groups, preserve coordinated references and writer version:
`/docs/chrome` §3.1. Reuse the active theme's container class (§3.7 there).
Replacing a category grid through a token, including rollback, has one recipe:
`/docs/components` §4.8.
