BunStackDocs

For developers

Template reference

Reference for your own HTML and JS on a component: template syntax, variables, actions, sanitising, JS, limits and examples.

Last updated · 27 min read

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 asWhat
{{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 and false count as false.
  • There is no way to output unescaped HTML.
  • There are no helpers and no included templates.

Limits

LimitValue
Template length200 000 characters
The rendered HTML200 000 characters
CSS length200 000 characters
JS length100 000 characters
Nested blocks32 levels
Steps per render50 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.

WhereWhat you see
The designerThe 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 cutdata-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

The three share one set of variables. The Custom component section gets the same set.

VariableTypeWhat
storefront.namestringThe storefront's name
storefront.slugstringThe storefront's slug
storefront.hoststringThe storefront's public host: its own domain once verified, else its bunstack.shop host
brand.logoUrlstringThe logo's URL, empty when none
brand.footerLogoUrlstringLight logo for dark surfaces. Falls back to logoUrl
brand.logoHeightnumberThe logo's height in px (16–96)
brand.taglinestringThe tagline
brand.copyrightstringThe copyright line
nav[]listThe top categories: label, href, children[]
nav[].labelstringThe category's name
nav[].hrefstringLink to the category
nav[].children[]listChild categories with the same fields as the top level, label and href. Loop with {{#each this.children}}{{this.label}}{{/each}}
pages[]listPages shown in the menu: label, href
cart.countnumberAlways 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[]listSocial links: key, label, href
contact.emailstringContact email, empty when unset
contact.phonestringPhone, empty when unset
currency.codestringSelected currency
currency.options[]listThe currencies the storefront sells in: code, label. Empty for a visitor from a country under Not sold to at Prices hidden
search.hrefstringLink to the search page
menu.links[]listThe menu's big links in order, from Menu panel › Links: label, href, children[], category, draft, products[]
menu.links[].labelstringThe link text: your own, otherwise the category's or the page's name
menu.links[].hrefstringWhere the link goes
menu.links[].children[]listA category link's subcategories: label, href. Empty for a page or an address
menu.links[].categorystringThe category's id for a category link, otherwise empty
menu.links[].draftbooleanThe page is not live yet. Only on the designer's canvas: the storefront does not show the link
menu.links[].products[]listThe 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[]listThe menu's small links in order: label, href
menu.headingstringThe heading over the products (Heading over the products), empty when there is none
menu.products[]listThe default cards: the featured products in order, with the card's fields. Empty until the menu has fetched them
i18n.menustring"Menu" in the storefront's language
i18n.closestring"Close"
i18n.searchstring"Search"
i18n.searchPlaceholderstringThe search box placeholder
i18n.cartstring"Cart"
i18n.wishliststring"Wishlist"
i18n.shopstring"Shop"
i18n.contactstring"Contact"
i18n.ordersstring"Track your order"

Product card

One template per card. It is drawn for each product in a grid.

VariableTypeWhat
product.idstringThe product's id, for data-bs-product-id
product.titlestringThe title
product.hrefstringLink to the product page
product.pricestringThe 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.priceAmountstringThe same price without "från" or "from". Empty when unpriced
product.priceVariesbooleanTrue when the variants that can be bought cost different amounts. The price is then the lowest of them
product.compareAtPricestringThe previous price: the lowest price in the 30 days before the reduction, formatted. Empty when the storefront states no reduction
product.currencystringCurrency code
product.imagestringThe first image's URL, empty when none
product.imageAltstringThe image's alt text
product.badgestringThe sale badge's text, empty when none
product.skustringEmpty. The card's product data has no SKU
product.inStockbooleanTrue 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.variantIdstringEmpty. 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.metastringThe tag line under the title
storefront.namestringThe storefront's name
storefront.slugstringThe storefront's slug
storefront.hoststringThe storefront's public host: its own domain once verified, else its bunstack.shop host
brand.logoUrlstringThe logo's URL, empty when none
brand.footerLogoUrlstringLight logo for dark surfaces. Falls back to logoUrl
brand.logoHeightnumberThe logo's height in px (16–96)
brand.taglinestringThe tagline
brand.copyrightstringThe 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.

VariableTypeWhat
product.idstringThe product's id, for data-bs-product-id
product.titlestringThe title
product.hrefstringLink to the product page
product.descriptionstringThe full description as text, without its formatting. Empty when none
product.paragraphs[]listThe description's paragraphs, headings and lists as text, one string per block
product.shortDescriptionstringThe short description as text. Empty when none
product.introstringThe text by the price: the short description, otherwise the first paragraph, clipped
product.pricestringThe shown variant's price, formatted in the shopper's currency. Empty when unpriced
product.compareAtPricestringThe previous price: the lowest price in the 30 days before the reduction, formatted. Empty when the storefront states no reduction
product.currencystringCurrency code
product.imagestringThe first image's URL (the shown variant's own image leads), empty when none
product.imageAltstringThe first image's alt text
product.images[]listEvery image: url, alt, caption (the merchant's own text, empty when none)
product.badgestringThe sale badge's text, empty when none
product.skustringThe shown variant's SKU, empty when none
product.stockStatusstringin_stock, preorder or out_of_stock, for the shown variant
product.stockLabelstringThe stock row's text in the storefront's language ("Only 3 left", "Preorder · ships in 10 days")
product.inStockbooleanAdd 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.variantIdstringThe shown variant, for data-bs-variant-id on add-to-cart. Empty without variants
product.variantTitlestringThe shown variant's title, empty without variants
product.variants[]listEvery variant: id, title, sku, price, stockStatus, stockLabel, inStock, selected, options[] (name, value)
product.options[]listThe 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[]listOne value: value, selected, available, variantId (what select-variant picks for the value)
product.categories[]listThe categories the product is in, the first = the breadcrumb: name, slug, href
product.tags[]listThe product's tags, as strings
product.metastringThe tag line (the first tags), empty when none
product.properties[]listThe 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.namestringThe storefront's name
storefront.slugstringThe storefront's slug
storefront.hoststringThe storefront's public host: its own domain once verified, else its bunstack.shop host
brand.logoUrlstringThe logo's URL, empty when none
brand.footerLogoUrlstringLight logo for dark surfaces. Falls back to logoUrl
brand.logoHeightnumberThe logo's height in px (16–96)
brand.taglinestringThe tagline
brand.copyrightstringThe copyright line
currency.codestringThe selected currency
cart.countnumberAlways 0 when the page is rendered. Write it in <span data-bs-cart-count>, as in the header
i18n.addToCartstring"Add to cart" in the storefront's language
i18n.chooseVariantstring"Choose variant"
i18n.inStockstring"In stock"
i18n.outOfStockstring"Out of stock"
i18n.preorderstring"Preorder"
i18n.priceOnRequeststring"Price on request"
i18n.vatstring"Incl. VAT" or "Excl. VAT", as the storefront prices
i18n.wishliststring"Add to wishlist"
i18n.homestring"Home", the breadcrumb's first crumb

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.

VariableTypeWhat
images[]listEvery image, the shown variant's own first: url, alt, caption (the merchant's own text, empty when none)
imagestringThe first image's URL, empty when none
imageAltstringThe first image's alt text
countnumberThe number of images
titlestringThe product's title
product.idstringThe product's id, for data-bs-product-id on toggle-wishlist
product.titlestringThe title
product.hrefstringLink to the product page
storefront.namestringThe storefront's name
storefront.slugstringThe storefront's slug
storefront.hoststringThe storefront's public host: its own domain once verified, else its bunstack.shop host
brand.logoUrlstringThe logo's URL, empty when none
brand.footerLogoUrlstringLight logo for dark surfaces. Falls back to logoUrl
brand.logoHeightnumberThe logo's height in px (16–96)
brand.taglinestringThe tagline
brand.copyrightstringThe copyright line
i18n.imagesstring"Product images" in the storefront's language
i18n.previousstring"Previous image"
i18n.nextstring"Next image"
i18n.noImagestring"No image"
i18n.wishliststring"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.

VariableTypeWhat
category.idstringThe category's id
category.namestringThe category's name
category.descriptionstringThe description, empty when none
category.slugstringThe category's slug
category.hrefstringLink to the category
category.imagestringThe category image's URL, empty when none
products[]listThe 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
countnumberThe number of products listed (after filters)
totalnumberThe number of products in the category
emptybooleanTrue when nothing is listed
filters[]listThe filter groups the page shows, in order. On All products and in search results the Category group comes first
filters[].labelstringThe group's name
filters[].paramstringThe address parameter its values write: f., or category
filters[].selectedCountnumberValues ticked in the group
filters[].values[]listThe group's values, in order
filters[].values[].labelstringWhat the value reads as (a category's name)
filters[].values[].valuestringThe value as the link writes it
filters[].values[].countnumberProducts on the page with the value
filters[].values[].depthnumberA subcategory's depth in the Category group, 0 otherwise
filters[].values[].selectedbooleanTicked by the shopper
filters[].values[].hrefstringThe page with this value ticked or unticked, the other ticks kept
filterednumberValues the shopper has ticked
clearHrefstringThe page with no ticks
stickybooleanFilters stay in view is on, and the page has filters
storefront.namestringThe storefront's name
storefront.slugstringThe storefront's slug
storefront.hoststringThe storefront's public host: its own domain once verified, else its bunstack.shop host
brand.logoUrlstringThe logo's URL, empty when none
brand.footerLogoUrlstringLight logo for dark surfaces. Falls back to logoUrl
brand.logoHeightnumberThe logo's height in px (16–96)
brand.taglinestringThe tagline
brand.copyrightstringThe copyright line
i18n.shopstring"Shop" in the storefront's language, the breadcrumb's first crumb
i18n.productsstring"Products"
i18n.emptystring"No products in this category yet."
i18n.noMatchstring"No products match your filters."
i18n.filtersstring"Filters"
i18n.clearstring"Clear filters"

The section's own settings.

VariableTypeWhat
headingstringThe heading
subtitlestringThe subtitle
imageUrlstringThe image's URL
imageAltstringThe image's alt text
ctaLabelstringThe button's text
ctaHrefstringThe button's link
alignstringleft or center
valignstringtop, center or bottom
sizestringxs … xl or screen
bleedstringcontained or full
ctaStylestringprimary, light or outline
storefront.namestringThe storefront's name
storefront.slugstringThe storefront's slug
storefront.hoststringThe storefront's public host: its own domain once verified, else its bunstack.shop host
brand.logoUrlstringThe logo's URL, empty when none
brand.footerLogoUrlstringLight logo for dark surfaces. Falls back to logoUrl
brand.logoHeightnumberThe logo's height in px (16–96)
brand.taglinestringThe tagline
brand.copyrightstringThe 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>.

ActionRequiredOptionalWhat
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-iddata-bs-variant-id, data-bs-quantityAdds 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-quantity must 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-newsletter needs a form with data-bs-newsletter and a field name="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 hasThe storefront
No variantsAdds the product to the cart
One variant in stockAdds that variant to the cart
Several variants, or one variant that is sold out or on preorderOpens 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.

GroupElements kept
Structurediv, section, header, footer, nav, aside, article, ul, ol, li, p, h1–h6, dl, dt, dd, details, summary, figure, figcaption, blockquote, address
Textspan, strong, em, b, i, u, s, small, sup, sub, mark, cite, abbr, time, br, hr, wbr
Links and imagesa, img, picture, source
Formsbutton, input, label, form
SVGsvg, g, path, circle, ellipse, rect, line, polyline, polygon

Attributes kept:

  • on every element: class, id, title, role, tabindex, lang, dir, hidden, style, translate, every aria-*, every data-bs-* and data-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: action when it is a path in the storefront, for example /search, and method when it is get or post
  • on SVG: viewBox, fill, stroke, stroke-width, d, points and the other drawing attributes

BunStack removes:

  • script, style, iframe, object, embed, noscript, template, textarea, select, video, audio, canvas, dialog, math and title, with all their content
  • every on* attribute and every comment
  • data-* other than data-bs-* and data-footer-*
  • addresses that are not relative, https:, http:, mailto: or tel:, except data: images on img
  • an action that 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 on document.
  • 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:

  1. Replace the home page's text hero with an Image banner with Width full and a dark photo.
  2. Choose Look: Icons + panel.
  3. Set Options: Sticky on, Transparent over hero on, Menu label off, Search on, Cart on, Announcement bar off.
  4. Choose the black logo as Logo under Content, with height 40. Choose the white one as Light logo.
  5. 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>