Storefront widgets
The widgets in @cascade-commerce/widgets render ratings, reviews, wishlists and the
loyalty program on a merchant's storefront. Drop one script tag on the page and mark up the
elements you want filled in:
The value of each attribute is the product slug. On Shopify that is the product handle:
The review and rating widgets read public data, so the publishable key is all they need. The wishlist widgets belong to one shopper, so they do their real work once the storefront says who that shopper is: see Identifying the customer and Wishlists. For a guest they keep the shopper's saves in the browser until they sign in. The loyalty widget works both ways too: it shows the program to anyone, and a shopper's own balance once the storefront signs them in.
Every endpoint these widgets call is documented in the Widget API reference.
If your storefront is a React app, the same widgets are React components you can import and take apart: see React widgets for the package, the parts each widget exposes and how to lay them out yourself.
Appearance
The widgets are built to disappear into your storefront. Out of the box they take their font and their text color from whatever they sit in, their background is transparent, and the one filled control uses your page's own text-on-page pair. On most stores that is already close enough that there is nothing to do.
When it is not, Settings, Widget appearance is where you change it. It is a form beside a live preview of the real widgets against sample data, so you can see a change before anyone else does.
Per site
Appearance is set per site, so a second storefront can look like itself. Pick the site at the top of the page. If you want two sites to match, use Copy from another site: it copies the colors and the custom CSS across in one go. It is a copy rather than a link, so the two drift apart the moment you edit either of them again.
The Stylesheet URL shown under the site picker is the stylesheet your widgets load. The embed script requests it for you, so you only need it if you would rather link it yourself; see How the theme reaches your storefront.
The properties you can set
Everything the widgets draw resolves through this set of CSS custom properties. The editor sets them for you, and you can also set any of them yourself from your own stylesheet.
A few notes on filling them in. The font is a CSS font stack, and left as inherit the
widgets use whatever font the page around them uses; the same goes for the text color. A
transparent surface lets your own page color show through, which is usually what you
want. The radius is a CSS length such as 8px, and cards, panels and controls all use it.
Two more are not in the editor, because they are derived rather than chosen. Set them from custom CSS if you need to:
--cascade-control-radiusrounds buttons and inputs on their own, so you can have pill buttons without rounding the cards to match. It follows--cascade-radiusby default.--cascade-floating-surfaceis the ground under the two things that float over your page, the loyalty panel and the wishlist dialog. It has to be opaque, so it does not follow--cascade-surface.
Check your accent against your text on accent. They are the one pairing in the set that carries text on a filled ground, and a light accent with light text on it is unreadable.
How the theme reaches your storefront
The embed script requests a stylesheet before it renders anything, so your storefront never flashes the default colors and then swaps:
It uses the site from data-cascade-site on your script tag. With no site named it serves
your oldest site's appearance, which is the right answer if you only have one.
That URL is cached for five minutes. If you would rather it were cached indefinitely, the
appearance page shows a versioned URL: put the version on your script tag as
data-cascade-theme-version and the file becomes immutable until you next change something.
You can also link the versioned URL from your own <head> instead, which is worth doing if
you load the embed script late.
Custom CSS
Anything the properties above cannot express goes in the custom CSS box. The class names below are a supported API: we will not rename them without saying so. Anything else in the markup is internal and can change, so do not select on it.
A modifier is always a -- suffix on the block it belongs to (.cascade-button--primary,
.cascade-loyalty-tier--current), so a state is easy to spot in a selector.
Your CSS is checked before it is saved, and what is stored is the checked version. The rules:
- Everything is scoped to the widget root. A rule for
bodyor:rootis kept but can never match, because it is rewritten to sit under.cascade. Nothing you write here can restyle the rest of your page, which is the point. - Only
@media,@supportsand@containersurvive.@import,@font-faceand the rest are removed. Load a webfont from your own theme and point--cascade-fontat it. url()may only carry adata:URI. The stylesheet is served from Cascade's domain, so it does not fetch from anywhere else. A declaration with any otherurl()is dropped.- Nested rules are dropped. Write each selector out in full.
- 20,000 characters is the limit. Past that the save is refused rather than truncated.
Anything removed is reported back to you, so you are never left wondering why a rule did nothing.
Writing CSS from a description
Describe what you want in the box above the editor and Write it for me turns it into CSS. It knows the properties and the class names above, and it rewrites your custom CSS in full rather than appending to it.
Nothing is saved until you say so. You get the CSS, a sentence on what it changes, and the preview showing it, and then you choose Accept or Discard. It can only do what you could do by hand: the result goes through the same checks as anything you type, so it cannot reach outside your widgets either.
Ask for a look, not for a layout. Colors, spacing, type, borders and corners are what this is for. It cannot reorder sections, hide the rating breakdown, change the star shape or remove the verified-buyer badge, because those are not styling.
It is limited to 20 requests per organization, refilling at one a minute.
Putting an earlier version back
Every save of the custom CSS is kept, including the ones a description wrote, and the last 50 are listed under Earlier versions with who saved them and what they were asked for. Restore puts one back. A restore is itself saved as a version, so undoing an undo costs nothing.
The colors are not versioned. They are one value each and the editor shows what they are, so Reset beside a property is the whole story there.
Merchant replies
Open a review in the dashboard and use the reply composer to answer it. The reply shows up under the review body in the reviews widget, headed "Response from" the name you replied as.
A reply is published separately from the review it answers, so you can draft one now and put it live later. Only published replies reach the widget: a draft is never sent to the storefront.
Review summaries
Once a product has enough published reviews, the reviews widget shows a short summary of what reviewers say above the list, headed "What reviewers say" and marked as generated. Under it are up to five themes, each with roughly how many reviews mention it and whether reviewers raise it approvingly, as a complaint, or both.
The summary is generated from the published reviews only, in whichever language most of them are written in, and it says so: shoppers are told it is not written by you or by a reviewer. It is kept out of the structured data on the page, because it is not a review anyone wrote.
It refreshes on its own as reviews come in, at most once a day per product, and it is never generated from fewer than five published reviews. On the product page in the dashboard you can regenerate it, hide it from your storefront, or go back to the previous version after a regeneration. To turn the whole feature off, use "Summarize what reviewers say" on the Reviews settings page.
Writing a review
The reviews widget carries a "Write a review" button above the list. It opens a form in place, so nothing floats over the page and the shopper keeps their place on it. The form asks for a rating, a title, the review, a name and an email address, and takes up to five photos or videos. If you have set up review questions, it asks those too, between the review and the name.
What happens next is your moderation setting, not the widget's: a review submitted here arrives unpublished and waits in Reviews, unless auto-publishing is on and the rating clears your threshold. The shopper is told which happened, so nobody is left expecting their review to appear straight away. A review that is waiting does not show up in the list or in the structured data on the page.
Two more things about these reviews:
- They are never verified buyers. That badge is for reviews collected from a request sent after a real order.
- The email address is not published. It identifies the reviewer for moderation and keeps one person to one review per product. Reviewers appear as a first name and a last initial, the same as every other review.
When the storefront has signed the shopper in (see Identifying the customer), their address is filled in, locked, and used instead of anything typed into the form.
To collect reviews only from the requests you send, turn the form off:
On Shopify it is the "Let shoppers write a review here" checkbox on the Product reviews block.
Review questions
Beside the stars you can ask reviewers a few short questions of your own: how something fits, how the quality feels, whether they would buy it again. You set them up on the Reviews settings page, and each one is either a scale (a row of numbered steps with a word at each end, and optionally one in the middle) or a multiple choice (a set of options, one or several of which can be picked).
The reviews widget uses them in three places.
- In the write form. Every question scoped to the product is asked under the review box, in the order you set. A scale is a row of steps with its end labels underneath; a multiple choice is a set of radio buttons, or checkboxes when several answers are allowed. A question you marked as required carries a "Required" note and the form will not send until it is answered.
- Above the list. Under the star breakdown, each question you chose to show gets a line of its own: a scale as a track with a marker at the average and a reading like "True to size, 3.0 of 5 on average", a multiple choice as one bar per option with the share that picked it. Only published reviews count, and only questions and options you chose to show appear. A question nobody has answered yet is left out rather than drawn empty. Because a shopper can pick several options on some questions, those shares can add up to more than 100%.
- On each review card. Under the review body, that reviewer's own answers read as short lines: "How is the fit? True to size", "Would you buy it again? Yes". A step you did not name reads as its position, like "4 of 5".
Archiving a question keeps the answers it already has, so the block above the list can still name it. Hiding a question, or one of its options, takes it off the storefront everywhere while the answers stay in your reports.
The same questions are asked on the emailed review form, so the two collect the same thing. See Collecting reviews.
To keep the answers on the review cards but drop the block above the list:
Sorting and filtering
The reviews widget carries a sort control above the list. Shoppers can order reviews by newest (the default), highest rating, lowest rating, or most helpful. Ties in the helpful order fall back to newest, so the list is stable from one page to the next.
The bars in the rating breakdown are buttons. Clicking one narrows the list to that star rating, and clicking it again (or "Show all") clears it. The counts in the breakdown always describe the whole product, never the filtered subset, so the shape of the ratings stays readable while a filter is on.
Helpful votes
Each review carries "Helpful" and "Not helpful" buttons with a running count. Voting takes no sign-in: the widget keeps a random token in the browser's local storage and sends that with the vote, so a visitor can change their mind and the earlier vote is replaced rather than counted twice. The buttons show which way this visitor voted, and that survives a reload as long as local storage is available.
Votes are anonymous and are not tied to a customer record. Only published reviews accept them, and a review that is unpublished later keeps its votes without showing them anywhere.
Reviews carousel
The carousel is a sliding row of review cards, for the places a full review list is too much: a home page, a landing page, the space under a product's description. Each card carries the stars, the review title and an excerpt of the body, the reviewer's name and the verified-buyer badge, and a thumbnail if the review came with a photo.
Store-wide is the default, and in that mode each card names the product the review is about and links to it when Cascade knows the product's URL. Put a product slug in the attribute to narrow it to one product, and the product line goes away because every card is the same product.
The cards are ordered by rating, highest first. They slide with the arrow buttons on either side of the heading, by dragging on a touch screen, or with the arrow keys once the row has focus. Nothing advances on its own, because a card that moves while someone is reading it is worse than one they have to ask for.
The carousel does not emit structured data. Your product pages already do that through the reviews or rating widget, and a second copy of the same reviews on a home page is not something to publish twice.
On Shopify it is the Reviews carousel block, with the same three settings plus an "Only this product" checkbox for product templates.
Media grid
The media grid is a wall of the photos and videos shoppers attached to their reviews, one tile per file. It is the shoppable UGC surface: a home page section, a landing page, or a band under a product's description. The tiles carry no words, and clicking one opens the photo full size with the review behind it: the stars, the title, an excerpt, the reviewer's name and their verified-buyer badge.
Store-wide is the default, and in that mode the lightbox carries a "View product" link to the product the review is about, when Cascade knows the product's URL. Put a product slug in the attribute to narrow it to one product, and the link goes away because every photo is the same product.
Tiles are ordered newest review first, and a review's own photos stay together, so a page can come back a little over the number you asked for. When there is more, a "Load more photos" button appends the next page. Video tiles show a frame from the start of the clip under a play glyph, and play with controls in the lightbox. Inside the lightbox, the arrow keys walk the whole grid and Escape closes it.
The grid does not emit structured data. Your product pages already publish these reviews through the reviews or rating widget.
On Shopify it is the Review photos and videos block, with the same three settings plus an "Only this product" checkbox for product templates.
Structured data
Both review widgets can emit schema.org JSON-LD, which is what
produces the star snippets in Google search results. The markup is a Product node
carrying an AggregateRating, and, from the reviews widget, the individual Review
records as well:
When the product has published questions and answers, the reviews widget
emits a second node beside it: a QAPage whose mainEntity is one Question per published
question, with your store's answer as the acceptedAnswer and any shopper answers as
suggestedAnswer. A question with no published answer is left out, because search engines
reject a question that has none.
The generated instant answer is deliberately absent from both nodes. It is private to the shopper who asked, and nobody wrote it.
Defaults
Override either with data-cascade-structured-data:
In React, the same controls are the structuredData and productUrl props on
<ProductRatingWidget> and <ProductReviewsWidget>. Both keep emitting their markup when
you compose the widget from parts.
Where to put the widget
Structured data describes the page it sits on, so where the widget goes decides which page Google attaches the stars to.
- Put the marked-up widget on the product's own page. A review snippet is only eligible when the page's main content is the thing being reviewed. Markup on a home page or a blog post will be ignored, or flagged in Search Console.
- One marked-up widget per page. If a product page has both widgets, leave the
rating widget's markup off (the default) and let the reviews widget emit it. Two
Productnodes with different rating values on one page is worse than one. - Leave it off in collection and search grids. Those pages list many products and none of them is the page's main content.
- Check what your theme already emits. Most Shopify themes emit their own
Productnode. That is fine, and Google reads both, but if the theme already publishes anaggregateRatingfrom another review app, turn Cascade's off rather than shipping two contradictory ratings.
The widget takes the page's <link rel="canonical"> for the product URL, and falls
back to the current path without its query string or fragment. Set
data-cascade-product-url when neither is right, for example when the widget sits in
a modal or a quick-view drawer served from a different path.
What gets emitted
- Nothing at all until the product has at least one published review. An
AggregateRatingwith a count of zero is invalid, so no script tag is written. - Only published reviews, which is what the widget itself shows.
- The reviewer's first name and last initial, the same name shown on the page. A review from an unnamed customer is attributed to "Anonymous".
- Reviews loaded through "Load more" are added to the markup as they appear, so what Google sees keeps matching what a visitor sees.
Verifying it
Run the product page through Google's Rich Results Test. Use the URL tab, not the code tab: the widget writes its JSON-LD after it fetches its data, so the markup only exists once the page's JavaScript has run, and the URL test renders the page the way Googlebot does. A healthy product page reports valid "Product snippets" and "Review snippets" items.
Identifying the customer
Anything that belongs to one shopper (their wishlists, their loyalty balance) needs the storefront to say who is signed in. Cascade will not take the browser's word for it, so the storefront proves the email address with a signature made on your server.
Add three more attributes to the same script tag:
All three go together. A page that carries some of them but not all is refused by the API, which is a wiring mistake worth surfacing rather than quietly serving the guest view. For a signed-out shopper, leave all three off: the review and rating widgets carry on working, and the wishlist widgets switch to browser-kept guest lists that are claimed on sign-in. See Before signing in.
The signing recipe
- Take the customer's email address exactly as you send it in
data-cascade-customer-email, including its case. - Take the current Unix time in seconds, as a string.
- Concatenate the two, in that order, with nothing between them.
- HMAC the result with SHA-256, keyed with your widget signing secret, and encode the digest as lowercase hexadecimal.
A signature is good for 24 hours, so a cached page keeps working for the rest of the day. Sign on every page render anyway: it costs nothing and the shopper never meets an expiry.
Your widget signing secret lives in Cascade under Settings > API keys, in the widget
signing secret card below your keys. There is one per organization, shared by every site
and every widget. Reveal it, copy it, and set it on your server as CASCADE_SIGNING_SECRET
or whatever your app calls it.
The publishable key is safe in page source. The signing secret is not: anyone holding it can sign as any of your customers, so keep it on your server and out of theme files, JavaScript bundles and version control.
Rotating the secret
The same card has a Rotate secret button, for when the value has leaked or has been sitting in one place too long. Rotation takes effect immediately and there is no grace period: every storefront still signing with the old value starts failing, and authenticated widgets stop loading for your shoppers until the new secret is deployed everywhere you sign. Have the replacement ready to ship before you rotate.
Rotations are recorded in the audit trail, so a widget that suddenly stops authenticating can be traced back to one.
Signing it on your server
All three produce the same signature. With the secret wsk_demo_signing_secret, the email
[email protected] and the timestamp 1735689600, every one of them returns
48eb876e883be4490d05d06ea8b9fe870b481c1098cc7ef88553998690a1dd31. Use that as a test
vector when you wire yours up.
On Shopify
There is nothing to do. Turn on the Cascade embed in your theme and the app handles this for you: it asks Cascade for the signature of whoever is signed in, on your store's own domain, and hands it to the widgets. Your signing secret stays on Cascade's servers.
Do not sign this in Liquid. Liquid has an hmac_sha256 filter, but the secret would have
to live in a theme setting or a shop metafield, and both are readable by anyone who can
view your storefront or edit your theme.
When a signature is rejected
Every widget call that carries a customer answers 401 with one of these messages:
Wishlists
Three embeds cover the wishlist feature: a save button for a product page, a full list manager for a customer account page, and the same manager in a modal.
All three need the customer attributes from Identifying the customer. See Wishlists for what each one does, how sharing and purchase tracking work, and the Shopify markup.
Loyalty
Two embeds show the loyalty program, and they render the same panel. Use whichever suits the page:
The launcher sits in the bottom right by default. Move it with
data-cascade-loyalty-position="bottom-left".
Signed in and signed out
The loyalty widget renders for everybody. Without a customer it shows the catalog: the tiers, the ways to earn, the offers, the stamp cards and what points can be spent on. That is the marketing surface, and it is what convinces somebody to join.
Add the customer attributes from Identifying the customer and the same panel gains the member's own state: their balance, their tier and how far they are from the next one, their stamp cards, and the rewards they are holding with the codes to use them. Joining, claiming a reward and canceling one all need a signed-in shopper.
Unlike the wishlist widgets, a page missing the customer attributes is not an error here. It renders as a signed-out visitor. Send all three attributes or none: a page carrying some but not all of them is treated as signed out.
Script tag attributes
Three optional attributes on the script tag apply to both loyalty embeds:
Which sections appear
The panel shows five sections, always in this order: tiers, ways to earn, offers, stamp cards and rewards. Which of them appear is decided in the dashboard rather than in your markup:
- Ways to earn, offers and stamp cards follow the Sections switches on the program settings page.
- Tiers appears whenever the program has any.
- Rewards appears once the program's points are named. A program with no points has no balance and nothing to spend, so the section would be empty.
A section that is switched off is not merely hidden. Its rules are never sent to the browser at all, so switching one off is a real change rather than a cosmetic one.
The order ships with every response, so the panel never hardcodes it and a future rearrangement needs no change to your theme.
Visits
The widget reports a visit when it loads for a signed-in shopper, which is what makes the "Visits the store" trigger work. Visits are deduplicated per browser session, and the allowance you set on the rule (so many times per day, week or month) is enforced by Cascade against your organization's own time zone rather than by the browser.
Set data-cascade-loyalty-visits="false" on the script tag if you would rather report
visits yourself, or not at all. It applies to both embeds.
Points on the product page
A one-line badge for a product page: what buying this product is worth in points. It reads your program's purchase earning rules, so the number is the same one the shopper's balance will move by after checkout.
The price has to come from your storefront, because only your storefront knows what the
product costs and which variant is selected. The attribute is read once, when the badge
mounts, so a theme that swaps the price on a variant change should re-render the badge (or
use the React component, whose price prop it follows).
A signed-in member reads "Earn 250 points with this purchase." and gets their tier multiplier counted; everybody else reads "Join to earn 250 points with this purchase." If the program names its points something else, the badge uses that name.
The badge renders nothing rather than an error when there is nothing honest to say: no program, no live purchase rule, a paused program, a program with no points, or a price too small to earn anything. Rules restricted to particular products are left out too, because a price on its own cannot say whether this product qualifies.
When the page also has the inline loyalty embed on it, the badge is a link to it. The floating launcher keeps its open state to itself, so on a page with only a launcher the badge is a plain sentence.
On Shopify it is the Points on product page block.
Referral links
A member's share link is your site's base URL with ?ref= and their code on it. The widget
reads ref from the landing URL, remembers it, and sends it with the join, which is what
turns the link into an attributed referral.
There is nothing to wire up for this beyond having the widget on the landing page. Note that attribution needs the friend to actually join, since a referral has to name a customer.
Links from loyalty emails
Cascade's loyalty emails link to your storefront, not to a page we host. Which page is up to you: the Sites card on the loyalty settings page takes a loyalty page URL per site, and leaving it empty points the links at the site's own base URL, where the launcher usually sits.
Every one of those links carries the fragment #cascade-rewards, and both embeds act on it:
- The launcher opens its panel on arrival, so the shopper sees the program rather than a pill in the corner.
- The inline embed scrolls itself into view.
There is nothing to configure for this. Have one of the embeds on whichever page you point the links at and it works.
Because the link lands on your storefront, a shopper who is signed in gets their own balance, tier and rewards: your theme signs the customer trio exactly as it does for any other page. A shopper who is not signed in gets the guest catalog and whatever sign-in your store already offers, which is a state they can get themselves out of.
What the widgets call
Every endpoint behind these embeds is documented in the Widget API reference. For the program itself, its rules, tiers and stamp cards, see Setting up loyalty.