> ## Documentation Index
> Fetch the complete documentation index at: https://swish.app/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Product updates

> New releases and improvements to Swish

**v0.153.0**

## Ask in a dialog before saving from the cart

Removing an item from the cart can now open a dialog that asks the customer whether to save it, which is harder to miss on a wide screen than the toast. Set **Global → Preferences → Cart → Ask before saving a cart line** to **Ask in a dialog**, or [`intents.saveCartLine.confirm`](https://swish.app/docs/references/sdk/configuration#savecartline) to `"dialog"`. **Ask in a toast** (`"toast"`, or `true` as before) keeps the prompt in the toast. Change the dialog's text with the `saveForLaterAlert` keys in [translations](https://swish.app/docs/guides/customize/translations.md#save-for-later-alert).

## Saves reach the Meta Pixel

Every save from a customer who allows marketing now fires Meta's standard `AddToWishlist` event on your Meta Pixel, with the saved variant as the content ID and its price in currency units, so saves can build audiences and retargeting alongside views and add-to-carts. With the pixel loaded in your theme there is nothing to do. With the pixel installed by the Facebook & Instagram app, a short custom pixel forwards the event; the [Meta Pixel](https://swish.app/docs/integrations/platforms/meta-pixel.md) page has it. It is on for every store, and `integrations.metaPixel.enabled: false` in `window.SwishOptions` turns it off.

## A subtitle on product cards

The [`productSubtitle`](https://swish.app/docs/guides/customize/transforms#product-subtitle) transform adds a line under the product title on product cards, in the drawer and on the Wishlist page. Return the text to show, for example the colour of a product from a combined listing once Color is out of the option select, or a value from a product metafield. It shows in the subdued text colour between the title and the price. Return nothing to leave cards as they are.

## The Wishlist page has a header

The standalone [Wishlist page](https://swish.app/docs/guides/customize/views/list-detail-page.md) now opens with a header. On a customer's own list it carries the list menu, with a lock beside a private list's name; renaming a list or changing its privacy updates the header, and deleting it returns the customer to all of their saved items. At the end of the header sits an account button: **Sign in** for a guest, and the customer's initial for an account, which opens a small menu with their lists, their account page and sign out. In the customiser, **Views → Wishlist page → Page header** sets the title shown on the all-items view, aligns it left or centred to match your collection pages, and turns the **Add all to cart** and account buttons on or off. A **Share** button is there too, off and locked for now: it can only share a single list, and it opens up once the whole wishlist can be shared. In code it's the `page-header` section of `ui.listDetailPage`, where `showShareButton: true` already enables it for single lists.

## Choose the image on product cards

The [`productImage`](https://swish.app/docs/guides/customize/transforms#product-image) transform picks the image a product card shows, in the drawer and on the Wishlist page. Return any of the product's images, for example the second one or a flat lay found by its alt text, or an image URL from a product metafield. Return nothing to keep the default: the variant's image, else the featured image.

## Switch product card images in the drawer

Turn on **Show image toggle** for the grid in **Views → Saves** or **Views → List detail** (`showImageToggle`) to give customers a switch between each card's default image and the one your [`productImage`](https://swish.app/docs/guides/customize/transforms#let-customers-switch-images) transform picks, such as model shots and flat lays. Cards open on the default image, and the choice lasts for the session. Rename the switch with `productCard.imageToggle` in [translations](https://swish.app/docs/guides/customize/translations.md).

## Save button beside the product title

The save button on product cards can now sit beside the product title and price instead of over the image. Set **Global → Preferences → Product cards → Save button position** to **Beside title**, or [`ui.productCard.saveButtonPosition`](https://swish.app/docs/references/sdk/configuration#product-cards) to `"title"`. There it shows as a plain icon in the card title colour, filled once saved. Quick buy and badges stay on the image. It applies to every product card in the drawer and on the Wishlist page. The default, **On image**, leaves cards as they were.

## Variant select on combined listings

Customers can pick a variant of a product from a Shopify combined listing in the save dialog, quick buy, the product detail view in the drawer and the Notify Me dialog. Without the [`combinedListingParent`](https://swish.app/docs/guides/customize/transforms#combined-listing-parent) transform, the picker offers the variants of the product the customer is on. With it, the picker also switches between the listing's products.

## Quick buy adds saved items straight to the cart

The quick-buy button on a card for an item saved with a variant now adds that variant straight to the cart, instead of opening the quick buy dialog to confirm it. It adds the amount on the card's quantity stepper when **Show quantity input** is on, and 1 otherwise. The card then shows as in the cart, and tapping it again takes the customer to their cart. Items saved without a variant still open quick buy to pick one. To keep the dialog for every card, turn on **Global → Preferences → Cart → Quick buy → Prompt for variant even when one is selected**, or set [`intents.initiateQuickBuy.promptVariantIfPresent`](https://swish.app/docs/references/sdk/configuration#initiatequickbuy) to `true`. [`initiate:quick-buy`](https://swish.app/docs/references/sdk/intents#initiate-quick-buy) follows the same setting: given a `variantId`, it adds to the cart and resolves as `create:cart-line`.

## Previewing the sign-in requirement covers `#swish` links

In the customiser, a page you open with a `#swish` link or a `?swish-intent=` URL now follows the **Sign-in requirement** you are previewing in **Global → Preferences → Wishlist experience**. Before, it followed your saved setting, so with **Require sign-in to view** chosen but not saved, the header button asked a signed-out customer to sign in while `/#swish` still opened the drawer. Live storefronts were not affected.

## A New badge for recently published products

A custom badge in **Global → Preferences → Product badges** can now show on products published in the last few days. Pick **Product published within** as its condition and set the number of days, 30 by default. Call it **New** to mark your latest arrivals without tagging them: the badge comes off each product by itself once the days have passed. In code the condition is `{ type: "published-within", days }`; see [`ui.badges.rules`](https://swish.app/docs/references/sdk/configuration#product-badges).

## Custom icons are sanitised before they render

Every SVG in [`ui.icons`](https://swish.app/docs/guides/customize/appearance/icons) is now sanitised in the browser before it renders, whatever saved it. Scripts, event handlers, `javascript:` links and elements that are not SVG are removed. If nothing drawable is left, the built-in icon shows instead. Icons made in the customiser render as before.

## Product not found in the drawer

When a customer opens a product that has since been deleted or unpublished, for example from a notification about it, the product detail view in the drawer now says **Product not found** instead of loading forever. Change the text with `drawer.productDetail.productNotFound` and `drawer.productDetail.productNotFoundDescription` in [translations](https://swish.app/docs/guides/customize/translations.md).

## X icon on Saved from cart

The button that removes an item from **Saved from cart** in the drawer now shows an X instead of a trash can. To keep the trash can, set **Global → Preferences → Cart → Saved from cart remove icon** to **Trash can**, or `ui.cart.savedFromCartRemoveIcon` to `"trash"`.

## Require sign-in for all features

**Require sign-in for all features** is a new sign-in requirement that asks a signed-out customer to sign in before every action Swish can gate, including any gated in a later release. Today that is saving and opening Swish: a customer who taps save, a header button, a `#swish` link or any `view:` intent goes to sign-in instead, and the header button and `<swish-drawer>` count read as empty until they are back. As under **Require sign-in to save**, an item saved automatically when a line is removed from the cart still saves. Pick it from **Sign-in requirement** in **Global → Preferences → Wishlist experience**, or set [`features.signInRequirement`](https://swish.app/docs/references/sdk/configuration#features) to `"all"`. See the [Account guide](https://swish.app/docs/guides/features/account.md#sign-in-requirements) for what each setting covers.

## Greet customers in the drawer header

Set **Views → Drawer → Header → Header style** to **Greeting** (`headerStyle: "greeting"`) to show **Hi, Sarah** at the top of the drawer instead of your logo, using the first name on the customer's account. Guests see **Welcome**, or your own text from **Guest greeting** (`guestGreeting`). The greeting uses the drawer's heading style. Reword either default with `drawerHeader.greeting` and `drawerHeader.guestGreeting` in [translations](https://swish.app/docs/guides/customize/translations.md). See [Drawer](https://swish.app/docs/guides/customize/views/drawer.md#greeting).

## Expand a list to the full width of the browser

Turn on **Show expand button** in **Views → List detail** to give customers an expand button in a list's header in the drawer. It widens the drawer to the full width of the browser, and the grid fills the room with more columns, so a long list or a list of large products shows more at once. The footer navigation hides while the drawer is expanded, and the header stays with the list's share and menu buttons. The drawer goes back to its usual width when the customer collapses it, leaves the list or closes the drawer. The button shows on screens 600px wide and up, where the drawer opens as a side panel. In code it's `showExpandButton` on the `list-items-grid` section of [`ui.listDetail`](https://swish.app/docs/guides/customize/views/list-detail#full-width-view). Its screen-reader labels are `drawer.listDetail.expand` and `drawer.listDetail.collapse` in [translations](https://swish.app/docs/guides/customize/translations.md).

## Save button animations

Save buttons can play an animation when a customer taps one to save an item: **Pulse**, **Jello** or **Particles**, a burst of small copies of the icon. Pick it under **Global → Save button → Save animation**, where the preview plays it, or set [`ui.saveButton.animation`](https://swish.app/docs/references/sdk/configuration#save-button). Placed save buttons and the hearts in the drawer and on the Wishlist page play the same one, and nothing plays for customers who turn on reduced motion. The default, **None**, plays nothing.

**v0.152.0**

## Continue with Shop follows your Sign in with Shop setting

The sign-in callout offers **Continue with Shop** only on stores with **Sign in with Shop** turned on in Shopify. With it off, customers see the regular sign-in button.

## Variant select on product cards

When a product card lets the customer change the variant ([`enableVariantChange`](https://swish.app/docs/references/sdk/configuration#views), on by default on the Wishlist page), an item saved without a variant reads **Select Size**, and its buy button opens quick buy to pick one. The prompt names the product's first option and is `variantSelect.selectOption` in [translations](https://swish.app/docs/guides/customize/translations.md). In code, `swish.state.itemState` and [`useSwishItem`](https://swish.app/docs/references/react/hooks#useswishitem) expose the saved variant as [`savedItemVariantId`](https://swish.app/docs/references/sdk/state#state-properties).

## Close button sits beside the drawer header's other buttons

**Views → Drawer → Header → Show close button** (`useCloseButton`) adds a close button at the end of the drawer header, after the notification and account buttons. To show the close button alone, also turn off **Account button** and **Notification button** (`accountButton` and `notificationButton`). See [Drawer](https://swish.app/docs/guides/customize/views/drawer.md).

## Notify Me lists only sold-out variants

The Notify Me dialog now offers only variants a shopper can't buy. Each option lists the values that lead to a sold-out variant, given what is picked in the options before it: pick a colour and the size list narrows to the sizes sold out in that colour. Since everything listed is sold out, values are no longer struck through or marked **Sold Out** in the dialog. If the dialog opens on an in-stock variant, for example from [`create:subscription`](https://swish.app/docs/references/sdk/intents#create-subscription), that pick is cleared and the shopper chooses again. When nothing is sold out, the dialog lists every variant as before.

## Notify Me asks for the variant with a dropdown

The Notify Me dialog now shows one dropdown per option instead of pills. Until the shopper picks one, the dropdown reads **Select an option**, which is `productOptions.selectPlaceholder` in [translations](https://swish.app/docs/guides/customize/translations.md). To keep pills or rows, set **Global → Preferences → Notify Me dialog → Variant picker**, or `ui.notifyMe.variantSelectDisplayType` to `"pills"` or `"rows"`. Your product page's **Variant select display** no longer applies to the dialog.

## Variant select display reaches the drawer and quick buy

**Product detail → Product info → Variant select display** now applies to the product detail view in the drawer and to quick buy. Both always showed pills before, whatever you chose. If you chose **Rows**, they now show rows.

## Link card to product page is one switch for every view

**Link card to product page** is now only under **Global → Preferences → Product cards**. **Views → List detail** no longer has its own switch, so the Global switch decides for every product card in the drawer, including cards in a list. The Wishlist page still always links its cards to the product page. In code, the `list-items-grid` sections of `ui.listDetail` and `ui.listDetailPage` no longer take a `linkToProductPage` option, and a value set there is ignored. Use [`ui.productCard.linkToProductPage`](https://swish.app/docs/references/sdk/configuration#product-cards) or the [`linkToProductPage`](https://swish.app/docs/guides/customize/transforms#link-to-product-page) transform.

## Low stock and discount badges

**Global → Preferences → Product badges** now has a built-in **Low stock** badge next to **Sold out** and **Sale**. It shows when 1 to your number of units are left (5 unless you change it). It reads the selected variant when there is one and the product's total stock otherwise, and never shows for a sold-out item or a variant selling on backorder. **Low stock** starts hidden: turn it on with the eye. Swish only fetches stock levels while it is shown. A custom badge can also show when a product is discounted by at least a percentage you choose (**Product discount is at least**, 20% unless you change it).

The text of **Sold out**, **Sale** and **Low stock** is now changed in each of your languages under **Global → Translations → Products**, so the **Badge text** field is gone from those three rows. If you entered text there, it showed in every language; after your next save the badge shows its translated text instead, so add your wording under **Translations**. In `window.SwishOptions` the new row is `{ type: "low-stock", max }` and the new condition is `{ type: "discount", minPercent }`; see [`ui.badges.rules`](https://swish.app/docs/references/sdk/configuration#product-badges).

## A setup runs on the theme it was set up for

Each setup is tied to the theme it was set up on, by the theme's own name (for example Dawn), so copies and new versions of that theme keep working. On a theme with a different name, the Swish app embed doesn't start Swish and logs a `[swish]` error in the browser console. The customiser still opens on any theme. To use a setup on another theme, choose **Duplicate to theme** in **Settings → Setups**; the copy keeps its placements only if the new theme has the same name. The customiser's top bar shows the setup you're editing and its theme.

## Saved items remember their quantity

The product detail view in the drawer opens on the quantity stored on a saved item. Changing it saves to the item, so the same quantity is there the next time the customer opens it, and **Add to cart** adds that amount. A save made from the drawer keeps the quantity chosen in the input. In code, the new [`edit:item-quantity`](https://swish.app/docs/references/sdk/intents#edit-item-quantity) intent writes the quantity, and `swish.state.itemState` and [`useSwishItem`](https://swish.app/docs/references/react/hooks#useswishitem) expose it as [`savedItemQuantity`](https://swish.app/docs/references/sdk/state#state-properties).

## List cards can show the saved quantity

Turn on **Show quantity input** under **Views → Wishlist page** or **Views → List detail** and each card shows a stepper set to the item's saved quantity. **Add to cart** and quick buy add that amount, and the quick buy dialog opens on it. On the customer's own list a change is saved to the item, so it is there after a reload. On a shared list, or a smart list such as Recently viewed, a change only sets the amount added, and a reload shows the saved quantity again. In code the option is `showQuantityInput` on the `list-items-grid` section of [`ui.listDetailPage`](https://swish.app/docs/references/sdk/configuration#list-items-grid) and `ui.listDetail`, and [`initiate:quick-buy`](https://swish.app/docs/references/sdk/intents#initiate-quick-buy) takes a `quantity`.

**v0.151.0**

## The heart on Saved from cart moves the item to the wishlist

Tapping the heart on a **Saved from cart** card saves the product to the customer's wishlist in the same quantity and takes it out of Saved from cart. In code, `swish.state.itemState` and [`useSwishItem`](https://swish.app/docs/references/react/hooks#useswishitem) take a [`quantity` option](https://swish.app/docs/references/sdk/state#options).

## Items moved from the cart are tagged `saved-from-cart`

A cart item a customer moves into their wishlist carries the `saved-from-cart` tag, so you can tell those saves apart in [`swish.api.items`](https://swish.app/docs/references/sdk/api-client#items) and the REST API. `swish.state.itemState` also takes a [`tags` option](https://swish.app/docs/references/sdk/state#options).

## Style each placed save button on its own

A placed save button can override any **Global → Save button** setting, including icon, sizes and the Unsaved and Saved colours, and inherits whatever you leave unset. In `window.SwishOptions`, see the [save button design tokens](https://swish.app/docs/references/sdk/placements.md#save-button-design-tokens).

**v0.150.0**

## Translations in the customiser

**Global → Translations** lists your store's languages, with your default language first and each other language marked **Translated** or **Needs translation**. Open one to change Swish's text line by line, grouped by where customers see it, with the English beside each line. Leave a line empty to keep Swish's own text. Your changes reach your storefront when you save. See [Translations](https://swish.app/docs/guides/customize/translations.md#in-the-customiser).

## Translate Swish into any language

Swish's text can be translated into languages it doesn't include, such as French or Spanish. Add your text under that language's code in [`translations`](https://swish.app/docs/guides/customize/translations). Customers who browse in that language see your translations, and English for anything you leave out.

## The customiser warns when your theme also configures Swish

If your theme's Liquid code sets `window.SwishOptions`, the customiser now shows a notice under its top bar: **Your theme has its own Swish settings**. Those settings might conflict with what you configure in the customiser. The notice only informs you. It changes nothing in your theme or your setup, and it doesn't appear when your theme sets no `window.SwishOptions`.

## Choose which badge a product shows

**Global → Preferences → Product badges** lists your product badges in priority order, and each product shows one: the first badge in the list that applies to it. **Sold out** and **Sale** start in the list in today's order, so nothing changes until you edit it. Drag a badge to change its priority, or hide it with the eye. Add your own badges with no code: each shows when a product has a tag, or when a product metafield has any value, equals a value or contains one. Every badge can have its own background and text colour; leave them blank to use the style from **Design → Badges**. **Sold out** and **Sale** can also take your own text, which replaces the translated text in every language. Swish fetches the metafields your badges name, so you don't list them under `metafields` as well. The list is `ui.badges.rules`, and the [`productBadges`](https://swish.app/docs/guides/customize/transforms#product-badges) transform receives the chosen badge in `defaultBadges`, so a transform can still change or drop it. **Sold out** and **Sale** without custom colours still arrive as the same strings as before; anything else arrives as a `Badge` object.

## Save buttons come in two looks

A placed save button is now either **On image** or **On page**, and each look has its own settings. On image is for buttons over product photos - collection cards and image corners: a dark heart on a white chip, filled once the item is saved. On page is for buttons in the page flow - under **Add to cart** or in your theme's button row: a white heart and **Save item** label on a dark button, sized like your theme's buttons and rounded by **Design → Rounding**.

Set each look on **Global → Save button**, where the preview shows the configured button on a light and a dark ground, exactly as a light or a dark theme renders it. Editing one look never changes the other, and **Icon style** stays shared. A product page placement picks its look with **Style**: choosing On image turns **Icon only** on and moves the button onto the product image as an overlay in its top corner, choosing On page puts the labelled button back under **Add to cart**. Collection placements are always On image. In `window.SwishOptions` the tokens are `ui.design.saveButtonOnImage` and `ui.design.saveButtonOnPage`, replacing `ui.design.saveButton` and `savedIconColor`, and a placement carries [`style: "on-image" | "on-page"`](https://swish.app/docs/references/sdk/placements#style). A placement that names no `style` is On page unless it sits on a collection card, so add `style: "on-image"` to a hand-coded overlay over product images. The hearts in the drawer and on the wishlist page follow the On image settings.

## Save for later skips items already saved

When a customer removes a cart item they have already saved, Swish no longer offers to save it again or saves a second copy. Instead a toast tells them **You've already saved this**, and nothing is added to **Saved from cart**. Removing an item that isn't saved works as before. The new text is `toast.alreadySaved` in [translations](https://swish.app/docs/guides/customize/translations.md), and it is hidden along with the other save-for-later feedback when `saveCartLine.showFeedback` is `false`.

## Saved from cart shows a first-time visitor's items

A visitor with nothing on their wishlist who removed an item from the cart saw the **We've saved this for you** toast, but **Saved from cart** in the drawer stayed empty. The item only appeared once they also saved something to the wishlist. It now shows straight away.

## Pick the unsaved heart colour in the drawer

The hearts in the drawer and on the wishlist page now take `saveButtonOnImageIconColor` before an item is saved, just as they already took `saveButtonOnImageSavedIconColor` once it is. Until now they stayed your primary button text colour, so only the saved half of the pair reached them. Every save button token is listed under `ui.design.saveButtonOnImage` and `ui.design.saveButtonOnPage` on the [Design](https://swish.app/docs/guides/customize/appearance/design.md#save-button) page.

## Preferences are split into rows

**Global → Preferences** no longer puts everything under one **Behaviour** row. It now lists **Wishlist experience**, **Saving**, **Cart**, **Notify Me dialog** and **Product cards**, each its own collapsible row, and the drawer-or-page choice is labelled **Wishlist surface**. Nothing to redo: the values are unchanged, they have only moved.

## Preview a placed save button as saved

A save button placement's preview now has **Unsaved** and **Saved** tabs, like the Save button page, and clicking the previewed button flips it too. Its **Saved background** and **Saved text colour** overrides show up in the Saved tab, so you can check the saved look before publishing.

## Show whole product photos

Product cards, gallery cards and the product detail slider crop each photo to fill its frame, which cuts the edges off non-square photos in a square frame. Set **Global → Components → Images → Image fit** to **Show whole image** to fit the entire photo inside the frame instead. The setting is `imageFit` under `ui.design.sizes`, and **Crop to fill** keeps the current look.

## Notify Me is off by default in the drawer

A sold-out variant in the drawer's product detail view and in quick buy shows the disabled **Sold out** button again, not **Notify me when available**. To offer sign-ups there, set `notifyWhenSoldOut: true` on the `buy-buttons` section of `ui.productDetail` in `window.SwishOptions`, and pass the whole [default `sections` array](https://swish.app/docs/guides/customize/views/product-detail) with it, since a `sections` array replaces the defaults. The **Notify Me** button placement on your theme's product page is unchanged.

## Components have their own screen

The per-component settings that sat at the bottom of **Design** — drawer width, thumbnails, product cards, product options, gallery cards and form inputs — now live on **Global → Components**, with each component as its own collapsible row. Nothing to redo: the values are unchanged, they have only moved.

## The header button matches your theme's header icons

A **Header** placement you haven't styled now looks like the icons beside it: a 44px button around a 20px heart drawn in your header's text colour. It used to borrow the **Save button**'s icon colour, which is white for a heart over product photos — and invisible on a light header. Set **Icon outline colour** on the placement to pick something else.

The saved-count badge is centred on the heart's corner, with no white ring, and stays there when **Show text** puts a label beside it — it used to hang off the button's edge, 11px from the heart, and drift further the longer the text. **Badge position** still picks the corner and the two **Badge offset** sliders still nudge it, but they now start from the heart, so a badge you had dragged into place is worth a second look. The customiser preview draws the badge as the round shape the storefront shows, and a numeric `statusIndicatorFontWeight` set through `swish.placements` now applies instead of being dropped.

## Add to cart is back by default

A store that has never written an `addToCartButton` transform shows **Add to cart** on the drawer's product detail again, and quick buy on **Home** featured collection rows — both had been missing since April. Hide them per product with the [`addToCartButton` transform](https://swish.app/docs/guides/customize/transforms#add-to-cart-button), or turn the product detail buttons off for the whole store with the eye toggle on **Views → Product detail → Buy buttons**.

## The drawer header shows your store name

The drawer opens with your store's name across the top, and follows it if you rename the store. Set a **Title** or a logo under **Drawer → Header** to show something else instead.

## Save from cart catches every removal

Removing an item didn't always offer to save it — on some themes, and when another app was reading the cart at the same time. Every removal is now caught.

## The Home view is now For You

The drawer's first tab reads **For You** in every language Swish ships, and the customiser lists the view as **Views → For you**. The rail on **Saves** that holds **Recently viewed** and **Saved from cart** is now **Smart Lists**, so no two surfaces share a name. Nothing to redo: the underlying settings are unchanged, and a translation you've overridden still applies.

## Remove items from Saved from cart

Each **Saved from cart** card in the drawer, on **For You** and in the list itself, now has a trash button above the heart that removes the item once the customer confirms. The heart saves the product to the customer's wishlist, and the card stays in Saved from cart until they remove it. Until now there was no way to take an item out of Saved from cart.

## The customiser no longer signs you out after your laptop sleeps

The customiser sometimes showed **Session expired** when you came back to it after your laptop slept, after the tab sat in the background, or after a brief network drop, even though your session was still valid. It now renews its sign-in as soon as you return to the tab, retries while the connection comes back, and when a save meets an expired sign-in it renews and saves again instead of stopping. **Session expired** now appears only when the session has really ended, and reopening the customiser from Swish admin still starts a new one.

**v0.149.0**

## Notify Me confirms the sign-up

After a shopper leaves their email, the dialog now stays on **You're on the list** with the address it went to and a **Done** button, instead of closing before anyone could read it. **Global → Preferences → Notify Me dialog** gains a third mobile-only switch, **Full height on mobile**, which takes the dialog to the top of the screen. In code, `fullHeight` on `ui.notifyMe`.

## Saves drops the empty "For You" rail

A shopper with nothing saved no longer sees an empty **For You** rail, or a grey card that never resolves when the lists request fails. A rail with nothing to show is hidden, and the one still loading now carries its name.

## Colors grouped by role

**Design → Colors** is now grouped into **Text**, **Buttons** and **Surfaces**, and badge colours have their own **Badges** section. Swish's own save hearts in the drawer and wishlist page now follow the **Save button** colours, so the two separate heart-background fields are gone.

## Buttons recover from a failing handler

A custom intent handler that throws no longer leaves a drawer or card button spinning. The button settles and can be tried again.

**v0.148.0**

## Build your own wishlist page or drawer

`swish.state` could already power a custom save button and a header badge, but a custom **page** of saved items meant calling the API yourself, paginating by hand, and wiring up every event that could change the grid. Four new state helpers take that over. `listContext(window.location)` reads which list a page is about from the same URL parameters share links and the `view:list` intent already use. `lists()` gives you the customer's named lists plus **Saved from cart** and **Recently viewed** for tabs. `items(context)` is the paginated collection itself: it drops an item the moment it's unsaved, prepends a fresh save, patches a changed variant, and refills after sign-in, whether the change came from your UI or ours. `product(ids)` resolves one card's product data lazily, sharing the built-in UI's cache and combined-listing handling.

The simplest custom page is a `listContext` feeding `items`, plus `product` and the existing `itemState` per card. A richer drawer writes one context signal per tab and reuses a single collection. Actions stay on intents, so there is nothing new to learn there. Every helper now has `dispose()`, including `itemCount()`, which previously kept refetching after its consumer went away. See the [State reference](https://swish.app/docs/references/sdk/state.md) for the full API and two end-to-end examples.

## Notify Me on sold-out products

When a product is sold out, shoppers can sign up to hear when it's back. **Notify me when available** takes the place of the theme's disabled button, and the dialog collects a size and an email address. A product with sizes still in stock is untouched — the theme's buy button stays where it is.

**Global → Preferences → Notify Me dialog** adds two mobile-only switches, both off by default: **Flush to edges on mobile** runs the dialog edge to edge against the bottom of the screen, and **Taller on mobile** gives it the drawer's height. In code, `flush` and `tall` on `ui.notifyMe`.

## Resize the drawer's footer menu labels

The **Home / Saves / Lists** labels along the bottom of the drawer were fixed at 10px, which is small next to a bold storefront font and smaller still once a translated label runs long. **Views → Drawer → Footer** now has a **Text size** slider: 1× is what you have today, and you can take it anywhere from 0.8× to 2×.

It's a multiplier rather than a pixel value, so if you've already set your own `navLabelFontSize` the slider scales from your size, not ours. Labels only — the icons under **Show icons** keep their own size, so the bar's height and alignment stay where your design put them.

In code it's `fontScale` on the `drawer-footer` section of `ui.drawer`, and it drives a new `--swish-theme-nav-label-font-scale` theme token.

**v0.147.0**

## Point the account button at your own account page

The account icon in the drawer header always opened Swish's profile view, and the only way out of that was to hide the button altogether. **Views → Drawer → Header** now has an **Account button opens** dropdown: **Swish profile** (the default, so nothing changes for anyone), **Shopify account page**, or **Custom URL** for a page of your own.

Pick **Shopify account page** and Swish works out the right address for your store. **Custom URL** takes anything else, like `/pages/my-account` or a full web address.

It covers the header icon only. The footer's **Profile** tab and the **Account link** placement are separate controls and still open the Swish profile view — turn those off too if you want customers off it entirely.

**v0.146.0**

## Notify Me on sold-out variants

A sold-out variant used to be a dead end: a greyed-out "Sold Out" and nothing to do about it. Now the shopper can ask to hear when it's back, and you keep the sale you were going to lose.

Two places it shows up. Inside Swish, **Views → Product detail → Buy buttons** has a new **Offer Notify Me when sold out** switch which turns that disabled button into **Notify me when available** in the drawer product detail and in quick buy. It's on by default, so this one arrives switched on for every store — turn it off there if you'd rather keep the disabled button. And on your own product page there's a new **Notify me button** placement under **Placements → Product detail**, sitting in the same list as the save button: pick it from the **Type** dropdown when you add a placement. That half is opt-in — nothing appears until you add it.

The placement is the interesting half, because it works with your theme rather than around it. Point it at your buy-button row — the Target field suggests `.product-form__buttons`, which is Dawn's — and Swish hides the element you pointed at *only* while the selected variant is out of stock. Switch to a variant that's in stock and your own button comes back exactly as it was, with its own styling and its own behaviour. Nothing is replaced, so nothing is lost. What you point at is what gets hidden, so aim at the button row itself rather than the form around it.

Either way, tapping it opens the standard Swish modal: product thumbnail, a `Product / Option / Option` breadcrumb, an email field and a signup button. A signed-in shopper's address is already filled in; a guest types one and can subscribe without an account. Submitting confirms inline and closes itself, and the signup lands in your subscriptions where the back-in-stock notification picks it up.

In code the placement is `element: { name: "notify-button" }` with `mount: { mode: "after" }`, the in-drawer switch is `notifyWhenSoldOut` on the `buy-buttons` section of `ui.productDetail`, and there's a new `create:subscription` intent to open the modal yourself. `swish.api.subscriptions` is now exposed too — `list`, `create`, `findById`, `updateById`, `deleteById`. All the copy is translated in all six languages under `notifyMe.*` and `buyButtons.notifyMe`.

## Content blocks on Saves and Lists

Content blocks are no longer a Home feature. **Views → Saves** and **Views → Lists** now carry them too, with everything Home has: **Add content block** under the section list, up to three per view, a delete control on each, the **Visible to** targeting group, and a drag grip on every row.

Which matters because Saves and Lists are where a shopper has already told you what they want. A block above the saved-items grid reaches someone with your product in mind and nothing in their cart — closer to the sale than the same message on Home, and closer still if you point it at a tag. Each view keeps its own blocks, so the offer on Saves can be a restock nudge while Lists gets the account prompt.

Both views ship one block, switched off, seeded just above their grid. Drag it wherever you want: on Lists a block can sit either side of the lists themselves, and on Saves it can go above or below the grid. The **All Items** screen is the saved-items grid on its own, so it drops content blocks the same way it drops the lists sliders.

Nothing about an existing block changes, and the targeting rules are identical across the three views — see [Content blocks](https://swish.app/docs/guides/customize/views/home.md#content-block) for the options in full. In code it's a `content-block` section of `ui.saves` or `ui.lists`, taking the same `title`, `body`, `image`, `link` and `visibleTo` as on `ui.home`. If you've saved either view before, your existing sections are kept and the new block is added above the grid.

## Show a content block to only some shoppers

Home now carries a content block, and you can point it at a slice of your customers instead of all of them. Under **Views → Home → Content block** there's a new **Visible to** group: **Shoppers** picks Everyone, Signed in only or Signed out only, and **Customer tags** takes any Shopify customer tags you use — `VIP`, `Loyal Customer`, whatever your store already tags on. A shopper carrying any one of the listed tags sees the block; everyone else doesn't.

So the segment-only message you'd otherwise send by email can live in the drawer: tag your best customers `VIP`, write the offer once, and only they get it. The block ships switched off, and the eye toggle still hides it from everyone regardless of who it's aimed at.

Home also takes **more than one** block now — up to three. **Add content block** sits under the section list, each block has a delete control, and every section has a drag grip, so you can put a promo above your saved items and a VIP offer below them. If a shopper matches two blocks they see both, in the order you've arranged them.

Tag matching ignores case, so `VIP` finds a customer tagged `vip`. Only customers have tags, so a tag list also means signed in — pairing one with **Signed out only** shows the block to nobody. The preview applies the rule as your shoppers will see it, the same way it already respects the eye toggle, so a block aimed at someone the preview isn't drops out of view — it stays in the **Sections** list, where you edit it.

In code the setting is `visibleTo: { audience, customerTags }` on a `content-block` section of `ui.home`. Headless stores need to pass `context.customer.tags` themselves; on Shopify themes the app embed handles it.

## The "in cart" button takes shoppers to their cart

Tap a product that's already in the cart — the quick buy button on a card, or **Add to cart** on the product detail view — and Swish now takes the shopper to their cart instead of doing nothing. By default it navigates to the cart page.

Stores that open a slide-out cart drawer instead of a cart page can point Swish at it: **Global → Preferences → Behaviour → Cart** has a new **Cart drawer selector** field. Enter the CSS selector of the element that opens your theme's cart drawer (for example `#cart-icon-bubble`) and Swish clicks it. Leave it blank — or if the selector is invalid or matches nothing — and Swish falls back to the cart page, so the button is never a dead end. A **Send shoppers to their cart** switch turns the whole behaviour off.

In code the settings are `ui.cart.navigateOnInCart: boolean` and `ui.cart.drawerTrigger: string`.

## Card layout for saved items

Three views now have a **Layout** setting: **Views → Saves** (the saved-items grid), **Views → List detail** and **Views → List detail page**. Leave it on **Gallery** and nothing changes; switch it to **Card** and each saved item gets a full-width row instead of a grid tile: the image on the left, and on the right the vendor, product title over two lines, price, saved variant and an **Add to cart** button.

Card layout is for stores where the grid tile is too cramped to sell — more room per product means a badge and a real call to action can sit side by side instead of competing for the same 150 pixels.

The Saves setting also drives the **All Items** screen its heading links to, since both read the same `all-items-grid` section. The lists sliders above the grid are unaffected.

A few things the layout decides for you. The product image renders square rather than following your **Global → Design → Image card** aspect ratio: a portrait image makes each row half again as tall and undoes the point of the layout. On Saves and in the drawer's list detail, **Add to cart** is always on in card layout — neither view exposes a buy-button switch to turn on — though it still only appears if you've supplied the [`addToCartButton` transform](https://swish.app/docs/guides/customize/transforms#add-to-cart-button). On the standalone list page the existing **Show buy button** switch keeps working, and **Desktop columns** still applies, but card rows are wide: 1 or 2 reads best, and mobile is always a single column.

The new vendor line has its own **Show product vendor** switch, on by default, and applies to card layout only — the gallery tile is unchanged. In code the settings are `layout` and `showVendor` on the `all-items-grid` section of `ui.saves` and on the `list-items-grid` section of `ui.listDetail` and `ui.listDetailPage`.

## Open your saved items on their own

The heading above the saves grid is now a link. Tap the chevron or the heading text and the saved items open on a screen of their own, headed **All Items** and without the **Your Lists** and **For You** sliders above them; the drawer's back arrow returns to Saves, and the Saves tab stays selected throughout.

It's the same grid, reading the same `all-items-grid` options you set for the saves view, so anything you configure there applies on both screens. The chevron only appears where it has somewhere to go: a store whose saves view is already nothing but the grid — single list mode, or both sliders hidden — doesn't get one.

**All Items** is translated into every built-in language (**Alle Artikel**, **Kaikki tuotteet**, **Все товары**, **Alla artiklar**, **Wszystkie produkty**). Want different wording? Override `saves.allItems` under [`translations`](https://swish.app/docs/guides/customize/translations), per language.

## Sign in prompt on Saves and Lists

Signed-out shoppers now see the sign-in callout on the **Saves** and **Lists** views, not just Home. It sits above the content, so a guest's own saved items and lists stay visible underneath — these are the two views where a shopper has something to lose by staying a guest, and until now neither said so.

Each of the three views has its own **Message** field under **Views → Home / Saves / Lists → Sign in**, so you can make a different case in each place — "Sign in to keep your saves on every device" on Saves, something else on Lists. Leave a Message empty and that view shows the default copy, translated into every [supported language](https://swish.app/docs/guides/customize/translations.md). The section's eye toggle hides it from that view alone.

In code the setting is `options.message` on the `sign-in` section of `ui.home`, `ui.saves` and `ui.lists`; a `t:`-prefixed value resolves as a translation key, so your own copy can be translated too.

## Item count stays put while it refreshes

`swish.state.itemCount()` and `useSwishItemCount()` used to report `loading: true` on every refetch, so a badge that hid itself while loading would blink empty each time a customer saved or unsaved. `loading` is now the first fetch only. Mid-session refreshes set `refetching: true` instead, and the last known count stays on the signal until the new number lands.

## Prices follow your currency formatting

If your store hides decimals under **Settings → General → Currency formatting**, Swish now hides them too — on product cards, the product detail view, quick buy, order totals, order line items and price-drop notifications. Previously every price showed cents regardless, so a storefront reading `$11,500` had a drawer reading `$11,500.00`.

Swish reads the decimal part of your format only. The currency symbol, where it sits and the thousands separator still come from Swish's own per-currency formats, so a format like `$ {{amount_no_decimals}}` renders `$11,500` rather than `$ 11,500`.

Headless setups pass this themselves as [`context.moneyFormat`](https://swish.app/docs/references/sdk/configuration); on Shopify themes the app embed handles it.

## Turn off quick buy on product cards

**Global → Preferences → Product cards** has a new **Show quick buy button** switch. Turn it off and the floating cart button disappears from every product card — the saves grid, both list detail views, product recommendations and featured collections — while **Add to cart** on the product detail view stays exactly as it is.

Until now the only way to remove those buttons was the `addToCartButton` transform, which is per product rather than per view, so switching it off took the product detail button with it. Views that have their own **Show quick buy button** toggle can still hide it in just that view; they can't show it while the global switch is off.

In code the setting is `ui.productCard.showQuickBuyButton: boolean`.

## The wishlist page says "Wishlist"

Your standalone wishlist page used to head its saved items **All Items** — multi-list wording on a page that holds one wishlist. It now reads **Wishlist**, translated into every built-in language (**Wunschliste**, **Toivelista**, **Список желаний**, **Önskelista**, **Lista życzeń**). Opening the page for a specific list still shows that list's own name.

Want different wording? Override `drawer.listDetail.allItems` under [`translations`](https://swish.app/docs/guides/customize/translations), per language.

## Require sign-in to save, or to open Swish at all

Saving works without an account by default and still does — but a store that treats a saved item as the start of a customer relationship can now ask for sign-in first. `features.signInRequirement` takes `"save"` to gate saving, or `"view"` to gate opening Swish.

A signed-out shopper who hits a gated action doesn't get silence. Swish runs the same `initiate:sign-in` flow as the drawer's account button — it confirms first, then hands off to your store's sign-in page. What they were doing travels with them as a return URL — the product, variant, quantity and tags of the save they came for — so it happens on the way back instead of being lost to the redirect.

The two settings gate different halves. `"save"` covers the shopper's own save and nothing else: the drawer stays browsable, and an item saved automatically when a line is removed from the cart never prompts. `"view"` is the stricter one — anything a shopper taps to open Swish, whether a header button, a `#swish` link or any `view:` intent, goes to sign-in instead, including the redirect to the classic wishlist page. Saving from a product page still works for guests under it. One way in stays open: the standalone wishlist page mounts from its own URL rather than from an intent, so a signed-out shopper who navigates straight to it still sees their saved items.

In code the option is `features.signInRequirement: "none" | "save" | "view"`, defaulting to `"none"`. See the [Account guide](https://swish.app/docs/guides/features/account.md#sign-in-requirements) for the full description.

**v0.145.0**

## Sign in with Shop fix

Sign in with Shop now works reliably where it previously failed to sign customers in in some cases.

**v0.144.0**

## Swish opens from your account link

**Placements → Header** has a new **Account link** placement. Add it and your theme's account link opens Swish instead of the Shopify account page, keeping your theme's own icon and styling and optionally showing the customer's saved item count as a badge. **Opens** picks which view it goes to — point it at **Profile** and signed-out customers go to sign-in instead.

## Swish can open on any view, not just Home

**Global → Preferences → Behaviour** starts with a new **Default view** dropdown — **Home**, **Saves**, **Lists**, **Notifications**, **Orders** or **Profile** — so a store running Swish as a pure wishlist can put Saves in front of every shopper without touching its theme. `view:home` now means "open Swish" rather than "open the Home view", so a header button, a `swish-intent="view:home"` button, a `#swish` link and a `?swish-intent=view:home` URL all land on your default view as they are. It is also the drawer's root: Back from a product, a list or any other view returns to it instead of to Home, and the save toast's **View** action follows it too.

The Home view stays reachable from the footer **Home** tab. In code the setting is `ui.defaultView: "home" | "saves" | "lists" | "notifications" | "orders" | "profile"`.

## Phone number and SMS consent on the Profile view

The Profile view now has an editable **Phone** field, and the **SMS** toggle under **Marketing consent** is always shown rather than appearing only for shoppers who already had a number on file. A number entered alongside the toggle is saved in the same request, so a shopper can add their phone and opt in at once. Numbers are expected in international format (`+353871234567`), and switching **SMS** on without a number asks for one instead of failing silently. A number already saved on the account can't be removed from this view yet.

## Button width and height can use px or %

A placement's **Design → Width** and **Height** now take a number with a switchable unit, so a Save button can be a fixed **300px** or stretch to **100%** of its container. Leave either blank to keep the button's own size. In code the design value carries its unit as a string (`"300px"`, `"100%"`), and a bare number is still read as pixels, so existing configs keep working.

## One list instead of many

**Global → Preferences → Behaviour** gained an **Allow multiple lists** setting, on by default. Turn it off and Swish stops offering list management while keeping the drawer: saving still shows its toast, but the toast's action opens your **Default view** instead of the list picker, unsaving skips the picker, and the list sections on **Home** and **Saves** plus the **Lists** tab in the drawer footer are hidden. **For you** lists are unaffected — they are smart lists with no create-or-manage actions.

In code the setting is `ui.wishlist.multipleLists: boolean`.

## Classic wishlist page, instead of the drawer

**Global → Preferences → Behaviour → Wishlist experience** is a new dropdown: **Drawer** (unchanged) or **Classic Wishlist page**, which replaces the drawer entirely. Anything that would have opened the drawer — a header button, a `#swish` link, any `view:` intent — sends the shopper to your standalone wishlist page at `context.routes.listDetailPagePath` (`/apps/wishlist` unless you set it), carrying a `list-id` through when one is given. Multi-list is off with it, so the save toast no longer offers **Add to list** and unsaving from a save button no longer asks which list to remove from — your **Confirm before unsaving** preference still applies.

In code the setting is `ui.wishlist.experience: "drawer" | "classic-page"`.

## Save for later can go to the wishlist

**Global → Preferences → Behaviour → Save cart line** gained a **Save destination** setting. Leave it on **Saved from cart** for the current behaviour, or pick **Wishlist** to have items removed from the cart saved as ordinary wishlist items — a "move to wishlist" instead of a separate list. Picking **Wishlist** hides the now-empty **Saved from cart** section on Home, and the **For you** section on Saves along with **Recently viewed**.

## Custom CSS can now style the button itself

A placement's **Custom CSS** is still applied to the storefront page — so the documented `[swish-placement-element="…"]` selector and parent selectors like `.product-card:hover [swish-placement-element="…"]` keep working — but it is now **also** adopted into the button's shadow DOM. That means `:host` and the component's internal selectors (such as `.button`) reach the button itself, which page-level CSS can't.

Per-placement **Breakpoint** overrides have been removed. Existing thresholds are preserved on save, so nothing you configured previously is lost.

## Save button settings

Save buttons now have their own settings card under **Global → Save button**. It sets the icon (heart, bookmark or your own SVG) and, for the unsaved and saved states separately, the icon thickness, size, fill and outline colour plus the background behind it. Those values are the defaults for every save button on your storefront — product pages, collection cards and the header link — and each placement can still override them in its own **Design** section.

Save buttons no longer take their look from the standard button's style, fill and size. Those selects still win when you set one, but they now default to *Inherit*. A save button created through `swish.ui.createElement("save-button")` inherits the same way: it renders with your Save button settings instead of the solid primary button it used to get — and with a transparent background until you set those settings. Save buttons also render their own `save` icon id rather than the shared `heart`, so restyling the save button no longer changes every other heart in the UI.

## Save buttons find their product

A placed save button resolves the product it acts on from the element it is attached to. When that element carries no product reference — a card's image wrapper, or a product page's button row — it now looks further: a product-page button reads the page itself, and a collection-card button walks to that card's own product link. Buttons that previously rendered but did nothing when tapped now save the right item.

## Placement fixes

Each placement's generated stylesheet is now scoped to that placement even when it has no id, so placements no longer overwrite one another's layout CSS — previously a second placement could silently strip the first one's positioning. Overlay placements position against the element they are mounted into rather than its parent, so an anchor corner lands on the box you targeted. Per-placement saved background and text colours, and the corner radius, now apply to the storefront; they were previously accepted and ignored.

## Notifications honor "Link card to product page"

Notifications in the drawer now follow the same **Link card to product page** rule as product cards: with it on, tapping a notification navigates to the product page; with it off, it opens the in-drawer product view as before. Because a notification only carries a slimmed-down product, a transform that reads storefront-only fields (such as `product.metafields`) falls back to the in-drawer view for that row rather than failing.

## Header button gets a text label and its own Design section

The header button can now show a label beside the heart: turn **Icon only** off and set **Text**. Its new **Design** section sets what the button actually reads — icon size, weight, colour and spacing either side, plus the saved-count badge's colour, text colour, size, corner and offset — and a blank field inherits your **Save button** settings. The old `background`, `color` and `radius` tokens are gone; no header stylesheet ever read them, so they saved and published while rendering nothing. If you set them through `swish.placements.create`, use `iconColor` for the icon and `statusIndicatorBackground` for the badge.

Two badge fixes come with it: the badge no longer lands in the middle of the button when no icon size is set, and its text colour now applies. The badge is positioned from a corner of the button rather than relative to the icon, so a placement relying on the old icon-relative position may need its offset nudged.

## Hidden placements stay hidden

The eye control on the Placements list now works: a hidden placement stops rendering on your storefront and keeps all of its settings, so you can un-hide it later. It previously kept rendering.

## Mini menu links follow the shopper's language

The Home view's mini menu now offers four ready-translated link titles — Contact us, Shipping, Returns and FAQs — in every language Swish supports. Pick one and each shopper sees it in their own language; pick **Custom** and your own wording is shown to everyone exactly as you typed it. Links you have already set up keep the title you gave them; re-pick one from the dropdown to translate it.

**v0.142.0**

## Sign in with Shop in the sign-in callout

The sign-in callout now offers **Sign in with Shop** to buyers Shop already recognizes, so they can authenticate in one tap instead of being sent to the classic login page. Everyone else sees the existing sign-in button exactly as before.

## Shop Pay and reorder links point at your store again

The Shop Pay button on the product detail view and the "order again" links in order history built their URLs against a malformed host, so buyers who tapped them landed on a browser error page instead of your store. Both now resolve to the correct storefront domain, including on localized storefronts served from a locale path such as `/en-gb`.

**v0.141.0**

## Orders and profile load for every signed-in customer

Signed-in customers now see their orders and profile in the drawer even before saving their first item. Previously `swish.api.orders` and `swish.api.profiles.getProfile()` returned empty until the customer had a Swish profile.

## Enrich saved items with render-ready product data

Building a custom wishlist page previously meant loading saved items, then calling `swish.storefront.loadProduct()` once per item to fetch its Shopify product data - and reimplementing combined-listing handling yourself. The new `swish.storefront.loadResolvedProduct(args)` returns a single product in a render-ready shape, transparently resolving combined listings the same way the built-in Swish UI does.

To build a wishlist page, resolve your saved items concurrently - product fetches are cached individually (repeat views and products shared across lists come from cache) and concurrent cache misses are automatically coalesced into batched Storefront requests, so a `Promise.all` over your items doesn't pay one round-trip per item.

```js theme={null}
const { data: items } = await swish.api.items.list({ limit: 24 });
const entries = await Promise.all(
  items.map(async (item) => ({
    item,
    ...(await swish.storefront.loadResolvedProduct(item)),
  })),
);
// entries: [{ item, product, variant }, ...] - ready to render
```

See [Storefront API client](https://swish.app/docs/references/sdk/storefront.md#loadresolvedproductargs).

**v0.140.0**

## Reliable redirect after sign-in

The `initiate:sign-in` intent now normalizes its `returnTo` value to a same-origin relative path before redirecting to the customer login page. Shopify rejects an absolute `return_to`, so passing a full URL previously stopped customers from being sent back to where they started after signing in. Relative paths are unchanged, and cross-origin targets fall back to the storefront home page.

**v0.138.0**

## Translate variant option labels

Variant option labels (for example `Size` or `Color`) come from Shopify and were shown as-is. You can now translate or rename them per language by adding overrides under the `optionName.` prefix, followed by the exact option name from your Shopify admin (case-sensitive):

```js theme={null}
window.SwishOptions = {
  translations: {
    pl: {
      "optionName.Size": "Rozmiar",
      "optionName.Color": "Kolor",
    },
  },
};
```

The translated label applies everywhere it appears - the option header, the "select option" prompts, the variant selector, and product card variant labels. Without an override the original Shopify name is shown. See [Translations](https://swish.app/docs/guides/customize/translations.md#variant-option-labels).

## API client defaults to the 2026-07 version

The unpinned `@swishapp/api-client` entry point (and the types re-exported from `@swishapp/sdk`) now resolves to the `2026-07` API version, up from `2026-04`. Version-pinned imports like `@swishapp/api-client/2026-04` continue to work unchanged.

**v0.137.0**

## Polish language support

Swish now ships Polish (`pl`) translations. When a storefront's active language is Polish, the Swish UI — buy buttons, lists, notifications, orders, and profile — renders in Polish automatically, instead of falling back to English.

## Undo saving from cart

When `intents.saveCartLine.confirm` is disabled (the default), the feedback toast shown after a cart item is saved now includes an **Undo** action. Pressing it removes the item from the `saved-from-cart` smart list. Set `intents.saveCartLine.showFeedback` to `false` to disable the toast (and the undo action) entirely.

## `swish.api.smartLists.removeItem`

A new SDK method removes an item from a smart list:

```js theme={null}
await swish.api.smartLists.removeItem("saved-from-cart", itemId);
```

It accepts `"recently-viewed"` or `"saved-from-cart"` and returns the removed item.

## Configurable toast duration

A new `ui.toasts.autoHideDuration` option controls how long toast notifications stay visible before auto-dismissing, in milliseconds:

```js theme={null}
window.SwishOptions = {
  ui: {
    toasts: {
      autoHideDuration: 8000,
    },
  },
};
```

The default is `5000` (5 seconds), which now applies to all toast variants — compact toasts previously dismissed after 2 seconds. Set `0` to disable auto-dismiss.

**v0.136.0**

## Configurable thumbnail aspect ratio

A new `thumbnailAspectRatio` design token controls the image ratio for notification cards, toast notifications, and order item thumbnails. Set it under `ui.design.sizes`:

```js theme={null}
window.SwishOptions = {
  ui: {
    design: {
      sizes: {
        thumbnailAspectRatio: "4 / 5",
      },
    },
  },
};
```

The default is `1 / 1` (square), matching the previous behaviour.

## Global default for `linkToProductPage`

A new `linkToProductPage` transform lets you control whether product cards link to the PDP globally, or per product, without configuring each section separately.

```js theme={null}
window.SwishOptions = {
  transforms: {
    linkToProductPage: ({ product }) => product.productType !== "Gift Cards",
  },
};
```

Return `true` to make cards navigate to the PDP; return `false` (the default) to keep the in-drawer product view behavior. The transform is the global fallback — section-level `linkToProductPage` options still take precedence when set.

`linkToProductPage` is also now exposed as a configurable option on all three product card sections (`all-items-grid`, `list-items-grid` in the drawer, `list-items-grid` on the standalone list page) so all views are consistent.

## List detail page mounts on `#wishlist-page`

The list detail page now mounts by replacing a `#wishlist-page` host element on init. On Shopify themes the `/apps/wishlist` app proxy page provides this host automatically; headless storefronts (Hydrogen, Next.js, or custom storefronts) should add `<div id="wishlist-page"></div>` themselves. See [List detail page](https://swish.app/docs/guides/customize/views/list-detail-page.md).

**v0.135.0**

## Placements API moved to `swish.placements`

The imperative placements API now lives at `swish.placements`, alongside the other top-level SDK namespaces (`swish.intents`, `swish.query`, …), instead of under `swish.dom`:

* `swish.dom.createPlacement(spec)` → `swish.placements.create(spec)`
* `swish.dom.placements` → `swish.placements.list`

Update any imperative placement calls accordingly. The declarative `placements` option passed to `createSwish({ ... })` is unchanged.

## Built-in translations for more languages

Swish now ships with built-in translations for German, Finnish, Russian, and Swedish, in addition to English. Swish loads the locale matching your storefront's active language and falls back to English when a language isn't built in.

Toast notifications and other SDK-triggered feedback are now localized too, so the entire UI follows the active language. You can still override any string per language. See [Translations](https://swish.app/docs/guides/customize/translations.md) for the full key reference, or email [support@swish.app](mailto:support@swish.app) to request another language.

**v0.134.0**

## Combined listings in the variant selector

The variant selector now handles [Shopify combined listings](https://help.shopify.com/manual/products/combined-listings). When a customer opens it on a child product, Swish loads the parent in the background and renders the full set of options across all siblings. As the customer's selections resolve to a variant on a different child, the active product switches automatically.

Merchants opt in via the new `combinedListingParent` transform - typically by storing the parent's GID on each child product as a metafield:

```js theme={null}
window.SwishOptions = {
  metafields: {
    product: ["custom.combined_listing_parent"],
  },
  transforms: {
    combinedListingParent: ({ product }) =>
      product.metafields.custom.combined_listing_parent ?? false,
  },
};
```

## Hide options in the variant selector

A new `hideOption` transform hides individual options from the variant selector. The option is locked to its first-available value, and the remaining visible options still reflect availability against that value. Common use: combined listings where one option (e.g. `"Color"`) drives the underlying product - hiding it prevents customers from accidentally switching products mid-flow, especially when saving from a collection card.

```js theme={null}
window.SwishOptions = {
  transforms: {
    hideOption: ({ option, product }) => {
      const isCombinedListing =
        !!product.metafields.custom?.combined_listing_parent;
      return isCombinedListing && option.name === "Color";
    },
  },
};
```

See [Combined listings end-to-end](https://swish.app/docs/guides/customize/transforms.md#combined-listings-end-to-end) for the full pattern.

## Metafields are now a nested record

**Breaking.** `product.metafields` and `variant.metafields` previously surfaced as an array of `{ namespace, key, value }` objects. They now expose a nested record keyed by namespace, then by key:

**Before:**

```js theme={null}
const value = product.metafields?.find(
  (m) => m?.namespace === "custom" && m?.key === "material",
)?.value;
```

**After:**

```js theme={null}
const value = product.metafields.custom?.material;
```

Every namespace you declare in `metafields.product` or `metafields.productVariant` is pre-seeded (as `{}` when no values exist), so you only need optional chaining at the namespace level for namespaces you didn't declare.

## More product-card knobs on list and saves sections

Filled the inconsistencies in `list-items-grid` and `all-items-grid` so the same product-card options are reachable from every section that renders cards.

* **`showVariantTitle`** — now configurable in both `list-items-grid` variants (drawer + standalone page). Was hardcoded `true` before. Default stays `true`.
* **`showBuyButton`** — now available on the drawer's `list-items-grid` and on `all-items-grid` (`saves` view). The standalone page already had it. Default `false` in the new locations to preserve current behavior. Distinct from `showQuickBuyButton` (the floating button on the card image); `showBuyButton` controls the solid button beneath the card.
* **`hideCompareAtPrice`** — new across all three schemas. Hides the strike-through compare-at price on cards so only the active price renders. Default `false`. Pairs naturally with the [`productPrice` transform](https://swish.app/docs/guides/customize/transforms) for stores that suppress original pricing entirely.
* **`linkToProductPage`** — now surfaced on all three schemas. Defaults preserve current behavior: `true` on the standalone list page (hard navigation to PDP), `false` in the drawer's `list-items-grid` and in `all-items-grid` (drawer cards open the in-drawer product view via intent). Flip to `true` in drawer contexts when you want native navigation instead.

```js theme={null}
ui: {
  saves: {
    sections: [
      {
        type: "all-items-grid",
        options: {
          showVariantTitle: true,
          showBuyButton: true,
          hideCompareAtPrice: true,
          linkToProductPage: true,
        },
      },
    ],
  },
}
```

**v0.133.0**

## New `<swish-save>` and `<swish-drawer>` custom elements

The HTML attribute API for declarative save buttons and drawer triggers has been replaced with two custom elements. Markup is shorter, state lives on the element where CSS can target it via standard `[state="..."]` selectors, and labels compose through nested children instead of per-state attributes.

**Removed:** `swish-item-state`, `swish-item-label`, `swish-item-label-unsaved/-saved/-saving`, `swish-item-match`, `swish-item-context`, `swish-item-count`, `swish-product-id`. **Removed:** `swish.dom.createElementLocator` from the public API.

**Before:**

```html theme={null}
<button
  type="button"
  swish-item-state
  swish-item-context="location"
  swish-item-match="auto"
>
  <span swish-item-label>Save</span>
</button>

<button type="button" swish-intent="view:home">
  Wishlist <span swish-item-count></span>
</button>
```

**After:**

```html theme={null}
<swish-save context="pdp">
  <button type="button">
    <span data-swish-when="unsaved">Save</span>
    <span data-swish-when="saved,saving" hidden>Saved</span>
  </button>
</swish-save>

<swish-drawer view="home">
  <button type="button">
    Wishlist
    <span data-swish-bind="count" data-swish-when="has-saves" hidden>0</span>
  </button>
</swish-drawer>
```

See the [Elements reference](https://swish.app/docs/references/sdk/elements.md) for the full attribute set and styling guide.

**v0.132.0**

## Configurable list detail views

The list detail view (the screen that opens when a customer taps a list) is now configurable like every other view, with sections under `ui.listDetail`. The same section type also drives the standalone full-page list detail under `ui.listDetailPage`, so both surfaces share one schema.

```js theme={null}
ui: {
  listDetail: {
    sections: [
      {
        type: "list-items-grid",
        options: {
          showVariantOptionNames: true,
          enableVariantChange: true,
        },
      },
    ],
  },
  listDetailPage: {
    sections: [
      {
        type: "list-items-grid",
        options: {
          desktopColumns: 3,
          showBuyButton: true,
          enableVariantChange: true,
        },
      },
    ],
  },
}
```

The standalone page exposes a couple of extra knobs the in-drawer view doesn't need - `desktopColumns` for the grid width and `showBuyButton` for whether each card carries a buy button.

If you were configuring the standalone page through `ui.fullpage.listDetail.*`, move those options under `ui.listDetailPage.sections[0].options`. The old shape no longer works.

## Custom price display per product

A new `productPrice` transform lets you replace or hide the price per product, based on tags, metafields, or anything else on the product. Common uses: "Call for price" on B2B SKUs, hiding price entirely on made-to-order items, or pulling a custom label from a metafield.

```js theme={null}
metafields: {
  product: ["swish.price_label"],
},
transforms: {
  productPrice: ({ product }) => {
    if (product?.tags.includes("b2b")) {
      return false;
    }
    const label = product?.metafields?.find(
      (m) => m?.namespace === "swish" && m?.key === "price_label",
    )?.value;
    return label ?? undefined;
  },
}
```

Return `undefined` to render the default price, `false` to hide the price entirely, or a string to replace it. The transform runs everywhere a price is shown except notifications and order line items. Custom strings inherit the default price typography and pick up `.product-price-custom` / `.price-custom` classes for further styling.

## Renamed product transforms

The product-scoped transforms now share a `product` prefix so the context is unambiguous next to the existing `variantTitle` and `optionValues`:

* `title` -> `productTitle`
* `description` -> `productDescription`
* `badges` -> `productBadges`
* (new) `productPrice`

Signatures are unchanged. Rename the keys in your `transforms` config when you move to the new version.

**v0.131.0**

## Smoother, faster save buttons

Save buttons across the storefront feel snappier in this release. Pages with lots of save buttons - collection grids, search results, the drawer - now make far fewer API requests, and the button no longer flashes back to "unsaved" for a split second while a save or unsave is in flight.

## Product-level save state on collection cards

Save buttons now have an optional `match` setting. With `match="product"`, the button shows as saved whenever the customer has saved any variant of the product - useful on collection cards that render a default variant but should reflect the product's overall save state.

```html theme={null}
<button
  type="button"
  swish-item-state
  swish-item-match="product"
  data-product-id="8234567890123"
  data-variant-id="44234567890123"
>
  Save
</button>
```

The same option is available on the SDK save button and the `useSwishItem` React hook. The default behavior is unchanged.

## React: simpler save state for headless stores

`useSwishItem` is easier to use on headless and Hydrogen stores. You can pass product, variant, or item IDs straight from your data layer and the hook handles the rest, including callbacks for tracking saves and unsaves.

```tsx theme={null}
useSwishItem({
  productId,
  onSave: ({ item }) => analytics.track("item_saved", { id: item.id }),
});
```

## Theme embed tracks the latest SDK

The Shopify theme app embed now loads the latest stable SDK by default. Stores pick up new releases automatically without re-saving the embed block.

**v0.130.0**

## Placements

A new SDK feature for inserting Swish elements (Save button, drawer trigger) anywhere in your storefront without editing theme templates. Placements use DOM observation to mount automatically when matching elements appear, and survive theme re-renders.

Declare placements upfront in `SwishOptions`:

```js theme={null}
placements: [
  {
    id: "card-save-overlay",
    target: { selector: ".product-card" },
    mount: { at: { query: ".product-card__image" }, mode: "append" },
    element: { name: "save-button" },
    layout: { type: "overlay", anchor: "top-right", offset: { x: 8, y: 8 } },
  },
  {
    id: "pdp-save-inline",
    target: { selector: ".product-form" },
    mount: { at: { query: 'button[type="submit"]' }, mode: "after" },
    element: { name: "save-button" },
    layout: { type: "inline" },
  },
];
```

Or create them imperatively with `swish.dom.createPlacement(spec)`. Each placement returns `{ mount, unmount }` so you can control them programmatically. See the [Placements reference](https://swish.app/docs/references/sdk/placements.md) and the [customization guides](https://swish.app/docs/guides/customize/placements/overview.md) for product detail, collections, and header placements.

## Customer profile

The drawer now includes a full profile view where customers can edit their account details, manage addresses, set notification preferences, and add their birthdate.

The view uses the new `profile-form` section under `ui.profile.sections` and the `view:profile` intent. The form integrates with both new and classic Shopify customer accounts and respects locale settings for address formats.

## Featured collection on home

A new `featured-collection` home section displays products from a Shopify collection in a horizontal slider on the drawer home screen.

```js theme={null}
ui: {
  home: {
    sections: [
      { type: "sign-in" },
      { type: "featured-collection", handle: "best-sellers" },
      { type: "saved-items" },
      { type: "lists" },
    ],
  },
}
```

The `handle` is the collection handle from Shopify. Products are loaded via the Storefront API.

## Drawer header customization

The drawer header now supports a few new options to fit a wider range of theme designs:

* `useCloseButton` - replace the account button with a close button (useful for mobile/full-screen drawers)
* `accountButton` - toggle the account button in the header
* `notificationButton` - toggle the notification icon in the header
* `logo.url` and `logo.altText` - display a logo image in the header

```js theme={null}
ui: {
  drawer: {
    sections: [
      {
        type: "drawer-header",
        options: {
          logo: { url: "/logo.svg", altText: "My Store" },
          useCloseButton: true,
        },
      },
      { type: "drawer-footer" },
    ],
  },
}
```

## Hide Add to Cart with a transform

A new `addToCartButton` transform lets you conditionally hide the Add to Cart button per product, for example for made-to-order items, pre-orders that aren't yet available, or B2B-only SKUs.

```js theme={null}
transforms: {
  addToCartButton: ({ product }) => !product.tags.includes("made-to-order"),
}
```

Return `false` to hide the button on a given product; the default is `true`.

## Notification indicator on the main menu

The notification icon in the drawer footer now shows an alert dot when there are unread notifications, making it easier for customers to see updates without opening the drawer.

## Bulk add to list after in-drawer creation

When a customer creates a new list from inside the drawer, the bulk add-to-list screen now opens automatically so they can populate the new list in one flow instead of returning to it manually.

## Auto-save from cart fixes

Cart change tracking is now keyed on Shopify line item keys instead of variant IDs, fixing a class of duplicate-save and missed-save issues on stores that attach custom line attributes. Removed items are tracked deterministically across consecutive cart updates.

**v0.122.0**

## Gallery view for lists

Lists can now be displayed in a gallery layout with a new `GalleryCard` component that renders list items in a visual grid. The gallery view is available as an alternative to the standard list layout in both the lists overview and list detail views.

## Intent renaming

Navigation intent names have been updated from `open:*` to `view:*` to better reflect their purpose. For example, `open:drawer` is now `view:drawer`, and `open:list` is now `view:list`. Update any custom intent references in your SDK configuration.

## Quick Buy and Save flows

A new save-first workflow lets customers save an item to their list immediately, then update the variant afterward. This includes a quantity input modal for add-to-cart confirmation with variant selection. The Quick Buy flow also supports adding items to cart directly from the list.

Configuration options:

* `quickBeforeVariantPrompt` - enable the save-first, select-variant-later flow
* Quantity input modal appears when adding to cart from a list item

## Product option improvements

Single-value product options are now hidden automatically to reduce clutter in variant selectors. Variant selection logic has been improved to better determine the selection journey based on product attributes and metafields.

## Custom content blocks

You can now add custom content blocks to the Swish drawer using the `contentBlocks` option, enabling merchant-defined content such as banners, messaging, or promotional sections within the drawer interface.

## Empty state improvements

Empty states across the drawer have been redesigned with clearer calls to action and better visual presentation when a customer has no saved items or lists.

## Clickable section headers

Section headers on the drawer home screen are now clickable, allowing customers to tap the header text to navigate directly to the full section view.

## Configurable menu text

The menu button text is now configurable through SDK settings via the `menuText` option, allowing merchants to customize the label (e.g., changing "Wishlist" to "Favorites" or "Saved Items").

## Admin PDP visit

Admins can now navigate directly to a product's storefront page when selecting an item from a list, making it easier to verify product details.

**v0.120.0**

## Smart lists

A new system for auto-generated, read-only lists that populate based on customer behavior. Smart lists appear as horizontal product rows on the drawer home screen with a "View all" link to a full paginated detail view.

**Recently Viewed** - Automatically tracks products a customer has viewed on the storefront. Data is tracked server-side and fetched via the Smart Lists API.

**Saved from Cart** - Items removed from the Shopify cart are automatically saved to this smart list. Read-only - items are managed automatically based on cart activity.

## Auto-save from cart

When a customer removes an item from their Shopify cart, it can automatically be saved to their list.

The SDK intercepts Shopify Ajax API responses to `/cart/change` endpoints. When items are removed, the SDK extracts the product ID, variant ID, quantity, title, and image from the cart change event, and saves the item with `source: "cart"`.

You can optionally show a confirmation toast before saving ("Save this for later?") and a success toast after ("We've saved this for you"). Both can be toggled independently.

Configuration options:

* `intents.removeCartLine.saveCartLine` - enable or disable the feature (default: off)
* `intents.saveCartLine.confirm` - show confirmation before saving (default: off)
* `intents.saveCartLine.showFeedback` - show success toast (default: on)
* `intents.saveCartLine.afterComplete` - optional callback after saving completes

## List sharing

Customers can share their lists via a public link.

* Share is a permanent action in the list menu and is also accessible from the list detail view header
* If the list is not already public, access is automatically set to public before sharing
* On mobile, uses the native share dialog. On desktop, copies the share URL to the clipboard
* Shows a "Link copied" toast as feedback

Recipients can open the link and view the list in read-only mode with no account required.

## Translation system with custom overrides

A new localization system that supports translating all UI text, with the ability to override any string.

Base locale files are loaded dynamically from CDN based on the Shopify storefront language. Translation keys use dot notation (e.g. `drawer.home.emptyCalloutTitle`), with support for `{{variable}}` interpolation and pluralization via `One` / `Other` key suffixes.

You can override any translation key by passing a `translations` object when initializing the SDK. Overrides are applied per language after the base locale loads - for example, changing "Add to Cart" to "Add to Bag", or "Save" to "Add to Wishlist".

## Custom product detail transforms

You can customize how product data is displayed using transform functions passed through SDK options.

Available transforms:

* **title** - customize the product title (e.g. append a brand name)
* **description** - modify the product description
* **variantTitle** - customize how variant titles are displayed
* **badges** - add, remove, or modify product badges (e.g. "New Arrival", "On Sale")
* **optionValues** - filter, reorder, or modify product option values before rendering (e.g. hide unavailable sizes)

Each transform is a function that receives the product or variant data and returns the modified value. If no transform is configured, the original data is shown as-is. Transforms are applied at render time across all UI surfaces including product cards, product detail, and quick buy.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.