Menu
Representative buying task

Try one product. See every honest answer.

Check nearby branches for this product. Then compare the eight widgets and try the playground.

Where to buy

Purus Corner Linear Drain near Stockholm

Built widget bundle

Loading the live embed from cdn.test.stockisto.com…

This example shows nearby branches for the sample product.

Your front door

Bring the last click home

A shopper looking for your product should arrive somewhere you control. That is your own site: the where-to-buy page or the product pages you already run. Stockisto appears there under your brand.

yourbrand.example/where-to-buy
Behind every embed

One bundle, one renderer, one contract.

Every card above is the same script reading different attributes. What a row shows is decided by the retailer's own data; what the embed looks like and how it behaves is decided by the tag.

The retailer ladder

Five rungs, from a public listing to reserve and collect.

A row shows only what its retailer has earned. Each rung adds a stronger action, and wanting the stronger action is what keeps a retailer's data current.

  1. 01ListedName, address and published opening hours from public data. An in-stock report nobody vouched for is labelled as a report and wears the neutral listed colour, never the confident green.Public data
  2. 02ReachableA Visit link where the shop published a website, a Call link where its number is dialable, and Directions where the listing carries real coordinates. Each control appears only when its own field exists.Phone or website
  3. 03Confirmed stockA shop that feeds its own stock gets the confirmed signal and a freshness line dated from its last report, and those rows rank above everything unproven.A stock feed
  4. 04PriceOn the retailer's own product pages, a row carries their price. The server emits it only where the shop publishes one and the figure is under a month old, so a price on a row is never a stale one.A published price
  5. 05ReserveThe reserve-and-collect placement turns confirmed rows into a reservation the shopper completes without leaving the page. A row reads Reserved only after the reservation request has actually succeeded.Reservations on

Confirmed stock and distance decide the order; placement is never sold. Each Visit, Call and Directions press is recorded as one click-out, and only once your consent tool has switched analytics on: before that the embed sends nothing at all.

Themes

A theme is attributes, not a fork

Five named presets, plus the tokens below, all read off the script tag. There is one bundle and one renderer behind every look on this page, so a theme costs you no separate build and no separate version to keep up to date.

  • heritageThe default. Warm paper, forest accent, soft elevation. What every embed on this page is wearing.
  • lightNeutral grey and white, for host pages the warm paper clashes with.
  • darkWarm dark surfaces for dark host pages, with the glow dropped for a flat edge.
  • minimalChrome-less white: tight corners, hairline borders, no elevation. Text colour is inherited from your page, which is the one thing this preset hands back to you.
  • denseHeritage colours at compact type and spacing, for sidebar and footer placements.
The tokens it reads
data-theme-preset="dark"
One of the five above. Anything else falls back to Heritage.
data-theme="#C06B4A"
Your accent, as a 3- or 6-digit hex.
data-radius-scale="sm"
Corner scale: sm, md or lg.
data-density="compact"
Row rhythm: cozy or compact.
data-shadow="flat"
Elevation: soft, flat or none.
data-font="serif"
Type: grotesk, sans, serif, mono or system. Named, never uploaded, so the embed uses a stack the page already has.
Who edits itWhoever owns the tag. The builder in your dashboard previews these tokens and writes them into the snippet you paste, and after that the tag is the theme: change a placement by editing its attributes, not by redeploying the embed.
Contrast is checked at renderAn accent that does not clear 4.5:1 against the surface it sits on is refused, and the widget paints its own accent instead. It happens on every page view rather than once at save time, so a brand colour cannot quietly make a row unreadable. Stock and freshness colours are exempt from theming entirely: no accent or preset can recolour a stock claim.
Every embed carries its ownA brand page's embed and a shop's embed on their own site are separate tags with separate attributes over the same data, so a shop site stays a shop site.
Three layers of configuration

Written once into the tag. Read on every render.

  1. 01Your dashboard writes the tagThe widget builder previews a configuration and emits the script tag for it, and saves the ones you want to come back to. No configuration is fetched from your account at render time: what the tag carries is what the embed does, which is also why a tag keeps working on a page nobody has touched in a year.
  2. 02The tag configures the placementdata-widget-type picks the widget, data-view picks a Where to buy view, data-map-height sizes a framed locator, and the theme tokens set the rest. Attributes the parser does not recognise are ignored rather than guessed at.
  3. 03The row earns the restWhat any single row shows is decided by that retailer's own data, not by the tag: the ladder above is the whole of it. You cannot switch on a price for a shop that publishes none, and the embed will not invent one.
Prepared for implementation

What your developers are agreeing to

Everything above renders from the one script tag at the top of this page. These are the behaviours behind it, and each one is either enforced by a build gate or covered by a test in this repository.

