Web components reference
Everything components.js 1.3.0 offers: five custom elements that put products, a cart and
checkout on any HTML page. They're built on storefront.js, which is bundled in.
<script
type="module"
src="https://developers.organizationdepot.com/storefront/v1/components.js"
></script>
<od-store token="odpk_YOUR_PUBLIC_TOKEN"></od-store>
<od-cart-button></od-cart-button>
<od-product handle="walnut-end-grain-board"></od-product>
- One ES module, about 19.3 kB gzipped, with no other dependencies. Loading it registers the elements. It exports nothing; the elements are the interface.
- Evergreen browsers from 2023 on: Chrome and Edge 111, Firefox 113, Safari 16.4.
- Every element renders in an open shadow root. Its events bubble and cross shadow roots, so you
can listen on
document. - Loading
components.jstwice (two tags, two versions) is harmless: the first copy's elements win.
How the elements work together
<od-store>is the connection. It makes the page's one client for its token (two<od-store>s with the same token share it), loads the shop and applies its branding.- Every other element finds its store by its
store="id"attribute, else the<od-store>it sits inside, else the first one on the page. Elements may come before the store, and the store may get itstokenlater: they wait, then load. An element that still finds no store after 3 seconds says so in the console. - One cart per storefront per browser. Every cart button and drawer on the page shows the same cart and stays in sync, with changes made in another tab and with a return from checkout through the Back button.
- Checkout is hosted. The drawer's Checkout sends the buyer to your checkout domain; after paying, the cart starts fresh.
- Text is text. Product, option, detail and personalization values are rendered as text, never as HTML.
- Strict Content-Security-Policy works. Styles are constructable stylesheets, which
style-srcdoesn't block; there are no inlinestyleattributes and noeval. See Security.
od-store
The page's connection to one storefront. It renders nothing itself; its children render as usual.
| Attribute | Default | What |
|---|---|---|
token |
required | Your token, odpk_… or odpv_… (preview) |
api-url |
https://api.organizationdepot.com/api |
The API's address. Local backend: http://localhost:12001/api |
api-version |
2026-12 |
The Storefront API version |
no-branding |
off | Don't apply the shop's branding; style everything yourself |
| Property | What |
|---|---|
client |
The shared storefront.js client (shop, products, cart, on); null without a token |
shop |
The shop once loaded, else null |
| Event | detail |
When |
|---|---|---|
od:load |
{ shop } |
The shop loaded and its branding was applied |
od:error |
{ error } |
The shop couldn't load (a StorefrontError). A refused token is also explained in the console |
const store = document.querySelector("od-store");
const cart = await store.client.cart.get(); // the same cart the elements show
od-product
A whole product block: photos that follow the photo option (choose Maple, see the maple board), the
title and price for the chosen variant, options as buttons (size groups such as Youth and Adult
labelled), "Sold out", the description, personalization fields, quantity, Add to cart, and the
details (properties) as a list.
| Attribute | Default | What |
|---|---|---|
handle |
required | The product's handle (its slug). Changing it loads that product |
heading-level |
2 |
The title's heading level, 1 to 6 (use 1 on a product page); Details is one lower |
store |
The id of the <od-store> to use, when the page has several |
| Property | What |
|---|---|
product |
The loaded Product, or null (loading, not found, failed) |
variant |
The selected Variant; undefined when that combination isn't sold |
selectedOptions |
The buyer's choice, { [optionName]: value } |
handle |
Reflects the attribute |
| Event | detail |
When |
|---|---|---|
od:load |
{ product } |
The product loaded |
od:variant-change |
{ product, variant, selectedOptions } |
The buyer's choice names a different variant |
od:added |
{ cart, product, variant, quantity, customizations } |
Add to cart succeeded |
od:error |
{ error } |
Loading or adding failed (the block shows its own message too) |
How it behaves:
- Personalization fields come from the product's form: text (with a character counter when it
has a limit), number, choice and checkbox. Required answers and limits are checked before
sending; the server checks again, and its
CUSTOMIZATION_INVALIDmessage appears on the field it names. After a successful add the fields clear, so the next item can carry a different name. - Add to cart is disabled for a sold-out variant, a combination that isn't sold, and while the
shop's sale is closed (
shop.sale.availableForSaleisfalse). A closed sale comes first, and the button says why, byshop.sale.reason: "Ordering opens soon" (NOT_OPEN_YET), "Ordering paused" (PAUSED), "Sale ended" (ENDED), else "Not on sale right now". With a preview token on<od-store>(shop.preview), the sale doesn't disable it: preview carts work whatever the sale says and pay only in test mode. It reads "Adding…" and ignores clicks while its request runs. The quantity is capped at the product's purchase limit and the stock left. - "Sold out" (the badge and the button) comes from stock only: a variant's or product's
availableForSale. A closed sale never marks products sold out. - Errors read as sentences: "Too many requests right now. Try again in 20 seconds."
(
RATE_LIMITED), the server's message forOUT_OF_STOCK,PURCHASE_LIMITandSALE_CLOSED, "This product isn't available." for an unknown handle, and a Try again button when loading fails.
| Slot | Default |
|---|---|
add-label |
"Add to cart" |
sold-out-label |
"Sold out" (the button) |
personalization-label |
"Personalize it" |
details-label |
"Details" |
not-found |
"This product isn't available." |
| Part | What |
|---|---|
root, gallery, info |
The layout: gallery beside info when the element is 44rem wide or more, stacked below |
media, image |
The main photo |
thumbnails, thumbnail (+ selected) |
The photo picker (two or more photos) |
title, price-row, price, description |
|
badge (+ sold-out) |
The "Sold out" badge |
options, option, option-label |
Each option (a fieldset) and its legend |
option-group, option-group-label |
A cluster of values and its label ("Youth") |
option-value (+ selected, unavailable) |
A value's button (a photo tile when every value has a photo) |
personalization, personalization-label |
The personalization fieldset |
field, field-label, input (+ invalid), field-help, field-count, field-error |
One field |
buy, quantity, quantity-button, quantity-input |
The quantity stepper |
add-button (+ busy) |
Add to cart |
message, added, view-cart-button, add-error |
After adding: the confirmation (with View cart) or the error |
details, details-label, detail, detail-label, detail-value |
The details list |
placeholder, not-found, error, retry-button |
Loading, an unknown handle, a failed load |
od-product-list
A grid of product cards: cover photo, title, price ("From $34.00" when variants differ) and "Sold out", a page at a time with Load more.
| Attribute | Default | What |
|---|---|---|
href |
none | Each card's link, with {handle} (and {id}) filled in, URL-encoded: /products/{handle}. Without it, cards aren't links |
tag |
all | Only products with this tag (tags are collections) |
first |
12 |
Page size, 1 to 100 |
store |
The id of the <od-store> to use |
| Property | What |
|---|---|
products |
The products shown so far |
| Event | detail |
When |
|---|---|---|
od:load |
{ products, pageInfo } |
After each page loads |
od:error |
{ error } |
A page couldn't load |
Load more moves focus to the first new card and announces how many were added.
| Slot | Default |
|---|---|
load-more-label |
"Load more" |
empty |
"No products to show yet." |
Parts: grid, card (+ sold-out; an a with href, else a div), card-media,
card-image, card-badge, card-title, card-price, load-more (+ busy), empty,
placeholder, error, retry-button.
od-cart-button
A button with the cart's item count that opens the page's <od-cart>. A page without one gets one
added to <body>. The count is part of the button's name ("Cart, 3 items") and is announced when it
changes; the button sets aria-expanded while the drawer is open.
| Attribute | What |
|---|---|
store |
The id of the <od-store> to use |
| Property | What |
|---|---|
count |
The number of items (0 until it has loaded) |
The default slot is the label ("Cart"): <od-cart-button>Basket</od-cart-button>.
Parts: button, icon, label, count (the badge, hidden at 0).
od-cart
The cart drawer, a modal dialog: focus moves in and stays in, Escape or the backdrop closes it,
focus returns to what opened it, and the page behind doesn't scroll. Each line has its photo,
title, options, personalization, a quantity stepper, Remove and its price; then the subtotal
and Checkout. It follows every change to the cart, from any element or from your own code. A
preview cart (made with an odpv_… token) shows "Preview cart:
checkout takes test cards only" above Checkout; checkout then takes Stripe's test cards and
places no order.
| Attribute | What |
|---|---|
open |
Open or close it (reflects) |
open-on-add |
Open whenever something is added (od:added) |
product-href |
Link each line's title to its product: /products/{handle} ({id} works too) |
store |
The id of the <od-store> to use |
| Method / property | What |
|---|---|
show() |
Open it |
close() |
Close it |
toggle() |
Open or close it |
open |
Whether it's open (settable) |
| Event | detail |
When |
|---|---|---|
od:cart-open |
{ cart } |
Opened |
od:cart-close |
{ cart } |
Closed |
od:checkout |
{ cart } |
Checkout was pressed, before leaving the page. Cancelable: preventDefault() stays here |
od:error |
{ error } |
An edit, loading the cart, or checkout failed (shown in the drawer too) |
// Hold checkout until the buyer has accepted your terms, say:
document.addEventListener("od:checkout", (event) => {
if (!buyerAgreedToTerms()) event.preventDefault();
});
| Slot | Default |
|---|---|
title |
"Your cart" |
empty |
"Your cart is empty." |
note |
"Tax and fees are calculated at checkout." |
checkout-label |
"Checkout" |
Parts: dialog, backdrop, panel, header, title, close-button, body, empty,
placeholder, lines, line (+ busy), line-media, line-image, line-info, line-title,
line-options, line-personalization, quantity, quantity-button, quantity-value,
remove-button, line-price, line-unit-price, footer, subtotal, subtotal-label,
subtotal-amount, note, preview-note, message, checkout-button (+ busy), retry-button.
Styling
How theming works for every Organization Depot element is in Embedding and theming. These are the Storefront elements' properties, parts and slots.
CSS custom properties
<od-store> sets your shop's branding on the page as the first seven properties below, with no
specificity: any rule of yours wins, on :root or on an element.
| Property | From branding | Default | Used for |
|---|---|---|---|
--od-color-brand |
color.brand |
#111827 |
Buttons, selected options, accents |
--od-color-surface |
color.surface |
#fff |
The drawer's panel, badges |
--od-color-text |
color.text |
the page's text colour (the drawer: #111827) |
Text |
--od-font-heading |
font.heading |
--od-font-body |
Titles |
--od-font-body |
font.body |
the system font | Everything else |
--od-radius-base |
radius.base |
10px |
Corners |
--od-density |
density |
1 (compact 0.85, spacious 1.2) |
Spacing |
--od-color-on-brand |
black or white, by contrast | Text on brand-coloured buttons | |
--od-color-focus |
the brand colour, darkened | Focus rings | |
--od-color-danger |
#b42318 |
Errors | |
--od-color-backdrop |
rgb(17 17 17 / .45) |
Behind the drawer | |
--od-drawer-width |
26rem (full width on phones) |
The drawer | |
--od-card-min-width |
10.5rem |
The narrowest a product card gets |
Tinted surfaces (option buttons, inputs, steppers) are your text colour at low opacity, so the
elements fit light and dark pages alike. Text on a brand-coloured button is worked out from the
brand colour in effect, yours or the shop's; set --od-color-on-brand to choose it yourself. A
font.* of "Inter" applies when your page (or the device) has Inter: components.js loads no
fonts.
:root {
--od-color-brand: #7c4a1e;
--od-radius-base: 4px;
}
od-cart {
--od-drawer-width: 30rem;
}
Parts
Every meaningful piece has a part name, listed per element above. A state is an extra part name, matched together with the base one:
od-product::part(add-button) {
min-height: 3.5rem;
}
od-product::part(option-value selected) {
outline: 2px solid currentColor;
}
od-product-list::part(card sold-out) {
opacity: 0.6;
}
od-cart::part(panel) {
border-radius: 16px 0 0 16px;
}
Slots
Labels can be replaced with your own markup, by slot name:
<od-product handle="walnut-end-grain-board"><span slot="add-label">Add to basket</span></od-product>
<od-cart><span slot="checkout-label">Pay securely</span></od-cart>
<od-cart-button>Basket</od-cart-button>
Planned: markup templates
Replacing an element's markup with your own, from a <template> child, is planned and not yet in
any release. For now, customize with the custom properties, parts and slots above, or build your
own markup on storefront.js (the store's client gives you the same cart).
TypeScript
The elements and their events are typed for document.querySelector("od-product") and
addEventListener("od:added", …) in the source, but no declarations ship for components.js in
1.0. Treat the elements as HTMLElement and the events as CustomEvent, with the detail shapes
above and the types from storefront.js.