Skip to content
Sneferu
by Hekana

Cart and controllers

Add a variant picker, add to cart and a cart drawer to a Sneferu theme with @hekana/sneferu-controllers, with no theme JavaScript, and style the cart page.

On this page

Themes cannot ship JavaScript. The behaviour of a store (picking a variant, adding to the cart, the cart drawer) comes from a small client runtime, @hekana/sneferu-controllers, that the platform loads. The commerce tags print the markup and a data-hk-controller hook; the controllers upgrade it. With no JavaScript the markup still works as links and plain forms.

TagWhat it printsController
hk-variant-pickerOne group of radio buttons (or a select) per optionvariant-picker
hk-add-to-cartA form: quantity, the chosen variant, a button, a message lineadd-to-cart
hk-cart-countAn empty live region the controller fillscart-count
hk-cart-itemsThe cart rows, with a part named emptycart-items
hk-cart-summarySubtotal and checkout linkcart-summary
hk-cart-drawerA <dialog> holding other tagscart-drawer
hk-product-galleryImages and thumbnailsgallery
blocks/product-buy.snf
<hk-variant-picker product="{{ product }}" display="buttons" />
<hk-add-to-cart
  product="{{ product }}"
  label="{{ block.settings.label }}"
  quantity="{{ block.settings.show_quantity }}"
/>
Result
A phone-sized product page with option buttons, a quantity stepper and an Add to cart button
The same blocks on a 390 px screen.

Rules the tags follow

  • A product whose options are malformed or missing shows a flat list of variants; add to cart still adds the default variant.
  • A sold-out product (or variant) shows a disabled button with the sold out label.
  • min_quantity: 500, quantity_step: 50 starts the quantity at 500 and steps by 50; the cap is dropped when it is below the minimum.
  • A product with no safe url gets a link to the cart page, styled as the button, instead of a form.
  • A variant with no title is listed as "Option 1", "Option 2", … (the option text) in the flat list, never by its id.
  • Prices on the page follow the variant picked (<hk-price amount="{{ product.price }}" product="{{ product }}" />).
  • Everything printed into data-hk-config is JSON, escaped for the attribute.

The cart page

hk-cart-items has an empty part: put the empty-state heading, image and button there. It also prints a link to the cart page that is visible until the controllers run, so a visitor whose script failed still has a way forward.

html
<hk-cart-items>
  <hk-part name="empty">
    <h2>{{ section.settings.empty_title }}</h2>
  </hk-part>
</hk-cart-items>
<hk-cart-summary />

Any element can open the drawer: put data-hk-open="cart-drawer" on a link to the cart page. Without JavaScript it is an ordinary link.

Styling hooks

sn-cart-item, sn-cart-item-title, sn-cart-item-price, sn-cart-item-qty, sn-cart-item-remove, sn-cart-empty, sn-cart-subtotal, sn-vp-option, sn-vp-value, sn-qty, sn-atc-button, sn-gallery-thumbs. The tags also emit the structural CSS (44 px targets, the drawer, reduced motion).

For hosts

ts
import { startControllers, type CartHost } from '@hekana/sneferu-controllers';
const stop = startControllers(document, host); // idempotent; call stop() to remove every listener

CartHost supplies get(), add(), update(), remove(), subscribe(), format(), checkoutUrl and t(). The controllers make no network calls and never compute authoritative totals; the host's cart store and the server do.

Each CartLine from get() may carry the product's quantity rules:

FieldMeaning
minQuantityThe least quantity the line may have (whole number, default 1). The minus button removes the line when one more step would go below it.
quantityStepWhat the plus and minus buttons add or take away (whole number, default 1).
maxQuantityThe most the line may have. The plus button is marked aria-disabled when one more step would pass it.

t(key) returns the shopper-facing text for these keys: add_to_cart, added, sold_out, remove, subtotal, checkout, empty, quantity, increase (label of the plus button), decrease (label of the minus button), add_failed and close. When add() returns { ok: false } with a message, the message is shown as text; without one, or when add() throws, the shopper sees add_failed and the error goes to console.warn. The bundle is under 4 KB gzipped, uses no eval and no innerHTML.