One tag
A single async script per placement, and no build step. The widget renders inside its own shadow root where the browser has one, so the host page's CSS and the embed's never reach each other. The corner button and the reserve toast mount at body level.
Weight is a gate, not a promise
The published bundle is measured on every build and the build fails if it grows past its recorded ceiling. The number lives in the widget's own budget file rather than in this sentence, so this page cannot drift from it.
Nothing loads early
The tag is async and the data request waits until the embed nears the viewport. The framed map and installer legs are iframes, so their tiles are the framed page's cost and not your product page's.
Privacy defaults
No cookies. Analytics stays off until data-analytics-consent is true, and until it is the embed sends no event at all. The one thing it writes is a single sessionStorage key that stops a back-navigation counting the same result twice.
Keyboard and contrast
Rows, toggles and expanders are real buttons, so Enter, Space and the focus ring come from the platform. Dialogs take focus, trap Tab, close on Escape and hand focus back. Every preset's text pairs are asserted against WCAG AA in the widget's test suite; the one preset that inherits your page's text colour records that instead of claiming a ratio it cannot know.
When things fail
A blocked or failed script renders nothing and leaves your page unharmed. A slow API shows a skeleton; a dead one says it could not load and offers a retry, never a false empty. A misconfigured tag logs the missing attribute and stamps a marker your monitoring can find, and still paints nothing for shoppers.
Playground

Make it yours, then copy the tag

Pick a mode, a theme preset, your accent color and layout tokens. The live preview re-renders and the snippet regenerates as you go. Every attribute you see is one the shipped embed actually parses.

widget default (#1E5E45)

Off-list values fail closed inside the widget: an unknown preset or a non-hex color falls back to the Heritage default. A typo can never break your product page.

Loading the live embed from cdn.test.stockisto.com…

<script
  src="https://cdn.test.stockisto.com/widget.js"
  data-stockisto-widget="true"
  data-stockisto-slug="demo-purus"
  data-api-base="https://api.test.stockisto.com"
  data-widget-type="where-to-buy"
  data-lat="59.3293"
  data-lng="18.0686"
  data-radius="50"
  data-supplier-name="Purus"
  data-jsonld="off"
  data-analytics-consent="false"
  async
></script>

data-analytics-consent stays "false" in every generated snippet. Set it to "true" only after your consent tool (CMP) records the shopper's consent.

Hostile hosts

Built for pages that fight back

Real host pages ship aggressive CSS, strict security policies and broken environments. Three of those conditions can run live right here; the rest run as automated tests against synthetic hostile hosts in CI and against the deployed environment. Nothing in this section is a claim without a test behind it.

Hostile :empty CSS

This container's stylesheet declares div:empty { display:none !important }, a rule that silently kills naive embeds before they load. The widget's mount ships a hidden sentinel node, so it never matches :empty and renders anyway.

Loading the live embed from cdn.test.stockisto.com…

Misconfigured on purpose

This embed ships with NO data source (no brand slug, no supplier id, no retailer). Watch what the widget does about it and what it refuses to do.

Loading the live embed from cdn.test.stockisto.com…

Open your browser console: the widget logged the exact missing attribute. That console error comes from the widget by design. It is the one honest side effect of a misconfigured embed.

320px column

The same live embed is constrained to a 320px column, the narrowest common sidebar/mobile slot. No horizontal overflow, no clipped actions.

Loading the live embed from cdn.test.stockisto.com…

The tested-conditions matrix

Each row maps to automated coverage that exists in our repository today: unit guards on every push, and browser tests against synthetic hostile host pages. This table reports that coverage; it doesn't extrapolate beyond it.

ConditionWhat the embed guaranteesEvidence
Strict CSP host: default-src 'none'; only the CDN script and API connection allowedBoots and paints with no inline-script or eval dependency; zero page errors.Deployed e2e (widget-hostile-host-matrix) + real-browser matrix (tests/e2e/widget-host)
Plain HTTP page (insecure context, no crypto.randomUUID)Boots without page errors; widget ids fall back to a guarded UUID path.Deployed e2e (widget-hostile-host-matrix) + every-push unit boot guard (packages/widget)
Crypto-less host: window.crypto missing or randomUUID strippedBoots with a spec-correct v4 widget id; no thrown TypeError kills the embed.Every-push unit boot guard (packages/widget hostile-host-boot) + deployed e2e
Hostile host CSS: div:empty { display:none } on the whole pageThe mount keeps a layout box (hidden sentinel, never :empty), lazy load still fires, the widget paints.Deployed e2e (widget-host-css-deadlock, widget-hostile-host-matrix) + the live demo above
Slow or failing API: 503, network reject, 8s stall, or the whole origin blockedA distinct, honest ‘couldn't load, try again’ state with a manual retry. Never a false ‘no retailers’, never a silent blank box.Every-push unit coverage (packages/widget render-failure)
Third-party host origin: the embed runs on a non-Stockisto domainAll data calls are anonymous, CORS-open API requests; no cookies or same-origin assumptions.Every deployed widget spec runs on synthetic third-party origins (*.stockisto-e2e.example)
Missing data source: no brand slug, supplier id, or retailer setRefuses to boot: console error naming the attribute + a data-stockisto-error stamp for monitoring. Nothing broken is painted for shoppers.Every-push unit boot test (packages/widget missing-data-source-boot) + the live demo above
Going live

No developer sprint required.

01

Paste one tag

A single script tag per page. No build step, no package to install, no iframe to size by hand.

02

It fits the page it's on

Width, row height and presentation are attributes. Inline and tab views stay in the page, so search engines can read them.

03

It only says what it can prove

Stock we can't verify is labelled, never promoted. When there's nothing to show, the embed says so instead of showing a zero.

04

Retailers climb a ladder

Stock feed, prices, buy link, cart handoff, reserve. Each rung a retailer earns puts a stronger row in front of shoppers, so keeping the data honest pays.

See it on your own catalogue.

Import your retailer network, paste one tag, and watch which stores your shoppers choose.

cebf50c · 2026-10-05 22:57