UI SDK (window.HypeUI)
Updated Oct 1, 20268 min read
On this page (14)
window.HypeUI holds the controller classes that drive Hype's campaign elements. Every element on the page — the product banner, the gift banner, the progress bar and the rest — creates one controller, and the controller does the work: it finds the campaign that matches the element's identifier, computes what should be shown, and publishes that as a reactive config store. Construct a controller yourself when you want the same data for your own markup, or want to react when it changes.
Use the Storefront SDK (window.Hype) instead when you need campaigns, the cart or discount codes rather than one block's rendered state.
What loads it
The Hype: Discounts app embed assigns window.HypeUI after it imports hype-ui.js, at the same time as window.Hype and window.HypeUIBlocksSchema. Turn the embed on at Online Store → Themes → Customize → App embeds → Hype: Discounts. Without it none of the three globals exist.
window.HypeUI exposes exactly these classes:
| Class | Element it drives | Campaign type |
|---|---|---|
ProductBannerController | <hype-product-banner> | Multi Products Discount, Order Discount |
ProductPriceController | <hype-product-price> | Multi Products Discount, Order Discount |
GwpBannerController | <hype-free-gift-banner> | Gift with Purchase |
TieredCartController | <hype-tiered-cart-progress-bar> | Cart Goals |
VolumeDiscountController | <hype-volume-selector> | Volume Discount |
VolumeDiscountTableController | <hype-volume-table> | Volume Discount |
CartDrawerController | The Hype Cart Drawer | Not campaign based; see below |
The announcement bar and the toast have no controller. They take everything from attributes; see Campaign elements overview.
Constructing a controller
The six campaign controllers take the block's App block ID — the same value you pass to an element's identifier attribute — and ProductPriceController also takes your shop's money format:
const banner = new window.HypeUI.ProductBannerController('YOUR_APP_BLOCK_ID')const price = new window.HypeUI.ProductPriceController('YOUR_APP_BLOCK_ID', '${{amount}}')const gifts = new window.HypeUI.GwpBannerController('YOUR_APP_BLOCK_ID')const progress = new window.HypeUI.TieredCartController('YOUR_APP_BLOCK_ID')const selector = new window.HypeUI.VolumeDiscountController('YOUR_APP_BLOCK_ID')const table = new window.HypeUI.VolumeDiscountTableController('YOUR_APP_BLOCK_ID')A controller initializes itself as soon as window.Hype.campaigns exists, and otherwise waits for the hype:campaigns-loaded event, so you can construct it at any point after the embed's script has run. On initialization it looks through window.HypeUIBlocksSchema for a block whose settings.identifier equals the identifier you passed, remembers that block and its campaign, and starts computing state. An identifier that matches no block leaves the controller at its default state forever; matching is exact, so trim the value.
Each controller is independent. An element on the page keeps its own controller private, so constructing yours with the same identifier gives you a second instance with its own store. Both compute the same state from the same campaign and cart, and both cost a product fetch on product pages.
Every campaign controller also exposes:
| Member | Type | What it holds |
|---|---|---|
config | nanostores atom | The rendered state; see the types below |
$cart | nanostores atom | A mirror of window.Hype.cart, updated after every cart change |
campaign | object or null | The matched campaign from window.Hype.campaigns |
uiBlock | object or null | The matched entry from window.HypeUIBlocksSchema |
hype | object or null | The window.Hype instance, once initialized |
reset() | method | Puts config back to its default state |
Reading and subscribing
config is a nanostores atom, so it has four methods you will use:
| Method | Behavior |
|---|---|
get() | Returns the current value |
subscribe(callback) | Calls callback(value) immediately and again after every change. Returns a function that unsubscribes. |
listen(callback) | Same as subscribe but skips the immediate call |
set(value) | Replaces the value. The controller calls this itself; you rarely should. |
const progress = new window.HypeUI.TieredCartController('YOUR_APP_BLOCK_ID')const stop = progress.config.subscribe((state) => { if (!state.isVisible) return document.querySelector('#my-progress').style.width = `${state.progressPercentage}%` document.querySelector('#my-progress-text').textContent = state.message})// later, if your markup goes awaystop()With React or Preact, use the official adapter:
import { useStore } from '@nanostores/react'function CartProgress({ identifier }) { const controller = React.useMemo( () => new window.HypeUI.TieredCartController(identifier), [identifier], ) const state = useStore(controller.config) if (!state.isVisible) return null return <p>{state.message} ({state.progressPercentage}%)</p>}With Lit, wrap the atom in StoreController from @nanostores/lit, which is what Hype's own elements do.
Actions you can call
A few controllers do more than publish state.
| Controller | Method | What it does |
|---|---|---|
GwpBannerController | addGiftToCart(variantId) | Claims a gift by variant ID. Sets isLoading and claimingVariantId while the request runs. |
TieredCartController | addGiftToCart(variantId, selectionLimit = 1, productHandles = [], isCustomSelection = false) | Claims a gift for a goal. Pass the goal's selectionLimit, giftProductHandles and isCustomSelection from the gift when the goal lets shoppers choose. |
VolumeDiscountController | handleAddToCart(tierIndex) | Adds the quantity for that tier, using the variants currently selected in tiers[tierIndex].selectedVariants |
VolumeDiscountController | handleVariantChange(slotId, tierIndex, optionName, value) | Changes one option of one variant slot in a tier and recalculates its prices. slotId is the key of that slot in tiers[tierIndex].selectedVariants. |
GwpBannerController, TieredCartController, VolumeDiscountController, VolumeDiscountTableController | updateConfig(partial) | Merges a partial object into config |
Config types
The shapes below are the ones published on each controller's config. Prices are numbers in cents unless they are described as formatted, and formatted strings already carry the shop's money format.
Shared types
type Icon = { type: 'emoji' | 'filled' | 'outlined' | 'none' name: string}type Badge = { icon: Icon text: string}type MarketWiseValueProxy = { isPrimaryMarketValue: boolean value: number}type Gift = { title: string price: number selectionLimit?: number productHandles?: string[] isCustomSelection?: boolean giftSubtitle: string description?: string isAddedToCart: boolean buttonText: string variants: ProductVariant[] options: ProductOption[] featuredImage: string badge: Badge}type SelectedVariant = { variantId: number selectedOptions: Record<string, string> price: number available: boolean imageSrc: string | null}type ProductOption = { name: string position: number values: string[]}ProductVariant is Shopify's own variant object from the product JSON endpoint: id, title, options, price in cents, compare_at_price, available, sku, featured_image and the rest.
For filled and outlined icons, name must be one of Hype's built-in icon names; the list is in Announcement bar element. For emoji, name is the emoji itself. For none, leave it empty.
A MarketWiseValueProxy whose isPrimaryMarketValue is true holds an amount in the shop's primary currency. Multiply it by window.Hype.utils.getCurrencyRate() to get the shopper's currency, which is what every controller does before displaying it.
ProductBannerController
type ProductBannerConfig = { isVisible: boolean message: string icon: Icon}message is the banner text with {{discount}} and {{discount_code}} already substituted.
ProductPriceController
type ProductPriceBlock = | { type: 'original_price'; isStrikedOff: boolean; text: string } | { type: 'discounted_price'; text: string } | { type: 'message'; isBadge: boolean; text: string }type ProductPriceConfig = { blocks: ProductPriceBlock[]}blocks holds zero to three entries in the order configured in the Hype admin, with every text already formatted and every message variable substituted. There is no visibility flag: an empty blocks array means nothing to show. Before a campaign matches, blocks carries the product's own price, so check controller.campaign if you need to know whether a discount is in play. See Product price element.
GwpBannerController
type GwpConfig = { showGifts: boolean isUnlocked: boolean isLoading: boolean claimingVariantId: number | null selectionLimit: number giftProductHandles: string[] isCustomSelection: boolean promoBarTitle: string promoBarIcon: Icon bannerMessage: string bannerIcon: Icon gifts: Gift[] showPromoBar: boolean showCollapsible: boolean}| Field | Meaning |
|---|---|
showGifts | The gift list should be visible |
isUnlocked | The cart meets the campaign's condition |
isLoading | A claim request is in flight; claimingVariantId says for which variant |
selectionLimit | How many gifts the shopper may claim |
giftProductHandles | Handles of the products offered as gifts |
isCustomSelection | The shopper chooses among the gifts rather than receiving a fixed one |
promoBarTitle, promoBarIcon | The collapsible header |
bannerMessage, bannerIcon | The message above the gift list, for the current state: pending, unlocked or added |
gifts | The gifts with their variants, options, price and isAddedToCart |
showPromoBar, showCollapsible | Layout settings from the block |
TieredCartController
type TieredCartProgressUIState = { isVisible: boolean cartTotal: number progressPercentage: number nextTier?: { index: number remainingAmount: number label: string isPrimaryMarketValue: boolean } allTiers: TierUI[] settings: TieredCartSettings message: string successMessages: string[] isLoading: boolean claimingVariantId: number | null}type TierUI = { index: number minAmount: number isPrimaryMarketValue: boolean isUnlocked: boolean label: string icon: Icon positionPercent: number successMessage: string showOffers: string isDisabled?: boolean gifts?: Gift[]}type TieredCartSettings = { identifier: string useCustomCode: boolean showIcon: boolean title: string successMessage: string showOffers: string goals: Array<FreeGiftGoal | NonGiftGoal> header: Badge}type BaseGoal = { text: string icon: Icon successMessage: string goalName: string}type FreeGiftGoal = BaseGoal & { type: 'freeGift' giftBlock: { showGiftBlock: boolean badge: Badge giftSubtitle: string description: string claimButtonText: string claimedButtonText: string }}type NonGiftGoal = BaseGoal & { type: 'freeShipping' | 'amountOff'}cartTotal and minAmount are in cents. progressPercentage runs from 0 to 100. positionPercent is where the tier's marker sits on the bar. nextTier is absent once every goal is reached, and message then carries the campaign's overall success message. showOffers is one of currentGoal, nextGoal or hideGoal, the block's Show offers setting.
VolumeDiscountController
type VolumeDiscountConfig = { isVisible: boolean isLoading: boolean tiers: VolumeDiscountTier[] header: VolumeDiscountHeader hasVariants: boolean variantPickerOptions: Record<string, string[]> variants?: ProductVariant[] options?: ProductOption[]}type VolumeDiscountTier = { cartEligibility: { type: 'quantity' | 'amount' value: MarketWiseValueProxy } customerGets: { appliesToEachItem: boolean type: 'percentage' | 'amount' value: MarketWiseValueProxy } popularBadge?: { icon: Icon; label: string } title: string discountBadgeText: string selectedVariants: Record<string, SelectedVariant> strikeOriginalPrice: boolean showAmountOffOrPercentageBadges: boolean}type VolumeDiscountHeader = { icon: Icon title: string}variantPickerOptions maps each option name to its values, for building pickers. selectedVariants holds one slot per unit in the tier, keyed by a generated id; iterate its entries rather than assuming the keys. variants and options are the full product data and are absent in the theme editor's sample state.
VolumeDiscountTableController
type VolumeDiscountTableConfig = { isVisible: boolean isLoading: boolean tiers: Array<{ eligibility: string; discount: string }> header: VolumeDiscountHeader}Each tier is two formatted strings, for example Buy 2 and Save 10%, with the block's variables already substituted. This VolumeDiscountTier shape is different from the selector's, even though the two share a name in the source.
CartDrawerController
CartDrawerController belongs to Hype Cart Drawer, not to campaigns. It is constructed with an options object rather than an identifier, keeps its state in a store named state rather than config, and offers cart actions such as open(), close(), setQuantity(key, quantity), addItem(), removeLine(key), applyDiscount(code) and goToCheckout(). It listens for the hype:cart:open window event. Its documentation lives with that app.
Related
Was this article helpful?
Related articles
Still stuck?
Ask a follow-up and we'll see it came from “UI SDK (window.HypeUI)”.
Opens chat with your question and a link to this article.