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

# Visual Search widget

> Add Layers image search, shop-similar buttons, and similar-products previews to your Shopify theme.

## Overview

The Visual Search widget gives shoppers three ways to discover products:

* **Image search modal:** shoppers upload an image or select an area of an image to find visually similar products.
* **Shop-similar modal:** shoppers open a product-specific view of similar products.
* **Similar-products preview:** you can display a product grid directly on a page and let shoppers open the full modal.

The widget is a standalone JavaScript module served from the Layers CDN. The Layers app embed does **not** load it. The embed writes the `storefrontApiToken` that the widget can use and loads the storefront pixel, which forwards widget events to Clarity or Blotout when those integrations are enabled. Add the widget script to your theme yourself. See [What the app embed handles](/developers/app-embed).

## Prerequisites

* Install Layers and enable the app embed if you want it to provide the storefront access token and forward widget events.
* Have a storefront access token available. When the app embed is enabled, the widget can read its `storefrontApiToken` automatically. You can also provide a token on the widget script or through the widget manager.
* Add the widget script to your Shopify theme. Enabling the app embed alone does not add the widget.

## Installation

Add the script to `layout/theme.liquid`, before the closing `</body>` tag:

```html theme={null}
<script
  id="layers-script"
  type="module"
  src="https://cdn.uselayers.com/widget/layers-widget.js"
></script>
```

The CDN build is an ES module, so use `type="module"`. Module scripts defer automatically; you do not need to add `defer`.

