> ## 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.

# Transforms

> Customize how product, variant, and option data is displayed in the Swish UI.

Transforms are functions that intercept product, variant, and option data right before it is rendered. Each function receives the relevant entity and returns the transformed value. Pass them under `transforms` in your `SwishOptions`.

```js theme={null}
window.SwishOptions = {
  transforms: {
    productTitle: ({ product }) => product.title.toUpperCase(),
    productBadges: ({ product, defaultBadges }) => {
      if (product.tags.includes("new")) {
        return [...defaultBadges, { id: "new", label: "New" }];
      }
      return defaultBadges;
    },
  },
};
```

Keys are prefixed with the entity they operate on (for example `productTitle` vs `variantTitle`) so the context is always unambiguous. Each transform falls back to the default behavior listed below when omitted.

| Transform | Signature | Default behavior |
| - | - | - |
| `productTitle` | `({ product }) => string` | Returns `product.title`. |
| `productSubtitle` | `({ product, variant }) => string \| undefined` | Returns `undefined` (no subtitle). |
| `productDescription` | `({ product }) => string` | Returns `product.descriptionHtml`. |
| `productBadges` | `({ product, variant, defaultBadges }) => (string \| Badge)[]` | Returns `defaultBadges` unchanged. |
| `productPrice` | `({ product, variant }) => string \| false \| undefined` | Returns `undefined` (default price rendering). |
| `productImage` | `({ product, variant, defaultImage }) => Image \| undefined` | Returns `defaultImage` (variant's image, else featured image). |
| `addToCartButton` | `({ product }) => boolean` | Returns `true` (cart buttons shown). |
| `linkToProductPage` | `({ product }) => boolean` | Returns `false` (in-drawer product view). |
| `variantTitle` | `({ variant }) => string` | Returns `variant.title`. |
| `optionValues` | `({ option, optionValues }) => OptionValue[]` | Returns option values unchanged. |
| `combinedListingParent` | `({ product }) => boolean \| string` | Returns `false` (not a combined listing). |
| `hideOption` | `({ option, product }) => boolean` | Returns `false` (option is visible). |
| `recommendations` | `({ product, defaultRecommendations }) => Ref[] \| Promise<Ref[]>` | Returns `defaultRecommendations` unchanged. |

> [!TIP]
>
> Transforms run for every render, so keep them pure and inexpensive. If you need
> product metadata that is not already on `product`, declare it under
> [metafields](https://swish.app/docs/guides/customize/metafields.md) so it is preloaded rather than
> fetching it here. `recommendations` is the one exception - it runs once per
> product view and may return a promise, so it can await a request.

## Product title

Replace the displayed product title without mutating the underlying Shopify data.

```js theme={null}
window.SwishOptions = {
  transforms: {
    productTitle: ({ product }) => product.title.replace(/\s*-\s*Sample$/, ""),
  },
};
```

## Product subtitle

Add a line under the product title on product cards, in the drawer and on the Wishlist page. The transform receives the resolved `product` and the card's `variant` (when there is one). Return the text to show, or `undefined` for no line. The line sits between the title and the price in the subdued text colour, and truncates to a single line.

Show the colour of a product from a [combined listing](#combined-listings-end-to-end) once [`hideOption`](#hide-option) has taken Color out of the option select. A product of a combined listing carries its colour on its variants rather than in its own `options`, so read the variant first:

```js theme={null}
window.SwishOptions = {
  transforms: {
    hideOption: ({ option }) => option.name === "Color",
    productSubtitle: ({ product, variant }) =>
      variant?.selectedOptions.find((option) => option.name === "Color")
        ?.value ??
      product.options.find((option) => option.name === "Color")
        ?.optionValues[0]?.name,
  },
};
```

Show a value from a product metafield, declared under [metafields](https://swish.app/docs/guides/customize/metafields.md) so it is preloaded with the product:

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

The subtitle shows on product cards only. The product detail view, dialogs such as quick buy and Notify Me, and toasts show the title alone.

## Product description

Replace the description HTML rendered on the product detail view. Return any HTML string - it is rendered inside the description block.

```js theme={null}
window.SwishOptions = {
  transforms: {
    productDescription: ({ product }) =>
      product.metafields.custom?.short_description ?? product.descriptionHtml,
  },
};
```

## Product badges

Decorate product cards and the product detail view with badges. The transform receives `defaultBadges`, the badge Swish would render by default, and returns the badges to display. `defaultBadges` holds at most one badge: the first match in the [badge priority list](https://swish.app/docs/references/sdk/configuration.md#product-badges) set under **Global → Preferences → Product badges**. The built-in **Sale** and **Sold out** badges arrive as strings unless the merchant gave them colours.

A `Badge` can be a plain string or an object:

```ts theme={null}
type Badge =
  | string
  | {
      id: string;
      label: string;
      style?: Record<string, string | number>;
    };
```

```js theme={null}
window.SwishOptions = {
  transforms: {
    productBadges: ({ product, defaultBadges }) => {
      const badges = [...defaultBadges];
      if (product.tags.includes("new")) {
        badges.unshift({
          id: "new",
          label: "New",
          style: { background: "#16a34a", color: "white" },
        });
      }
      return badges;
    },
  },
};
```

Return an empty array to hide all badges, or omit specific defaults to suppress them.

## Product price

Replace or hide the price on product cards and the product detail view based on product attributes. The callback receives the resolved `product` and (when applicable) `variant`, and returns one of:

* `undefined` to render the default price.
* `false` to hide the price entirely.
* A `string` to replace the price with custom text (for example, `"Call for price"`).

The transform runs for every price display except notifications and order line items.

```js theme={null}
window.SwishOptions = {
  metafields: {
    product: ["swish.price_label"],
  },
  transforms: {
    productPrice: ({ product }) => {
      if (product?.tags.includes("b2b")) return false;
      return product?.metafields.swish?.price_label;
    },
  },
};
```

Replacement text inherits the default price typography and is wrapped in a container with the modifier classes `.product-price-custom` and `.price-custom` for additional styling.

## Product image

Choose the image a product card shows. The transform receives the resolved `product`, the card's `variant` (when there is one), and `defaultImage` - the image the card shows without the transform: the variant's image, else the product's featured image. Return an image object with at least a `url`, or `undefined` to keep `defaultImage`.

Show the second image uploaded to the product. A product with only one image keeps its default:

```js theme={null}
window.SwishOptions = {
  transforms: {
    productImage: ({ product }) => product.images.nodes[1],
  },
};
```

Pick an image by its alt text, for example a flat lay. `product.images` holds the product's first 20 images:

```js theme={null}
window.SwishOptions = {
  transforms: {
    productImage: ({ product }) =>
      product.images.nodes.find((image) => image.altText === "Flat lay"),
  },
};
```

Show an image URL stored in a product metafield:

```js theme={null}
window.SwishOptions = {
  metafields: {
    product: ["custom.card_image_url"],
  },
  transforms: {
    productImage: ({ product }) => {
      const url = product.metafields.custom?.card_image_url;
      return url ? { url } : undefined;
    },
  },
};
```

> [!NOTE]
>
> Store the URL in a **URL** or **Single line text** metafield. A **File**
> metafield reaches the transform as a `gid://shopify/MediaImage/...` ID rather
> than a URL, so it can't be shown. Images on `cdn.shopify.com` are resized for
> the card; images on other hosts load at full size.

The transform applies to product cards in the drawer, on the Wishlist page, and in the product rows of the Home and product detail views. The product detail image slider and the images in dialogs such as quick buy and Notify Me keep the default.

### Let customers switch images

Turn on **Show image toggle** for the grid in **Views → Saves** or **Views → List detail** to put a switch above the grid, as on a collection page that switches between model shots and flat lays. In code, it's `showImageToggle` on the [`all-items-grid`](https://swish.app/docs/guides/customize/views/saves#all-items-grid) and [`list-items-grid`](https://swish.app/docs/guides/customize/views/list-detail#list-items-grid) sections.

* Cards open on their default image. Turning the switch on shows the image `productImage` picks on every card, on both grids.
* The choice lasts until the customer's browser session ends.
* The switch appears only when `productImage` is set. A grid without the switch always shows the transform's pick.

The switch reads **Alternate view**. Rename it with the `productCard.imageToggle` [translation key](https://swish.app/docs/guides/customize/translations.md), for example to "Flat lay".

## Add to cart button

Show or hide Swish's cart buttons per product: **Add to cart** on the product detail view, the buy button beneath product cards, and the quick-buy button on card images.

This transform defaults to `true`, so cart buttons render wherever their section option allows. Reach for it when a product shouldn't be bought from Swish at all - made-to-order items, pre-orders, B2B-only SKUs.

Turn them off everywhere:

```js theme={null}
window.SwishOptions = {
  transforms: {
    addToCartButton: () => false,
  },
};
```

Or per product:

```js theme={null}
window.SwishOptions = {
  transforms: {
    addToCartButton: ({ product }) => !product.tags.includes("display-only"),
  },
};
```

The section options that control these buttons gate them independently - a button renders only when both its section option and this transform allow it. `showBuyButton` covers the solid button on the card grids; `showQuickBuyButton` covers the floating one on `all-items-grid`, on both List Detail grids, and on the product detail `complementary-products` / `related-products` carousels.

The two levels point opposite ways on purpose. This transform defaults to `true`, so a cart button is always available store-wide; every section option above defaults to `false`, so you choose the surfaces that actually show one.

> [!NOTE]
>
> One surface has no section option: **featured collection** rows on the Home
> view always render quick-buy, so there a button appears as soon as this
> transform allows it. To suppress it, return `false` from this transform for
> those products, or turn off the store-wide
> [`ui.productCard.showQuickBuyButton`](https://swish.app/docs/references/sdk/configuration#product-cards)
> switch.

This transform is per product, not per view, so it can't keep **Add to cart** on the product detail view while removing quick-buy from cards - returning `false` hides both. To do that, leave this transform alone and set [`ui.productCard.showQuickBuyButton`](https://swish.app/docs/references/sdk/configuration#product-cards) to `false`, which hides quick-buy on every card and leaves the product detail button alone.

## Link to product page

Control whether product cards link to the PDP globally, or per product. Return `true` to make the card navigate to the product page; return `false` to keep the default in-drawer intent behavior.

The transform decides only when [`ui.productCard.linkToProductPage`](https://swish.app/docs/references/sdk/configuration#product-cards) is unset. The customiser always sends that setting from its **Link card to product page** switch (**Global → Preferences → Product cards**), so on a store set up in the customiser, the switch decides and this transform does not.

```js theme={null}
window.SwishOptions = {
  transforms: {
    // Enable PDP linking globally, but disable it for gift cards
    linkToProductPage: ({ product }) =>
      product.productType !== "Gift Cards",
  },
};
```

> [!TIP]
>
> The standalone list detail page (`ui.listDetailPage`) always links its cards to
> the PDP, whatever this transform returns, since it has no in-drawer fallback.

## Variant title

Customize how a variant's title is rendered (for example, in the variant selector or saved item card).

```js theme={null}
window.SwishOptions = {
  transforms: {
    variantTitle: ({ variant }) =>
      variant.title === "Default Title" ? "" : variant.title,
  },
};
```

## Option values

Filter or reorder the option values shown in the variant selector. The transform receives the option (e.g. `Size`) and its full list of values, and returns the values to display.

```js theme={null}
window.SwishOptions = {
  transforms: {
    optionValues: ({ option, optionValues }) => {
      if (option.name !== "Size") return optionValues;
      const order = ["XS", "S", "M", "L", "XL"];
      return [...optionValues].sort(
        (a, b) => order.indexOf(a.name) - order.indexOf(b.name),
      );
    },
  },
};
```

Return a subset of values to hide options entirely.

## Combined listing parent

Tell Swish that a product is part of a Shopify [combined listing](https://help.shopify.com/manual/products/combined-listings) so the variant selector renders the correct set of options and switches between the underlying products as the customer makes their selection. Shopify does not expose any way to detect a combined listing from the Storefront API, so the merchant signals this via the transform.

Returns one of:

* `false` (default) - not part of a combined listing.
* `true` - part of a combined listing, but no parent product reference is available.
* A parent product GID (e.g. `"gid://shopify/Product/7345123456"`)

The recommended pattern is to reference the parent product on each child product in a metafield, and return it from the transform:

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

## Hide option

Hide a product option from the variant selector. Useful for combined listings where one option drives the underlying product (for example, "Color"). Hiding it prevents the customer from accidentally switching products mid-flow - especially when saving from a collection card, where switching products can cause an adjacent card's heart to toggle instead of the one the customer clicked.

The option is hidden only when a value can be auto-selected for it (from the product's `selectedOrFirstAvailableVariant`). If no value is available, the option is shown as usual and a warning is logged.

```js theme={null}
window.SwishOptions = {
  transforms: {
    hideOption: ({ option, product }) => {
      // Hide the "Color" option on products that are part of a combined listing
      const isCombinedListing =
        !!product.metafields.custom?.combined_listing_parent;
      return isCombinedListing && option.name === "Color";
    },
  },
};
```

The hidden option's value still constrains the rest, so the visible options correctly reflect availability against the locked value.

## Recommendations

Filter or reorder the products shown in the `complementary-products` and `related-products` sections of the [product detail view](https://swish.app/docs/guides/customize/views/product-detail.md). The transform receives the current product and the recommendations Shopify returned, and returns the list to render.

This is the only transform that may return a promise, so you can call your own recommendation service:

```js theme={null}
window.SwishOptions = {
  transforms: {
    recommendations: async ({ product, defaultRecommendations }) => {
      const response = await fetch(`/api/recommendations/${product.id}`);
      if (!response.ok) return defaultRecommendations;
      const { productIds } = await response.json();
      return productIds.map((id) => ({ id }));
    },
  },
};
```

Both `defaultRecommendations` and the values you return carry only `id` - the recommendations query fetches nothing else, and each card re-fetches its own product data. Return an empty array to render no recommendations.

```js theme={null}
window.SwishOptions = {
  transforms: {
    // Cap the list at 8, keeping Shopify's order
    recommendations: ({ defaultRecommendations }) =>
      defaultRecommendations.slice(0, 8),
  },
};
```

Filtering by product attributes isn't possible here - `defaultRecommendations` entries carry only `id`, so there are no tags or types to test. Cap, reorder, or replace the list with ids of your own instead.

> [!NOTE]
>
> If the transform throws or its promise rejects, Swish logs the error and falls
> back to the unfiltered Shopify recommendations rather than leaving the section
> in a loading state.

## Combined listings end-to-end

Combined listings most often pair both transforms: signal the parent with `combinedListingParent`, then hide the option that drives the product switch with `hideOption`. The customer picks from the remaining options against the locked value, and never accidentally jumps to a sibling product.

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

    hideOption: ({ product, option }) => {
      if (!product.metafields.custom.combined_listing_parent) return false;
      return option.name === "Color";
    },
  },
};
```

## Further reading

* [Configuration reference](https://swish.app/docs/references/sdk/configuration.md) for all SDK options
* [Metafields](https://swish.app/docs/guides/customize/metafields.md) to preload custom fields used inside transforms
* [Full defaults reference](https://swish.app/docs/guides/customize/defaults.md) for every option and its default value


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