For developers
Template reference
Reference for your own HTML and JS on a component: template syntax, variables, actions, sanitising, JS, limits and examples.
How you open the code editor is on Your own code on a component.
Where: the designer, Customize with code › Code on a component, in Advanced mode. The Custom component section follows the same rules.
The root is the storefront's
Every component has a root that the storefront draws. Your template is the content of the root.
- The header's root is
[data-bs-header]. The storefront sets the states[data-bs-transparent],[data-bs-scrolled]and[data-bs-sticky]on the root, even when the header is your own HTML. - Write no
<header>of your own in the template. Start with what goes inside. - Your CSS is scoped to the root. Your JS gets the root as
root. - A Custom component section has the root
[data-bs-block="<section-id>"].
Hooks and CSS reference lists every root.
Template syntax
The template is HTML with variables in double curly braces. This is the whole syntax.
| Written as | What |
|---|---|
{{storefront.name}} | The value, always HTML-escaped. An unknown variable gives empty text. |
{{#each nav}} … {{/each}} | Repeats the block for each entry in a list. |
{{this}}, {{this.label}} | The entry in an #each, or a field on the entry. A bare {{label}} is looked up on the entry first. |
{{@index}}, {{@first}}, {{@last}} | The counter from 0, and true on the first and last pass. |
{{#if cart.count}} … {{else}} … {{/if}} | Shows the block when the value is true. {{else}} is optional. |
{{#unless brand.logoUrl}} … {{/unless}} | The opposite of #if. |
- Empty text,
0, an empty list andfalsecount as false. - There is no way to output unescaped HTML.
- There are no helpers and no included templates.
Limits
| Limit | Value |
|---|---|
| Template length | 200 000 characters |
| The rendered HTML | 200 000 characters |
| CSS length | 200 000 characters |
| JS length | 100 000 characters |
| Nested blocks | 32 levels |
| Steps per render | 50 000 |
- BunStack cuts anything longer and shows a warning that says where, for example
css cut at 200 000 characters. - A render stops after 50 000 steps. A loop inside a loop cannot hang the storefront.
- The designer renders a template of 33 333 characters or more once without data. When that render hits a limit, the limit shows under May cut with some data on the Code tab.
- The storefront lists only what its own render cuts.
Errors and warnings
A template that cannot be read draws nothing. This applies, for example, to an {{#each}} that is never closed, or a block closed with the wrong tag. The storefront then shows the standard look, and the designer shows the error at the code.
| Where | What you see |
|---|---|
| The designer | The error or warning in red below the code. |
| The storefront's HTML, error | <!-- bs-component-error: … --> inside <template data-bs-component-errors>. |
| The storefront's HTML, warning | <!-- bs-component-warning: … --> in the same place. The component is drawn anyway. |
| A product card or banner that was cut | data-bs-component-warning on the root. The console shows the reason once per style. |
A warning can concern a CSS rule that could not be scoped, or an option that was rejected.
Variables
Header, Menu panel and Footer
The three share one set of variables. The Custom component section gets the same set.
| Variable | Type | What |
|---|---|---|
storefront.name | string | The storefront's name |
storefront.slug | string | The storefront's slug |
storefront.host | string | The storefront's public host: its own domain once verified, else its bunstack.shop host |
brand.logoUrl | string | The logo's URL, empty when none |
brand.footerLogoUrl | string | Light logo for dark surfaces. Falls back to logoUrl |
brand.logoHeight | number | The logo's height in px (16–96) |
brand.tagline | string | The tagline |
brand.copyright | string | The copyright line |
nav[] | list | The top categories: label, href, children[] |
nav[].label | string | The category's name |
nav[].href | string | Link to the category |
nav[].children[] | list | Child categories with the same fields as the top level, label and href. Loop with {{#each this.children}}{{this.label}}{{/each}} |
pages[] | list | Pages shown in the menu: label, href |
cart.count | number | Always 0 when the page is rendered, because the browser keeps the cart. Write it in <span data-bs-cart-count>. The storefront keeps that element up to date |
social[] | list | Social links: key, label, href |
contact.email | string | Contact email, empty when unset |
contact.phone | string | Phone, empty when unset |
currency.code | string | Selected currency |
currency.options[] | list | The currencies the storefront sells in: code, label. Empty for a visitor from a country under Not sold to at Prices hidden |
search.href | string | Link to the search page |
menu.links[] | list | The menu's big links in order, from Menu panel › Links: label, href, children[], category, draft, products[] |
menu.links[].label | string | The link text: your own, otherwise the category's or the page's name |
menu.links[].href | string | Where the link goes |
menu.links[].children[] | list | A category link's subcategories: label, href. Empty for a page or an address |
menu.links[].category | string | The category's id for a category link, otherwise empty |
menu.links[].draft | boolean | The page is not live yet. Only on the designer's canvas: the storefront does not show the link |
menu.links[].products[] | list | The cards for the link: the category's products, featured first and with the subcategories, otherwise the default cards. With the card's fields |
menu.secondary[] | list | The menu's small links in order: label, href |
menu.heading | string | The heading over the products (Heading over the products), empty when there is none |
menu.products[] | list | The default cards: the featured products in order, with the card's fields. Empty until the menu has fetched them |
i18n.menu | string | "Menu" in the storefront's language |
i18n.close | string | "Close" |
i18n.search | string | "Search" |
i18n.searchPlaceholder | string | The search box placeholder |
i18n.cart | string | "Cart" |
i18n.wishlist | string | "Wishlist" |
i18n.shop | string | "Shop" |
i18n.contact | string | "Contact" |
i18n.orders | string | "Track your order" |
Product card
One template per card. It is drawn for each product in a grid.
| Variable | Type | What |
|---|---|---|
product.id | string | The product's id, for data-bs-product-id |
product.title | string | The title |
product.href | string | Link to the product page |
product.price | string | The price, formatted in the shopper's currency. When the variants cost different amounts it is the lowest, with "från" in front on a Swedish storefront and "from" on any other. Empty when unpriced |
product.priceAmount | string | The same price without "från" or "from". Empty when unpriced |
product.priceVaries | boolean | True when the variants that can be bought cost different amounts. The price is then the lowest of them |
product.compareAtPrice | string | The previous price: the lowest price in the 30 days before the reduction, formatted. Empty when the storefront states no reduction |
product.currency | string | Currency code |
product.image | string | The first image's URL, empty when none |
product.imageAlt | string | The image's alt text |
product.badge | string | The sale badge's text, empty when none |
product.sku | string | Empty. The card's product data has no SKU |
product.inStock | boolean | True on the card, the checkout checks stock. False for a visitor from a country under Not sold to, where the storefront does not sell. Use it for the buy button, not for an out-of-stock text. At Prices hidden price, priceAmount, compareAtPrice and badge are empty too |
product.variantId | string | Empty. Add-to-cart from a card puts the product's only variant in the cart, or opens the product page when the product has several |
product.meta | string | The tag line under the title |
storefront.name | string | The storefront's name |
storefront.slug | string | The storefront's slug |
storefront.host | string | The storefront's public host: its own domain once verified, else its bunstack.shop host |
brand.logoUrl | string | The logo's URL, empty when none |
brand.footerLogoUrl | string | Light logo for dark surfaces. Falls back to logoUrl |
brand.logoHeight | number | The logo's height in px (16–96) |
brand.tagline | string | The tagline |
brand.copyright | string | The copyright line |
Product page
The gallery and the buy panel, drawn for the variant shown. When the customer chooses another variant with select-variant, the template is drawn again. The price, the SKU, the stock row, product.variantId and every selected update with it. The rest of the page is the standard page's: details, reviews and related products. Under the template the storefront draws the product's questions, the line about cart add-ons and the Optional add-ons box. add-to-cart in the template adds the add-ons the customer ticked and stops at a required question left empty, as the standard button does.
| Variable | Type | What |
|---|---|---|
product.id | string | The product's id, for data-bs-product-id |
product.title | string | The title |
product.href | string | Link to the product page |
product.description | string | The full description as text, without its formatting. Empty when none |
product.paragraphs[] | list | The description's paragraphs, headings and lists as text, one string per block |
product.shortDescription | string | The short description as text. Empty when none |
product.intro | string | The text by the price: the short description, otherwise the first paragraph, clipped |
product.price | string | The shown variant's price, formatted in the shopper's currency. Empty when unpriced |
product.compareAtPrice | string | The previous price: the lowest price in the 30 days before the reduction, formatted. Empty when the storefront states no reduction |
product.currency | string | Currency code |
product.image | string | The first image's URL (the shown variant's own image leads), empty when none |
product.imageAlt | string | The first image's alt text |
product.images[] | list | Every image: url, alt, caption (the merchant's own text, empty when none) |
product.badge | string | The sale badge's text, empty when none |
product.sku | string | The shown variant's SKU, empty when none |
product.stockStatus | string | in_stock, preorder or out_of_stock, for the shown variant |
product.stockLabel | string | The stock row's text in the storefront's language ("Only 3 left", "Preorder · ships in 10 days") |
product.inStock | boolean | Add to cart works: stock available on Automatic, or the mode In stock (manual) or Preorder. Never on Out of stock, and never for a visitor from a country under Not sold to. Use it for the buy button and stockStatus for the stock text. At Prices hidden price, compareAtPrice, badge and the variants' price are empty |
product.variantId | string | The shown variant, for data-bs-variant-id on add-to-cart. Empty without variants |
product.variantTitle | string | The shown variant's title, empty without variants |
product.variants[] | list | Every variant: id, title, sku, price, stockStatus, stockLabel, inStock, selected, options[] (name, value) |
product.options[] | list | The option groups in picker order: name, label (the name as the picker writes it, for example Number of balls for Number-of-balls), values[] |
product.options[].values[] | list | One value: value, selected, available, variantId (what select-variant picks for the value) |
product.categories[] | list | The categories the product is in, the first = the breadcrumb: name, slug, href |
product.tags[] | list | The product's tags, as strings |
product.meta | string | The tag line (the first tags), empty when none |
product.properties[] | list | The properties the page shows, in group order: group (empty without a group), label, value (as the storefront prints it), values[] (the value's items), display (bar, chips, table or badge), numeric, percent (a bar's fill on its scale, 0–100). Filter only and Hidden are not in it |
storefront.name | string | The storefront's name |
storefront.slug | string | The storefront's slug |
storefront.host | string | The storefront's public host: its own domain once verified, else its bunstack.shop host |
brand.logoUrl | string | The logo's URL, empty when none |
brand.footerLogoUrl | string | Light logo for dark surfaces. Falls back to logoUrl |
brand.logoHeight | number | The logo's height in px (16–96) |
brand.tagline | string | The tagline |
brand.copyright | string | The copyright line |
currency.code | string | The selected currency |
cart.count | number | Always 0 when the page is rendered. Write it in <span data-bs-cart-count>, as in the header |
i18n.addToCart | string | "Add to cart" in the storefront's language |
i18n.chooseVariant | string | "Choose variant" |
i18n.inStock | string | "In stock" |
i18n.outOfStock | string | "Out of stock" |
i18n.preorder | string | "Preorder" |
i18n.priceOnRequest | string | "Price on request" |
i18n.vat | string | "Incl. VAT" or "Excl. VAT", as the storefront prices |
i18n.wishlist | string | "Add to wishlist" |
i18n.home | string | "Home", the breadcrumb's first crumb |
Gallery
The product images on the product page. When the product page is your own HTML, that template draws the gallery itself, and the gallery's design is not drawn at all. The code editor, the storefront's report and the canvas show this, for example Not rendering — the product page is your own HTML and draws the gallery itself.
| Variable | Type | What |
|---|---|---|
images[] | list | Every image, the shown variant's own first: url, alt, caption (the merchant's own text, empty when none) |
image | string | The first image's URL, empty when none |
imageAlt | string | The first image's alt text |
count | number | The number of images |
title | string | The product's title |
product.id | string | The product's id, for data-bs-product-id on toggle-wishlist |
product.title | string | The title |
product.href | string | Link to the product page |
storefront.name | string | The storefront's name |
storefront.slug | string | The storefront's slug |
storefront.host | string | The storefront's public host: its own domain once verified, else its bunstack.shop host |
brand.logoUrl | string | The logo's URL, empty when none |
brand.footerLogoUrl | string | Light logo for dark surfaces. Falls back to logoUrl |
brand.logoHeight | number | The logo's height in px (16–96) |
brand.tagline | string | The tagline |
brand.copyright | string | The copyright line |
i18n.images | string | "Product images" in the storefront's language |
i18n.previous | string | "Previous image" |
i18n.next | string | "Next image" |
i18n.noImage | string | "No image" |
i18n.wishlist | string | "Add to wishlist" |
Category
The collection page with the products it lists: a category, All products or search results. The template gets the page's filters as data in filters[]: each group with its values, counts, ticks and a link that ticks or unticks the value. The shop puts no filters into the template's own markup. A template lists what the address says (?sort=…&f.…&category=…). The template replaces the Collection page's sections, the product grid included. The page's own CSS (the Code tab of the Collection page) still applies.
| Variable | Type | What |
|---|---|---|
category.id | string | The category's id |
category.name | string | The category's name |
category.description | string | The description, empty when none |
category.slug | string | The category's slug |
category.href | string | Link to the category |
category.image | string | The category image's URL, empty when none |
products[] | list | The products listed, after the customer's filters and sort. One entry per product with the card's fields: id, title, href, price, priceAmount, priceVaries, compareAtPrice, currency, image, imageAlt, badge, sku, inStock, variantId, meta. For a visitor from a country under Not sold to inStock is false, and at Prices hidden price, priceAmount, compareAtPrice and badge are empty |
count | number | The number of products listed (after filters) |
total | number | The number of products in the category |
empty | boolean | True when nothing is listed |
filters[] | list | The filter groups the page shows, in order. On All products and in search results the Category group comes first |
filters[].label | string | The group's name |
filters[].param | string | The address parameter its values write: f. |
filters[].selectedCount | number | Values ticked in the group |
filters[].values[] | list | The group's values, in order |
filters[].values[].label | string | What the value reads as (a category's name) |
filters[].values[].value | string | The value as the link writes it |
filters[].values[].count | number | Products on the page with the value |
filters[].values[].depth | number | A subcategory's depth in the Category group, 0 otherwise |
filters[].values[].selected | boolean | Ticked by the shopper |
filters[].values[].href | string | The page with this value ticked or unticked, the other ticks kept |
filtered | number | Values the shopper has ticked |
clearHref | string | The page with no ticks |
sticky | boolean | Filters stay in view is on, and the page has filters |
storefront.name | string | The storefront's name |
storefront.slug | string | The storefront's slug |
storefront.host | string | The storefront's public host: its own domain once verified, else its bunstack.shop host |
brand.logoUrl | string | The logo's URL, empty when none |
brand.footerLogoUrl | string | Light logo for dark surfaces. Falls back to logoUrl |
brand.logoHeight | number | The logo's height in px (16–96) |
brand.tagline | string | The tagline |
brand.copyright | string | The copyright line |
i18n.shop | string | "Shop" in the storefront's language, the breadcrumb's first crumb |
i18n.products | string | "Products" |
i18n.empty | string | "No products in this category yet." |
i18n.noMatch | string | "No products match your filters." |
i18n.filters | string | "Filters" |
i18n.clear | string | "Clear filters" |
Banner
The section's own settings.
| Variable | Type | What |
|---|---|---|
heading | string | The heading |
subtitle | string | The subtitle |
imageUrl | string | The image's URL |
imageAlt | string | The image's alt text |
ctaLabel | string | The button's text |
ctaHref | string | The button's link |
align | string | left or center |
valign | string | top, center or bottom |
size | string | xs … xl or screen |
bleed | string | contained or full |
ctaStyle | string | primary, light or outline |
storefront.name | string | The storefront's name |
storefront.slug | string | The storefront's slug |
storefront.host | string | The storefront's public host: its own domain once verified, else its bunstack.shop host |
brand.logoUrl | string | The logo's URL, empty when none |
brand.footerLogoUrl | string | Light logo for dark surfaces. Falls back to logoUrl |
brand.logoHeight | number | The logo's height in px (16–96) |
brand.tagline | string | The tagline |
brand.copyright | string | The copyright line |
Write the heading as <h1> when the banner is the page's title block. Then it is the page's title. When the template has no <h1>, the storefront adds a hidden page title after the banner when the banner is the page's title block. The first banner or hero on the page is the title block, unless a heading section at level 1 takes the title.
Actions
Your markup has no event handlers of its own. Put data-bs-action on an element, and the storefront does the rest on click. The storefront uses the same listeners as for the standard markup. The attribute works on any element, including an <svg>.
| Action | Required | Optional | What |
|---|---|---|---|
data-bs-action="open-cart" | – | – | Opens the cart the way the storefront's choice under Checkout › Cart says: the panel, or the cart page |
data-bs-action="open-search" | – | – | Opens the search box |
data-bs-action="open-menu" | – | – | Opens the menu panel |
data-bs-action="close-menu" | – | – | Closes the menu panel |
data-bs-action="add-to-cart" | data-bs-product-id | data-bs-variant-id, data-bs-quantity | Adds the product to the cart |
data-bs-action="toggle-wishlist" | data-bs-product-id | – | Adds the product to the wishlist, or removes it |
data-bs-action="select-variant" | data-bs-variant-id | – | Selects the variant on the product page. The price, the stock row, product.variantId and every selected update with it. Only in the product page's own template |
data-bs-action="open-cookie-settings" | – | – | Opens the cookie settings, the same dialog as the standard footer's link |
data-bs-action="set-currency" | data-bs-currency | – | Switches the currency to the code in the attribute, as in data-bs-currency="EUR". The codes are in currency.options[] |
data-bs-action="subscribe-newsletter" | data-bs-newsletter | – | Signs the address in the name="email" field up for the newsletter. Put the action on the <form> or on its submit button |
- When an attribute that is required is missing, nothing happens on the click.
data-bs-quantitymust be a positive whole number. Anything else reads as 1.- An action that does not exist, or that lacks a required attribute, gives a warning when the template is compiled. The warning shows at the code and as
<!-- bs-component-warning: … -->in the storefront's HTML. subscribe-newsletterneeds a form withdata-bs-newsletterand a fieldname="email". It runs when the form is submitted, so Enter in the field works. The address gets the same check and the same reply as in the standard footer.
add-to-cart without data-bs-variant-id, for example on a card, asks the storefront about the product:
| The product has | The storefront |
|---|---|
| No variants | Adds the product to the cart |
| One variant in stock | Adds that variant to the cart |
| Several variants, or one variant that is sold out or on preorder | Opens the product page |
Hooks
The hooks are the attributes in the storefront's HTML that your CSS and JS target: the component's root, inner elements and states on the root. Every hook per component, the hooks on the page and the rules in the tokens CSS are under Hooks and CSS reference.
Sanitising
BunStack sanitises the rendered template before it reaches the page, the same way on the server and in the browser.
| Group | Elements kept |
|---|---|
| Structure | div, section, header, footer, nav, aside, article, ul, ol, li, p, h1–h6, dl, dt, dd, details, summary, figure, figcaption, blockquote, address |
| Text | span, strong, em, b, i, u, s, small, sup, sub, mark, cite, abbr, time, br, hr, wbr |
| Links and images | a, img, picture, source |
| Forms | button, input, label, form |
| SVG | svg, g, path, circle, ellipse, rect, line, polyline, polygon |
Attributes kept:
- on every element:
class,id,title,role,tabindex,lang,dir,hidden,style,translate, everyaria-*, everydata-bs-*anddata-footer-* - on links:
href,target,rel - on images:
src,srcset,sizes,alt,width,height,loading - on buttons and fields:
type,name,value,placeholder,disabled - on
form:actionwhen it is a path in the storefront, for example/search, andmethodwhen it isgetorpost - on SVG:
viewBox,fill,stroke,stroke-width,d,pointsand the other drawing attributes
BunStack removes:
script,style,iframe,object,embed,noscript,template,textarea,select,video,audio,canvas,dialog,mathandtitle, with all their content- every
on*attribute and every comment data-*other thandata-bs-*anddata-footer-*- addresses that are not relative,
https:,http:,mailto:ortel:, exceptdata:images onimg - an
actionthat points outside the storefront
BunStack removes an element that is not in the list but keeps its content. It sanitises style="…" like other CSS. On links it keeps target="_blank" and adds rel="noopener", and it keeps target="_self". It removes other target values. It closes open elements at the end, so the result is always balanced HTML.
Classes and sizes
The standard markup has a few Tailwind classes, for example flex, items-center and gap-4. The designer compiles no new classes from your template. A class that is not already in the storefront does nothing. Set size and spacing on the Design tab, in Code › CSS on the hooks or as style on the element.
CSS
The CSS on the Code tab applies to the component only. How the selectors are scoped to the root, what sanitising removes and how to load your own fonts are under Hooks and CSS reference.
About px and rem: see Your own CSS and head code.
JS
- The JS runs once per page load, when the page has loaded, on every page where the component is.
- It gets the component's root element as
root. When the component is not on the page, it does not run. - The menu panel's root exists only while the panel is open. Its JS runs every time the panel opens, against the new root. Bind listeners on
root, not ondocument. - An error, a syntax error included, goes to the browser console with the style's name. It stops neither the page nor other components' JS.
- The code also runs on the canvas. Turn off Run JS in the builder to pause it there for the session. The storefront and Preview (⌘P) still run it.
root.querySelectorAll('[data-bs-header-icons] a').forEach((a) => {
a.addEventListener('mouseenter', () => a.classList.add('is-hot'));
});
A product card is on a page many times. root is then the first card. Use document.querySelectorAll('[data-bs-card]') for all of them.
The page's JS on the Code tab runs the same way, without root. What you write on the Collection page applies to every category page.
Example: a navbar on a Classic storefront
The goal is a full-width header. It is transparent over the hero image with a white logo and white icons, and white once the page has scrolled 24 px. On the right are four icons of 30 px: search, wishlist, cart and menu. Most of it is controls. The rest is six lines of CSS.
The controls on the component panel for Header:
- Replace the home page's text hero with an Image banner with Width full and a dark photo.
- Choose Look: Icons + panel.
- Set Options: Sticky on, Transparent over hero on, Menu label off, Search on, Cart on, Announcement bar off.
- Choose the black logo as Logo under Content, with height 40. Choose the white one as Light logo.
- Check that Theme › Colours › Header text over hero is white.
Turn on Advanced, press Customize with code and write under Code › CSS:
[data-bs-header] { padding: 16px; }
@media (min-width: 768px) { [data-bs-header] { padding: 32px; } }
[data-bs-header-inner] { max-width: none; }
[data-bs-header-icons] { gap: 16px; }
[data-bs-header-icons] a, [data-bs-header-icons] button { opacity: 1; }
[data-bs-header-icons] svg { width: 30px; height: 30px; stroke-width: 1.6; }
Press Save as new style and name the style "My navbar". The style is now under Your header styles in every storefront of the workspace. The logos belong to the theme. Set them under Content in each storefront.
The same navbar as your own HTML
Open Code › HTML and press Start from the standard markup. The field fills with the standard look's markup. The template below is the same navbar written by hand. It uses the same inner hooks, so the CSS above applies unchanged. The burger opens the storefront's standard panel, which has its own Close button and closes with Escape.
<div data-bs-header-inner>
<a href="/" aria-label="{{storefront.name}}">
<img src="{{brand.logoUrl}}" alt="{{storefront.name}}" height="{{brand.logoHeight}}" class="logo-dark">
<img src="{{brand.footerLogoUrl}}" alt="" height="{{brand.logoHeight}}" class="logo-light">
</a>
<div data-bs-header-icons>
<button type="button" data-bs-action="open-search" aria-label="{{i18n.search}}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>
</button>
<a href="/wishlist" data-bs-wishlist-icon aria-label="{{i18n.wishlist}}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" aria-hidden="true"><path d="M20.8 4.6a5.5 5.5 0 0 0-7.8 0L12 5.7l-1-1.1a5.5 5.5 0 0 0-7.8 7.8L12 21l8.8-8.6a5.5 5.5 0 0 0 0-7.8z"/></svg>
</a>
<button type="button" data-bs-action="open-cart" data-bs-cart-icon aria-label="{{i18n.cart}}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" aria-hidden="true"><path d="M6 6h15l-1.5 9h-12z"/><circle cx="9" cy="20" r="1"/><circle cx="18" cy="20" r="1"/><path d="M6 6 5 3H2"/></svg>
<span data-bs-cart-count>{{cart.count}}</span>
</button>
<button type="button" data-bs-action="open-menu" data-bs-menu-button aria-label="{{i18n.menu}}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" aria-hidden="true"><path d="M3 6h18M3 12h18M3 18h18"/></svg>
</button>
</div>
</div>
In the standard markup the storefront swaps the logo itself. In your own template both images are in the markup, and the CSS chooses with the state on the root:
[data-bs-header-inner] { display: flex; align-items: center; justify-content: space-between; }
[data-bs-header-icons] { display: flex; align-items: center; }
.logo-light { display: none; }
[data-bs-transparent]:not([data-bs-scrolled]) .logo-dark { display: none; }
[data-bs-transparent]:not([data-bs-scrolled]) .logo-light { display: inline; }
The menu panel as your own HTML
The storefront draws the panel's root [data-bs-menu] and opens it with open-menu. The template loops the menu's links from Menu panel › Links, the big ones with their subcategories and then the small ones:
<nav aria-label="{{i18n.menu}}">
<ul>
{{#each menu.links}}
<li>
<a href="{{this.href}}">{{this.label}}</a>
{{#if this.children}}
<ul>{{#each this.children}}<li><a href="{{this.href}}">{{this.label}}</a></li>{{/each}}</ul>
{{/if}}
</li>
{{/each}}
{{#each menu.secondary}}<li><a href="{{this.href}}">{{this.label}}</a></li>{{/each}}
</ul>
{{#if contact.email}}<p data-bs-menu-contact><a href="mailto:{{contact.email}}">{{contact.email}}</a></p>{{/if}}
<button type="button" data-bs-action="close-menu">{{i18n.close}}</button>
</nav>
open-search opens the storefront's search box as a layer above the header and the panel. When your markup has an element with [data-bs-search-slot], the storefront draws the search box there instead. A search form of your own also works:
<form action="/search" method="get" role="search">
<input type="search" name="q" placeholder="{{i18n.searchPlaceholder}}" aria-label="{{i18n.search}}">
<button type="submit">{{i18n.search}}</button>
</form>
A product card as your own HTML
The storefront draws the card's root [data-bs-card]. The template is the inside. The button gets its product through the attributes.
<a href="{{product.href}}">
<div data-bs-card-media>
{{#if product.image}}<img src="{{product.image}}" alt="{{product.imageAlt}}" loading="lazy">{{/if}}
{{#if product.badge}}<span class="badge">{{product.badge}}</span>{{/if}}
</div>
<div data-bs-card-body>
<h3 data-bs-card-title>{{product.title}}</h3>
<p data-bs-card-price>{{#if product.compareAtPrice}}<s>{{product.compareAtPrice}}</s> {{/if}}{{product.price}}</p>
</div>
</a>
<button type="button" data-bs-action="add-to-cart" data-bs-product-id="{{product.id}}">Add to cart</button>