Once the script loads, you can add the custom elements where you want the entry points and preview to appear. See [Image search](#image-search-toggle), [Shop similar](#similar-products-toggle), and [Similar-products preview](#similar-products-preview).

### Script attributes

Put these attributes on the script element with `id="layers-script"`.

| Attribute | Type and values | Default | Description |
| :- | :- | :- | :- |
| `data-api-key` | String | No value | Storefront access token. If present when the widget initializes, it becomes the configured key. If no key was configured at initialization, the widget also checks the current attribute value for each API request. |
| `data-hide-branding` | String: `"true"` | Unset | Set to `"true"` to hide the **Powered by Layers** footer in the image-search and similar-products modals. A boolean `hideBranding` value set through `configure()` overrides this attribute. |
| `data-debug` | String: `"true"` or `"false"` | `"false"` | Enables widget debug logging when the script initializes. The widget reads this attribute at initialization; use `configure({ debug: true })` to change debug logging later. |

### API key resolution and load order

The widget resolves its access token whenever it makes an API request, in this order:

1. `widgetManager.config.apiKey`, set with `setApiKey()`, `configure({ apiKey })`, or by a non-empty `data-api-key` value read when the widget initializes.
2. The current non-empty `data-api-key` value on `#layers-script`.
3. The `storefrontApiToken` value in the app embed's `<script id="layers-config" type="application/json">` element.

If you inject `data-api-key` after the widget loads and no key was configured at initialization, the widget can use the new value on its next request. A key already stored in `config.apiKey` remains higher priority than later changes to the attribute. The widget caches the app embed token after it first parses a valid value. Missing or invalid embed JSON does not prevent initialization.

`data-debug` is read once when the widget initializes. In contrast, `data-hide-branding` is checked when each modal renders, unless you explicitly configure `hideBranding`.

### JavaScript configuration

The widget manager is available at `window.Layers.widgetManager`. Configure it after the module has loaded:

```html theme={null}
<script>
  document.addEventListener('DOMContentLoaded', () => {
    window.Layers.widgetManager.configure({
      debug: false,
      showInstructions: true,
      hideBranding: true,
      currencyFormat: {
        currency: 'EUR',
        locale: 'de-DE'
      }
    })
  })
</script>
```

`configure()` accepts these options:

| Option | Type | Default | Description |
| :- | :- | :- | :- |
| `apiKey` | String | Unset | Sets the highest-priority storefront access token. |
| `debug` | Boolean | `false` | Enables or disables debug logging. |
| `showInstructions` | Boolean | `true` | Shows or hides the image-search instructions by default. |
| `hideBranding` | Boolean | Unset | When set, overrides `data-hide-branding` for both modals. |
| `currencyFormat` | Object | See below | Sets price formatting. `configure()` merges this object with the existing currency format instead of replacing it. |

The `currencyFormat` object accepts:

| Property | Type and values | Default | Description |
| :- | :- | :- | :- |
| `currency` | ISO currency code, such as `"EUR"` | Unset | Explicit currency override. |
| `locale` | Locale string, such as `"de-DE"` | Unset | Explicit locale override. |
| `style` | `"currency"` or `"decimal"` | `"currency"` | Selects currency or decimal number formatting. |
| `currencyDisplay` | `"symbol"`, `"code"`, or `"name"` | `"symbol"` | Controls how the currency appears when `style` is `"currency"`. |
| `showSymbol` | Boolean | `true` | Legacy configuration field. The current formatter uses `currencyDisplay` to choose the currency representation. |

For example, `configure({ currencyFormat: { currency: 'EUR' } })` keeps the default `style: 'currency'` and `currencyDisplay: 'symbol'`. You can also use `updateCurrencyFormat()` to merge currency-format properties.

Other available methods include:

* `setApiKey(apiKey)` and `getApiKey()` to set or resolve the storefront access token.
* `createImageSearchToggle(options)`, `createSimilarProductsToggle(options)`, and `createSimilarProductsPreview(options)` to add widgets from JavaScript.
* `updateTheme(properties)` to set CSS custom properties at runtime.
* `updateTranslations(widgetType, translations)` to merge translations for `image-search` or `similar-products`.
* `debugTheme()` to log the theme values the widget manager tracks.

## Image search toggle

Use `<layers-image-search-toggle>` to add a button that opens the image-search modal.

```html theme={null}
<layers-image-search-toggle
  upload-mode="drag-drop"
  icon="CameraSearch"
  tooltip-enabled="true"
  tooltip-text="Search with an image"
></layers-image-search-toggle>
```

| Attribute | Type and values | Default | Description |
| :- | :- | :- | :- |
| `template-id` | String | Unset | ID of a `<template>` whose contents appear in the upload step. |
| `upload-mode` | `"drag-drop"` or `"button"` | `"drag-drop"` | Selects the drag-and-drop upload area or the upload button. Any value other than `"button"` uses drag-and-drop mode. |
| `icon` | `"ScanSearch"`, `"Search"`, `"Camera"`, or `"CameraSearch"` | `"ScanSearch"` | Chooses the toggle icon. |
| `show-instructions` | String: `"true"` or `"false"` | `true` | Shows or hides the image-search instructions. |
| `enable-paste` | String: `"true"` or `"false"` | `false` | Set to `"true"` to enable pasting an image. |
| `tooltip-enabled` | String: `"true"` or `"false"` | `true` | Set to `"false"` to disable the tooltip. |
| `tooltip-text` | String | Built-in translation: `"NEW: Search with your camera"` | Sets the tooltip text. |
| `tooltip-duration` | Integer, milliseconds | `15000` | Sets how long the tooltip stays visible. |
| `tooltip-max-shows` | Integer | `15` | Sets the maximum number of tooltip displays for the current session. |
| `tooltip-show-close-button` | String: `"true"` or `"false"` | `true` | Set to `"false"` to hide the tooltip's close button. |

### Custom upload content

Add a `<template>` and reference its `id` with `template-id` to display custom content in the upload step:

```html theme={null}
<template id="image-search-upload-tips">
  <div>
    <p>Choose a photo or paste an image to find similar products.</p>
  </div>
</template>

<layers-image-search-toggle template-id="image-search-upload-tips"></layers-image-search-toggle>
```

### Reference images and area-selection events

Use `<layers-image-search-reference-image>` to open image search from an image:

```html theme={null}
<layers-image-search-reference-image
  src="/files/product-inspiration.jpg"
  alt="Search with this image"
></layers-image-search-reference-image>
```

The custom element copies its attributes to an image and opens the search flow when a shopper clicks it. The widget also emits a bubbling `layers-image-search-area-selected` event after the shopper selects an image area:

```html theme={null}
<script>
  document.addEventListener('layers-image-search-area-selected', (event) => {
    console.log('Selected image area:', event.detail)
  })
</script>
```

## Similar-products toggle

Use `<layers-similar-products-toggle>` to add an entry point for a product's similar-products modal:

```liquid theme={null}
<layers-similar-products-toggle product-id="{{ product.id }}"></layers-similar-products-toggle>
```

| Attribute | Type and values | Default | Description |
| :- | :- | :- | :- |
| `product-id` | String | Required | Product ID used to find similar products. |
| `reference-image-src` | String URL | Unset | Shows a reference image in the modal. |
| `data-facet-code` | String | Unset | Requests and displays facet options for the specified facet. |
| `data-filter-options` | JSON string | Unset | The toggle parses and forwards an array of objects with a `title` and an `options` array; each option has a `name` and `value`. The current modal does not read this attribute, so it does not change the displayed filters. |

You can add a filter group as a child script. The widget validates this JSON before using it:

```html theme={null}
<layers-similar-products-toggle product-id="{{ product.id }}">
  <script type="layers/filter-groups">
    {
      "conditional": "AND",
      "expressions": [
        {
          "property": "price",
          "operator": "greater_than",
          "values": [50]
        }
      ]
    }
  </script>
</layers-similar-products-toggle>
```

Set `conditional` to `"AND"` or `"OR"`. Each expression has a string `property`, a string `operator`, and a `values` array containing strings, numbers, or booleans.

## Similar-products preview

Use `<layers-similar-products-preview>` to render a product grid with a button that opens the similar-products modal:

```liquid theme={null}
<layers-similar-products-preview
  product-id="{{ product.id }}"
  limit="4"
  lazy-load="true"
></layers-similar-products-preview>
```

| Attribute | Type and values | Default | Description |
| :- | :- | :- | :- |
| `product-id` | String | Required | Product ID used to find similar products. |
| `lazy-load` | String: `"true"` or `"false"` | `true` | Loads results when the preview approaches the viewport. Set to `"false"` to load immediately. |
| `limit` | Integer | `4` | Number of products to show in the preview grid. |

The preview also accepts a child `<script type="layers/filter-groups">` with a filter group in the same format as the similar-products toggle.

## Prices, currency, and Shopify Markets

The image-search and similar-products requests send `context.market` when `window.Shopify.country` contains a country code. The widget trims and uppercases the value, for example `"ca"` becomes `"CA"`. If Shopify country is unavailable or empty, the widget omits `context.market`. The API then uses the request's geo country, or the US if no country is available, and returns prices localized for that market.

When the response includes `_meta.pricing.currency_code`, the widget uses it to format the returned prices. It resolves currency in this order:

1. `currencyFormat.currency`, if you explicitly configure it.
2. The response's `_meta.pricing.currency_code`.
3. `window.Shopify.currency.active`.
4. `USD`.

The locale order is:

1. `currencyFormat.locale`, if you explicitly configure it.
2. `window.Shopify.locale`, with `window.Shopify.country` appended when the locale has no region subtag, such as `en-CA`.
3. The bare `window.Shopify.locale`.
4. `en-US`.

If the requested locale is invalid, the widget tries the bare Shopify locale and then `en-US`.

Set explicit formatting when your storefront needs a fixed currency or locale:

```javascript theme={null}
window.Layers.widgetManager.configure({
  currencyFormat: {
    currency: 'EUR',
    locale: 'de-DE',
    style: 'currency',
    currencyDisplay: 'symbol'
  }
})
```

`configure()` merges `currencyFormat` properties, so setting only `currency` keeps the existing currency style and symbol display. For more about configuring Shopify market context, see [Shopify Markets](/shopify-integration/shopify-markets).

## Styling

The widget applies theme values across its shadow roots. Set supported variables on `:root` in your theme CSS, or call `window.Layers.widgetManager.updateTheme()` to change them at runtime. The theme manager reads computed values from `document.documentElement` and observes changes to its style and class attributes.

```css theme={null}
:root {
  --layers-widget-color-primary: #234567;
  --layers-widget-font-family: Arial, sans-serif;
}
```

```javascript theme={null}
window.Layers.widgetManager.updateTheme({
  '--layers-widget-color-primary': '#234567',
  '--layers-widget-font-family': 'Arial, sans-serif'
})
```

The table lists every supported variable with its default. `updateTheme()` does not track `--layers-widget-transition-properties`; set that one in CSS.

| Category | CSS variable | Default |
| :- | :- | :- |
| Primary color | `--layers-widget-color-primary` | `#123fff` |
| Primary color | `--layers-widget-color-primary-hover` | `#607ffd` |
| Primary color | `--layers-widget-color-primary-light` | `rgba(18, 63, 255, 0.05)` |
| Primary color | `--layers-widget-color-primary-transparent` | `rgba(18, 63, 255, 0.2)` |
| UI color | `--layers-widget-color-text` | `#161616` |
| UI color | `--layers-widget-color-text-secondary` | `#707070` |
| UI color | `--layers-widget-color-text-light` | `#ffffff` |
| UI color | `--layers-widget-color-background` | `#ffffff` |
| UI color | `--layers-widget-color-border` | `#e5e5e5` |
| UI color | `--layers-widget-color-shadow` | `rgba(0, 0, 0, 0.1)` |
| UI color | `--layers-widget-color-hover-bg` | `rgba(0, 0, 0, 0.05)` |
| UI color | `--layers-widget-color-overlay-bg` | `rgba(0, 0, 0, 0.5)` |
| Product color | `--layers-widget-color-price` | `#161616` |
| Product color | `--layers-widget-color-compare-price` | `#9d9d9d` |
| Product color | `--layers-widget-product-image-bg` | `#f8f8f8` |
| Typography | `--layers-widget-font-family` | `-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif` |
| Typography | `--layers-widget-font-size-sm` | `0.9rem` |
| Typography | `--layers-widget-font-size-md` | `1rem` |
| Typography | `--layers-widget-font-size-lg` | `1.2rem` |
| Typography | `--layers-widget-font-size-xl` | `1.5rem` |
| Typography | `--layers-widget-font-weight-normal` | `400` |
| Typography | `--layers-widget-font-weight-medium` | `500` |
| Typography | `--layers-widget-font-weight-bold` | `600` |
| Spacing | `--layers-widget-spacing-xs` | `0.25rem` |
| Spacing | `--layers-widget-spacing-sm` | `0.5rem` |
| Spacing | `--layers-widget-spacing-md` | `0.75rem` |
| Spacing | `--layers-widget-spacing-lg` | `1rem` |
| Spacing | `--layers-widget-spacing-xl` | `1.25rem` |
| Spacing | `--layers-widget-spacing-xxl` | `1.5rem` |
| Spacing | `--layers-widget-spacing-xxxl` | `2.5rem` |
| Borders | `--layers-widget-border-radius-sm` | `0.25rem` |
| Borders | `--layers-widget-border-radius-md` | `0.5rem` |
| Borders | `--layers-widget-border-width` | `0.0625rem` |
| Borders | `--layers-widget-border-width-dashed` | `0.125rem` |
| Borders | `--layers-widget-border-style-dashed` | `dashed` |
| Icons | `--layers-widget-icon-size-sm` | `1rem` |
| Icons | `--layers-widget-icon-size-md` | `1.5rem` |
| Icons | `--layers-widget-icon-size-lg` | `2rem` |
| Icons | `--layers-widget-icon-size-xl` | `3rem` |
| Transitions | `--layers-widget-transition-duration` | `0.2s` |
| Transitions | `--layers-widget-transition-timing` | `ease` |
| Transitions | `--layers-widget-transition-properties` | `background-color, border-color, color, transform, box-shadow` |
| Dialog | `--layers-widget-dialog-max-width` | `28.125rem` |
| Dialog | `--layers-widget-dialog-width` | `28.125rem` |
| Dialog | `--layers-widget-dialog-max-height` | `100vh` |
| Dialog | `--layers-widget-dialog-shadow` | `-0.3125rem 0 1.25rem var(--layers-widget-color-shadow)` |
| Dialog | `--layers-widget-dialog-shadow-mobile` | `0 -0.3125rem 1.25rem var(--layers-widget-color-shadow)` |
| Dialog | `--layers-widget-dialog-border-radius` | `var(--layers-widget-border-radius-md)` (`0.5rem`) |
| Results | `--layers-widget-results-image-max-width` | `12.5rem` |
| Results | `--layers-widget-product-grid-columns` | `2` |
| Results | `--layers-widget-product-grid-columns-desktop` | `3` |
| Results | `--layers-widget-product-grid-columns-mobile` | `2` |
| Results | `--layers-widget-product-grid-columns-desktop-mobile` | `2` |
| Results | `--layers-widget-product-grid-gap` | `1rem` |
| Results | `--layers-widget-product-image-aspect-ratio` | `1 / 1` |

## Translations and analytics

Use `updateTranslations()` to deep-merge custom text for either widget:

```javascript theme={null}
window.Layers.widgetManager.updateTranslations('image-search', {
  modal: {
    header: {
      title: 'Find products with an image'
    }
  }
})
```

The widget exposes `window.Layers.analytics.subscribe(handler)` for its analytics events. When Clarity or Blotout is enabled, the app embed can also forward widget events to those services when their scripts are present and consent allows it. See [app embed integrations](/developers/app-embed#third-party-integration-forwarding).

## Troubleshooting

### The widget does not appear

* Confirm that `layout/theme.liquid` includes the module script and that its `src` URL loads successfully.
* Confirm that your page includes the appropriate custom element. The similar-products toggle and preview also need a `product-id`.
* Check the browser console for script, configuration, or network errors.

### The console says "No API key found"

Confirm that at least one token source is available: `data-api-key` on `#layers-script`, `storefrontApiToken` in the app embed's `#layers-config` JSON, or `window.Layers.widgetManager.setApiKey()` / `configure({ apiKey })`. A token configured at initialization takes priority over later attribute changes.

### Prices use the wrong currency

Check `window.Shopify.country`, `window.Shopify.locale`, and `window.Shopify.currency.active` on the storefront. Review the API response's `_meta.pricing.currency_code` and any explicit `currencyFormat` settings, which take priority over Shopify's active currency. See [Shopify Markets](/shopify-integration/shopify-markets).

### Branding is still visible

Set `data-hide-branding="true"` on the script element with `id="layers-script"`. If you set `hideBranding` with `configure()`, its boolean value overrides the script attribute.

### Enable debug logging

Set `data-debug="true"` on `#layers-script` before the module initializes, or enable logging later:

```javascript theme={null}
window.Layers.widgetManager.configure({ debug: true })
```

## Next steps

* [What the app embed handles](/developers/app-embed) for token delivery, storefront pixel behavior, and event forwarding.
* [Theme implementation checklist](/developers/theme-implementation-checklist) for the wider Shopify theme integration.
* [Shopify Markets](/shopify-integration/shopify-markets) for market and currency setup.
* [Liquid integration](/developers/liquid-integration) for reading Layers configuration and metaobjects in your theme.
