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

# Elements

> Reference for the Swish declarative custom elements - <swish-save> and <swish-drawer>.

The SDK ships two custom elements that wrap a merchant-provided button and bind the [`swish.state`](https://swish.app/docs/references/sdk/state) signals declaratively. Drop them into Liquid (or any HTML) without writing JavaScript.

## `<swish-save>`

Wraps a merchant-provided save button. Click toggles the saved state for the product. The SDK reflects status on the element so CSS and conditional children can react.

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

### Inputs

| Attribute | Required | Description |
| - | - | - |
| `context` | Recommended | Where to resolve the product from. `"pdp"` parses from the current URL and watches for SPA navigation. `"product-card"` walks to the nearest `a[href*='/products/']` link and watches its href (so variant swaps on the link update the bound state). |
| `match` | Optional | How to interpret variant matching. One of `"variant"`, `"product"`, `"auto"`. Defaults to `"product"` when `context="product-card"` (so a default-variant ID in the link doesn't make "saved" depend on which variant happens to render), and `"auto"` everywhere else. See [`itemState` options](https://swish.app/docs/references/sdk/state#itemstatecontext-options). |
| `product` | Override | Product ID. When set, takes precedence over `context` - resolves the saved state from this ID directly with no DOM walking or URL parsing. Use when the rendered DOM doesn't reflect a single product (e.g. a static "recommend this item" tile). |
| `variant` | Override | Variant ID. Combine with `product` to scope state to a specific variant. |

Prefer `context` over explicit IDs - it reacts to URL changes (PDP SPAs) and href changes (variant pickers that update product card links) automatically. Set `product`/`variant` only when the DOM doesn't carry the product reference you need.

If none of the above are set, the SDK falls back to reading `data-product-id`, `data-product-handle`, `data-variant-id`, or `data-item-id` from the element itself.

### Outputs

| Attribute | Value | Notes |
| - | - | - |
| `state` | `"unsaved"` \| `"saving"` \| `"saved"` | Reflects the current item status. |
| `ready` | Boolean presence | Set once after first successful state resolution. Useful for opt-in pre-paint hiding (see [Styling](#styling)). |
| `aria-pressed` | `"true"` \| `"false"` | Mirrors `state === "saved"`. |

### Examples

> [tab] PDP
>
> The SDK reads the product from `window.location` and watches for SPA navigation.
>
> ```html theme={null}
> <swish-save context="pdp">
>   <button class="swish-save-button">
>     <span class="swish-save-icon">{% render 'icon-bookmark' %}</span>
>     <span class="swish-save-label" data-swish-when="unsaved">Save</span>
>     <span class="swish-save-label" data-swish-when="saved,saving" hidden
>       >Saved</span
>     >
>   </button>
> </swish-save>
> ```

> [tab] Collection card
>
> The SDK walks up the DOM to find the nearest product link (`a[href*='/products/']`) and watches its href for variant swaps.
>
> ```html theme={null}
> <div class="product-card">
>   <a href="{{ card_product.url }}">{{ card_product.title }}</a>
>   <swish-save context="product-card">
>     <button class="swish-save-button">
>       <span class="swish-save-icon">{% render 'icon-bookmark' %}</span>
>     </button>
>   </swish-save>
> </div>
> ```

The `context` attribute does double duty: it drives the resolver and gives CSS a hook for styling variants of the same element.

```css theme={null}
/* Style the same element differently based on context */
swish-save[context="pdp"] .swish-save-button {
  /* full-width PDP button */
}
swish-save[context="product-card"] .swish-save-button {
  /* icon-only overlay */
}
swish-save[context="product-card"] .swish-save-label {
  display: none;
}

/* React to state. `:is()` groups multiple states so the optimistic
 * "saving" window looks the same as the resolved "saved" state. */
swish-save:is([state="saved"], [state="saving"]) .swish-save-icon svg path {
  fill: currentColor;
}
swish-save[state="saving"] .swish-save-button {
  pointer-events: none;
}
```

## `<swish-drawer>`

Wraps a merchant-provided drawer trigger button. Click opens the drawer to the given view via the [intents](https://swish.app/docs/references/sdk/intents.md) system. The SDK reflects whether there are saved items and exposes the count for badge binding.

```html theme={null}
<swish-drawer view="home">
  <button class="my-drawer-button">
    <span data-swish-bind="count" data-swish-when="has-saves" hidden>0</span>
  </button>
</swish-drawer>
```

### Inputs

| Attribute | Required | Description |
| - | - | - |
| `view` | Optional | Which view to open on click. One of `"home"` (default), `"saves"`, `"lists"`, `"orders"`, `"notifications"`, `"profile"`. `"home"` opens the store's [`ui.defaultView`](https://swish.app/docs/references/sdk/configuration), which is the Home view unless you changed it. |
| `badge` | Optional | `"true"` renders a saved-count badge over the wrapped content. Use it when you're wrapping markup you can't edit - a theme's account icon - and so can't add a badge element yourself. |

Unknown `view` values log a warning and fall back to `"home"`.

`view="profile"` is the account entry point: a signed-in customer gets the profile view, a signed-out one goes straight to sign-in and returns to the page they left. Every other view opens the drawer whether or not the customer is signed in.

When the element wraps a link, holding Cmd, Ctrl, Shift or Alt while clicking opens that link instead - so a wrapped account link still opens in a new tab or window the way customers expect. Around a button there's no link to open, so every click opens the view.

### Outputs

| Attribute | Value | Notes |
| - | - | - |
| `state` | `"empty"` \| `"has-saves"` | Reflects whether the customer has any saved items. |
| `ready` | Boolean presence | Set once after first successful count resolution. |

### Example: header trigger with count badge

```html theme={null}
<swish-drawer view="home">
  <button class="swish-drawer-button">
    <span class="swish-drawer-icon">{% render 'icon-bookmark' %}</span>
    <span
      class="swish-drawer-badge"
      data-swish-bind="count"
      data-swish-when="has-saves"
      hidden
      >0</span
    >
  </button>
</swish-drawer>
```

```css theme={null}
swish-drawer[state="has-saves"] .swish-drawer-icon svg path {
  fill: var(--theme-accent-color);
}
```

The SDK writes the live count to the badge's `textContent` via `data-swish-bind="count"`, and toggles the badge's `hidden` based on the `data-swish-when="has-saves"` state. No JS required. The [examples](https://swish.app/docs/references/sdk/examples.md#header-badge-and-drawer-trigger) page shows the same badge built with `swish.state.itemCount()` and with the React hook.

### Styling the `badge` count

The **Account link** placement (see the [account guide](https://swish.app/docs/guides/features/account.md#account-link)) wraps your theme's own account link, where there's nowhere for you to add a badge element. `badge="true"` renders one: a `span.swish-drawer-badge` in the element's top-right corner, hidden while the customer has nothing saved, and `aria-hidden` so the wrapped link keeps naming the control.

Restyle it with custom properties rather than overriding the rules - they win without a specificity fight:

```css theme={null}
swish-drawer[badge] {
  --swish-drawer-badge-background: #b91c1c;
  --swish-drawer-badge-size: 18px;
  --swish-drawer-badge-font-size: 11px;
}
```

The full set is `background`, `color`, `size`, `font-size`, `font-weight`, `padding`, `border-radius`, `inset`, `offset-x` and `offset-y`, each prefixed `--swish-drawer-badge-`.

These rules ship with the placement, scoped to it. In a `<swish-drawer>` you write into the theme yourself, add the badge span shown above instead and style it as your own markup.

## Child conventions

### `data-swish-when`

Place on a descendant of `<swish-save>` or `<swish-drawer>`. The SDK toggles native `hidden` so the descendant is shown only when the parent's `state` attribute matches. Pass a comma-separated list to show the same content for multiple states - useful for optimistic UI where the post-click "saving" state should look identical to the resolved "saved" state.

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

Set `hidden` on non-default states in your template so the markup renders correctly before the SDK hydrates.

### `data-swish-bind`

Place on a descendant of `<swish-drawer>`. The SDK writes the bound value to `textContent`. Combine with `data-swish-when` on the same element when you need to hide the binding for empty states - the example badge uses `data-swish-bind="count" data-swish-when="has-saves" hidden` so it stays hidden when the customer has no saved items.

| Key | Element | Value source |
| - | - | - |
| `count` | `<swish-drawer>` | [`swish.state.itemCount()`](https://swish.app/docs/references/sdk/state#itemcount) |

While the [sign-in requirement](https://swish.app/docs/guides/features/account.md#sign-in-requirements) is `"save"` or `"all"`, `count` reads `0` and `state` reads `empty` for a signed-out customer, since they can't save until they sign in.

## Styling

### Class hooks

The custom elements impose no CSS. Add your own classes to the inner button and children - styles cascade in normally (no Shadow DOM).

The canonical sample uses `swish-save-button`, `swish-save-icon`, `swish-save-label` and `swish-drawer-button`, `swish-drawer-icon`, `swish-drawer-badge` so support devs have predictable selectors when overriding per-merchant styles.

### Pre-paint hiding with `ready`

Before the SDK boots, custom elements render their markup as-is. For places where a layout shift would be jarring (product card grids), opt into pre-paint hiding by targeting `:not([ready])`:

```css theme={null}
.product-grid swish-save:not([ready]) {
  visibility: hidden;
}
```

For elements that need to be visible immediately (a header drawer button), skip the rule - accept a momentary label/count flicker on first paint instead of a layout shift.

## Liquid snippet

Drop the snippet below into `snippets/swish.liquid` and the stylesheet into `assets/swish-theme.css`. The snippet dispatches between the elements by name; when called with no arguments it sets `window.SwishOptions` and loads `swish-theme.css`.

Usage in `theme.liquid`:

```liquid theme={null}
{%- comment -%} Init: before {{ content_for_header }} {%- endcomment -%}
{%- render 'swish' -%}

{%- comment -%} Render the elements wherever they're needed {%- endcomment -%}
{%- render 'swish', component: 'save', context: 'pdp' -%}
{%- render 'swish', component: 'save', context: 'product-card' -%}
{%- render 'swish', component: 'drawer', view: 'home' -%}
```

### `snippets/swish.liquid`

```liquid snippets/swish.liquid theme={null}
{% capture swish_icon %}
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 12" fill="none">
    <path d="M1.55078 1.74121C2.82601 0.418525 4.70947 0.418466 5.99512 1.74902L5.99805 1.75195L6.43555 2.19922C6.57634 2.3431 6.7694 2.42448 6.9707 2.4248C7.17209 2.42507 7.3656 2.34376 7.50684 2.2002L7.8252 1.87695L7.8291 1.87305C9.07534 0.584735 11.2091 0.427984 12.4121 1.57129C13.5834 2.6847 13.5732 4.65056 12.0732 6.19434L7.01562 11.2285L2.17969 6.43262L2.14551 6.40137L1.99121 6.26074C1.24251 5.55 0.851887 4.75694 0.767578 4.01172C0.679115 3.22933 0.919449 2.43413 1.55078 1.74121Z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"></path>
  </svg>
{% endcapture %}

{% capture swish_save_component %}
  <swish-save context="{{ context | default: 'pdp' }}">
    <button class="swish-save-button">
      <span class="swish-save-icon">{{ swish_icon }}</span>
      <span class="swish-save-label" data-swish-when="unsaved">Save</span>
      <span class="swish-save-label" data-swish-when="saved,saving" hidden>Saved</span>
    </button>
  </swish-save>
{% endcapture %}

{% capture swish_drawer_component %}
  <swish-drawer view="{{ view | default: 'home' }}">
    <button class="swish-drawer-button">
      <span class="swish-drawer-icon">{{ swish_icon }}</span>
      <span class="swish-drawer-badge" data-swish-bind="count" data-swish-when="has-saves" hidden>0</span>
    </button>
  </swish-drawer>
{% endcapture %}

{% capture swish_init %}
  <script>
    window.SwishOptions = {
      // Add Swish options here
      ui: {
        // Uncomment load custom CSS for Swish UI
        // css: [new URL("{{ 'swish-app.css' | asset_url }}", location.origin)]
      }
    }
  </script>
  {{ 'swish-theme.css' | asset_url | stylesheet_tag: preload: true }}
{% endcapture %}

{% case component %}
  {% when 'save' %}
    {{ swish_save_component }}
  {% when 'drawer' %}
    {{ swish_drawer_component }}
  {% else %}
    {{ swish_init }}
{% endcase %}
```

### `assets/swish-theme.css`

Starter stylesheet with every selector the snippet renders. Paste merchant button styles into the empty rule bodies.

```css assets/swish-theme.css theme={null}
/* ///////////////////////////////////////////////// */
/* Drawer button - header                            */
/* ///////////////////////////////////////////////// */

.swish-drawer-button {
}

.swish-drawer-icon {
}

.swish-drawer-button svg {
}

.swish-drawer-button svg path {
}

/* When the customer has saved items */
swish-drawer[state="has-saves"] .swish-drawer-button svg path {
}

.swish-drawer-badge {
}

/* ///////////////////////////////////////////////// */
/* Account link badge - header                       */
/* ///////////////////////////////////////////////// */

/* The saved-count badge the SDK renders for `badge="true"`. Set the custom
 * properties rather than the rules - they win without a specificity fight. */
swish-drawer[badge] {
  /* --swish-drawer-badge-background: #000; */
  /* --swish-drawer-badge-color: #fff; */
  /* --swish-drawer-badge-size: 16px; */
  /* --swish-drawer-badge-font-size: 10px; */
  /* --swish-drawer-badge-offset-x: 25%; */
  /* --swish-drawer-badge-offset-y: -25%; */
}

/* ///////////////////////////////////////////////// */
/* Save button - shared                              */
/* ///////////////////////////////////////////////// */

/* Disable interaction while a save/unsave is in flight */
swish-save[state="saving"] .swish-save-button {
  pointer-events: none;
}

/* ///////////////////////////////////////////////// */
/* Save button - Product card                        */
/* ///////////////////////////////////////////////// */

swish-save[context="product-card"] .swish-save-label {
  display: none;
}

swish-save[context="product-card"] .swish-save-button {
}

swish-save[context="product-card"] .swish-save-button:hover {
}

swish-save[context="product-card"] .swish-save-button svg {
}

swish-save[context="product-card"] .swish-save-button svg path {
}

/* Filled icon when saved (also during the optimistic saving window) */
swish-save[context="product-card"]:is([state="saved"], [state="saving"])
  .swish-save-button
  svg
  path {
}

/* ///////////////////////////////////////////////// */
/* Save button - PDP                                 */
/* ///////////////////////////////////////////////// */

swish-save[context="pdp"] .swish-save-button {
}

swish-save[context="pdp"] .swish-save-button svg {
}

swish-save[context="pdp"] .swish-save-label {
}

swish-save[context="pdp"] .swish-save-button svg path {
}

/* Filled icon when saved (also during the optimistic saving window) */
swish-save[context="pdp"]:is([state="saved"], [state="saving"])
  .swish-save-button
  svg
  path {
}

/*
 * Opt-in pre-paint hiding for placements where layout shift is acceptable
 * (e.g. product cards below the fold). Skip for above-the-fold elements
 * where hiding would cause a visible layout shift.
 */
/*
.product-grid swish-save:not([ready]) {
  visibility: hidden;
}
*/
```


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