Skip to content

VIBE on custom themes

This reference covers custom VIBE integrations on Online Store 2.0 themes.

It documents:

  • The vibe.widget_config shop metafield that drives the storefront widget.
  • The runtime global (window.__vibe) the widget exposes on the page.
  • The data attributes the theme app extension blocks render.
  • The locale keys the widget reads for translatable UI text.

Audience: Theme developers and agencies. Merchants should configure VIBE in the app and use Enable VIBE on your theme.

Scope: This page documents the shipped widget and theme extension. Direct metafield edits are unsupported and can be overwritten by the app.


1Admin settingsThe merchant configures VIBE in the embedded app.
2Shop metafieldThe app publishes normalized widget configuration.
3Theme blockThe app embed or search block exposes the mount and environment data.
4LoaderThe storefront runtime reads configuration and loads the active mode.
5SurfaceQuick search or the search page renders in the theme's light DOM.
Appearance Installation showing analyzed-theme readiness and storefront setup checks.
Appearance Installation showing analyzed-theme readiness and storefront setup checks.
  1. The merchant configures VIBE in the app admin. The app writes the settings to a single shop metafield: namespace vibe, key widget_config (type JSON).
  2. The theme app extension renders a hidden mount element on the storefront. It serializes the metafield value into a data-widget-config attribute and adds a small set of environment data attributes (shop domain, proxy URL, locale, country, asset URLs).
  3. vibe-loader.js boots on page load, parses data-widget-config and the locale data, normalizes everything into a frozen config object, and publishes runtime state on window.__vibe.
  4. The loader applies styling (CSS variables and classes on <html>) and then dynamically imports the active mode bundle (overlay/fallback or native).

Two things follow from this design:

  • The blocks themselves have no theme-editor settings. Both blocks declare "settings": []. All runtime behavior comes from the metafield. You cannot configure VIBE from the theme editor’s block settings panel; you configure it in the app.
  • The widget runs in the light DOM (no shadow DOM). Styling is applied via CSS custom properties and classes on document.documentElement (<html>).

VIBE ships two blocks in the vibe-search theme app extension.

  • Source: extensions/vibe-search/blocks/vibe-embed.liquid
  • Schema target: body (an app embed, enabled per theme).
  • Renders an element with id="vibe-search-root" and data-vibe-root.
  • This is the block merchants enable to turn VIBE on across the storefront (header search, predictive/quick search).
  • Source: extensions/vibe-search/blocks/vibe-search-page.liquid
  • Schema target: section, enabled_on.templates: ["search"] (only available on the search template).
  • Renders an element with data-vibe-root and data-vibe-surface="search-page", plus a separate mount element with data-vibe-search-page-mount and data-vibe-search-action="/search".
  • Used to render the VIBE search results experience directly on the storefront search page.

Both blocks load the same assets at the end of their markup:

<script src="{{ 'vibe-loader.js' | asset_url }}" type="module"></script>
<link rel="stylesheet" href="{{ 'vibe-widget.css' | asset_url }}" media="all">

The loader (widget-src/loader.js) selects which mount element (“root”) to use:

  1. It collects all elements matching [data-vibe-root] and #vibe-search-root (de-duplicated).
  2. On the main search page, if any root has data-vibe-surface="search-page", that root is used.
  3. Otherwise it uses #vibe-search-root, falling back to the first [data-vibe-root] element found.

If no root is found, the loader throws [VIBE] #vibe-search-root not found and does nothing. If a root is found but has no data-proxy-url, the loader throws [VIBE] No proxyUrl configured.

This means a custom theme integration must, at minimum, render an element with data-vibe-root (or id="vibe-search-root") carrying a valid data-proxy-url. The simplest and supported way to get this markup is to enable the shipped app embed.


The blocks render these attributes on the root element. The loader reads them as the environment/bootstrap inputs.

| Attribute | Source (Liquid) | Purpose | | --------------------------------- | ------------------------------------- | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | | id="vibe-search-root" | static (app embed only) | Primary root selector. | | data-vibe-root | static | Root selector (both blocks). | | data-vibe-surface="search-page" | static (search-page block only) | Marks the search-page root so it wins selection on the search template. | | data-proxy-url | {{ shop.url }}/apps/search | App Proxy endpoint the widget calls for search, controls, and tracking. Required. | | data-shop | {{ shop.permanent_domain }} | The shop’s permanent .myshopify.com domain. | | data-locale | {{ localization.language.iso_code | default: request.locale.iso_code }} | Active storefront language ISO code; used to pick a locale translation override. | | data-country | {{ localization.country.iso_code }} | Active storefront country ISO code. | | data-native-src | {{ 'vibe-native.js' | asset_url }} | CDN URL of the native-mode bundle. | | data-fallback-src | {{ 'vibe-fallback.js' | asset_url }} | CDN URL of the overlay/fallback-mode bundle. | | data-fallback-css | {{ 'vibe-fallback.css' | asset_url }} | CDN URL of the overlay/fallback-mode stylesheet. The loader injects it as a <link> when it loads vibe-fallback.js; only the slim vibe-widget.css base is render-blocking. | | data-i18n | per-key {{ "vibe_search.\*" | t | json }} | JSON object of translated UI strings from the theme’s locale files (see Locale keys). | | data-widget-config | {{ widget_config | json | escape }} | The serialized vibe.widget_config metafield value (JSON). Defaults to {} when the metafield is empty. |

The search-page block additionally renders a second mount element:

AttributeSource (Liquid)Purpose
data-vibe-search-page-mountstaticMount point for the search-page results UI.
data-vibe-search-action="/search"staticThe storefront search form action the search-page experience targets.

The root element is rendered hidden (style="display:none" and aria-hidden="true"); it is a configuration carrier, not visible UI.

The loader sets a small number of attributes/classes on document.documentElement (<html>):

  • vibe-cards--simple / vibe-cards--detailed: card style class.
  • vibe-cols--3 / vibe-cols--4: results grid columns class.
  • vibe-fonts--theme: present when font mode is theme (inherit the storefront font); absent when font mode is vibe.
  • CSS variables --vibe-radius-card, --vibe-radius-sm, --vibe-spacing: numeric pixel values for corner radius and component spacing.
  • --vibe-<name> color variables (for example --vibe-accent): set only when colorSchemeMode is custom (see Color scheme).
  • data-vibe-overlay="true" on <html>: set when the widget runs in overlay mode and the overlay guard is installed.

The metafield is a single JSON object. Every field is optional; the loader applies a defined default (or derives a value) for each one. Where a field is not present in the metafield, the loader also accepts a matching data-* attribute on the root for some keys (noted below), but the metafield is the primary, app-managed source.

Field names below are exactly the JSON keys the loader reads from the parsed widgetConfig object.

FieldTypeDefaultNotes
cardStylestringdetailedOnly simple or detailed; any other value falls back to detailed. Also accepts data-card-style.
cornerRadiusnumber (px)8Clamped from 0 to 32, rounded. Sets --vibe-radius-card (and --vibe-radius-sm at ~0.75x). Also accepts data-corner-radius.
componentSpacingnumber (px)16Clamped from 0 to 48, rounded. Sets --vibe-spacing. Also accepts data-component-spacing.
gridColumnsnumber4Only 3 or 4; any other value falls back to 4. Also accepts data-grid-columns.
fontModestringthemetheme (inherit storefront font) or vibe (use the widget’s own font). Any value other than vibe is treated as theme. Also accepts data-font-mode.
colorSchemeModestringdefaultWhen custom, the widget applies customColors. See Color scheme. Also accepts data-color-scheme-mode.
customColorsobjectnoneMap of color-token name to CSS color string. Applied only when colorSchemeMode is custom. See the token list below.

When colorSchemeMode is custom, the loader reads customColors (an object) and sets the corresponding --vibe-<name> CSS variables on <html>. Only string values are applied. Recognized token names (from widget-src/lib/color-scheme.js):

TokenDefault
bg-1#FFFFFF
bg-2#F5F6F8
primary-text#3A3636
secondary-text#FFFFFF
accent#590FB8
accent-secondary#D8C4F9
primary-gray#767C85
secondary-gray#AFBCCD
tertiary#D8DDE2

When colorSchemeMode is not custom, these defaults (defined in the widget stylesheet) apply and customColors is ignored.

FieldTypeDefaultNotes
overlayModestringoverlayoverlay forces the fullscreen overlay; native opts out (inject into the theme’s own search UI). Any other value defaults to overlay.
placeholderstringsearch_placeholder locale value, else Search products, collectionsSearch input placeholder. Also accepts data-placeholder.
resultsCountnumber12Main results page size. Clamped from 1 to 48 (packages/shared/src/quick-search-limits.ts). Also accepts data-results-count.
quickSearchAutocompleteEnabledbooleantrueSet false to disable autocomplete suggestions on the quick (predictive) search surface; any value other than the boolean false is treated as enabled.
searchPageAutocompleteEnabledbooleantrueSet false to disable autocomplete suggestions on the full search page; any value other than the boolean false is treated as enabled.
showSearchHintbooleantrueShow the search hint text. Also accepts data-show-search-hint.
searchHintTextstringTry natural language like "cozy autumn jacket"Hint text shown with search. Also accepts data-search-hint-text.
showExploreHintbooleantrueShow the explore (sliders) hint. Also accepts data-show-explore-hint.
exploreHintTextstringAdjust fine-details with slidersExplore hint text. Also accepts data-explore-hint-text.
gtmEventsEnabledbooleanfalseWhen true, the widget pushes Google Tag Manager dataLayer events. See GTM events.
abTestActivebooleanfalseWhen true, the widget assigns the session to an A/B group (see A/B testing).

These cap how many of each result type appear in quick (predictive) search. They are sanitized to integers >= 1 and clamped to a maximum (packages/shared/src/quick-search-limits.ts).

FieldTypeDefaultMax
quickSearchProductLimitnumber48
quickSearchCollectionLimitnumber38
quickSearchSuggestionLimitnumber33

These control how VIBE renders on the storefront search template.

FieldTypeDefaultNotes
searchPageModestringdisabledOne of app-block, embed-replace, disabled. Any other value falls back to disabled.
searchPageTemplateKindstringunknownDetected template type: json, liquid, or unknown.
searchPageRecommendedIntegrationstringunknownRecommended integration: app-block, app-embed, or unknown.
searchPageAppBlockInstalledbooleanfalseWhether the Search Page app block is detected as installed.
searchPageReplaceSelectorstring""Selector of the theme element to replace in embed-replace mode.
useNativeSearchPageCardsbooleanfalseRender results using the theme’s native product card. Effective only when true and searchPageProductCardSelector is set.
searchPageProductCardSelectorstring""Selector for the theme’s search-page product card.
searchPagePriceSelectorstring""Selector for the price element within the native card.
searchPageCompareAtSelectorstring""Selector for the compare-at price within the native card.

VIBE locates the storefront’s search inputs and containers using selectors. Most have desktop/mobile variants that fall back to the base selector when unset, and several have a hard-coded default targeting the standard Shopify search form.

The base selectors:

FieldDefault
quickSearchSelector""
quickSearchInputSelectorform[action="/search"] input[name="q"]
quickSearchPopularQueriesSelector""
quickSearchSuggestionsSelector""
mainSearchSelector""
mainSearchInputSelectorform[action="/search"] input[name="q"]
mainSearchSuggestionsSelector""
mainSearchProductListSelector""

For each of the quick- and main-search groups, the loader also reads desktop and mobile variants and falls back to the base value (and, for input selectors, to the standard form default) when a variant is unset:

  • Quick: quickSearchDesktopSelector, quickSearchDesktopInputSelector, quickSearchDesktopPopularQueriesSelector, quickSearchDesktopSuggestionsSelector, and the matching quickSearch Mobile* keys.
  • Main: mainSearchDesktopSelector, mainSearchDesktopInputSelector, mainSearchDesktopSuggestionsSelector, mainSearchDesktopProductListSelector, and the matching mainSearchMobile* keys.

Most of these are detected automatically by VIBE’s theme analyzer and written into the metafield by the app. Set them manually only when integrating on a theme whose markup the analyzer cannot detect.

Advanced native-integration fields (analyzer-detected)

Section titled “Advanced native-integration fields (analyzer-detected)”

The metafield can also carry a set of advanced fields used for deep “native” integration, where VIBE injects its results into the theme’s own predictive-search markup. These are normally populated automatically by VIBE’s theme analyzer and are not intended to be hand-authored. They are listed here for completeness; the loader reads each one and defaults it to an empty string, null, or false when absent.

| Field | Type | Default | | --------------------------------------- | ------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------ | | useNativeQuickCards | boolean | false | | quickSearchProductCardSelector | string | "" | | quickSearchPriceSelector | string | "" | | quickSearchCompareAtSelector | string | "" | | quickSearchCardSnippetName | string | "" | | quickSearchCardRenderTagForProduct | string | "" | | quickSearchCardStrategy | (any) | null | | quickSearchOpenTarget | string | "" | | quickSearchOpenAttributes | (any) | null | | quickSearchOpenClasses | (any) | null | | quickSearchResultsContainer | string | "" | | quickSearchResultsGroups | (any) | null | | quickSearchResultsCloneTemplate | (any) | null | | quickSearchMobileResultsContainer | string | "" | | quickSearchMobileResultsGroups | (any) | null | | quickSearchMobileResultsCloneTemplate | (any) | null | | quickSearchFetchMode | string | "" | | quickSearchSectionId | string | "" | | quickSearchGridSelector | string | "" | | quickSearchCollectionPlacement | string | "" (only suggestions-list or after-products accepted) | | translations | object | none | Per-locale translation overrides; see Per-locale overrides. |

Do not hand-edit the analyzer fields. They describe the exact markup of a specific theme and are regenerated whenever the app re-analyzes the theme. Manual values may be overwritten and can break native integration if they no longer match the theme.


After bootstrap, the loader publishes a frozen object on window.__vibe. This is the supported read-only surface for integrations that need to observe VIBE’s runtime state.

window.__vibe = {
config, // the normalized, frozen config object (see below)
loaded: false, // set to true once the active mode bundle has initialized
version: "2.0.0", // widget runtime version
sessionId, // cookieless anonymous session id (or null)
abGroup, // 'vibe' or 'control'
abTestActive, // boolean: whether an A/B test is running
controlsPrefetchPromise, // Promise of prefetched semantic controls, or null
};

Notes:

  • window.__vibe is replaced (not mutated) at key points. loaded becomes true after the mode bundle’s init() runs, and sessionId is re-synced if the session id changes. Read it freshly rather than caching a reference.
  • version is the widget runtime version string as shipped in widget-src/loader.js. It is diagnostic information, not a stable public API. Do not branch storefront behavior on a specific value.
  • controlsPrefetchPromise is non-null only when controls are prefetched (overlay mode, the search-page surface, or the main search page with search-page mode enabled) and the session is not in the A/B control group.

window.__vibe.config is the normalized configuration the widget runs on. It is derived from the metafield, the data attributes, and built-in defaults, then frozen. Its keys mirror the metafield fields documented above (for example proxyUrl, shop, placeholder, resultsCount, cardStyle, cornerRadius, componentSpacing, gridColumns, fontMode, the quick-search and main-search selectors, the search-page fields, and an i18n object of resolved translations). A few keys are normalized to a different name or shape than the raw metafield:

config keyDerived fromNotes
proxyUrldata-proxy-urlApp Proxy base URL.
shopdata-shopPermanent shop domain.
useOverlayoverlayModetrue for overlay, false for native.
gtmEventsgtmEventsEnabledBoolean.
quickSearchAutocompleteEnabled / searchPageAutocompleteEnabledsame metafield keysPer-surface autocomplete toggles; true unless explicitly false.
quickSearchProductLimit / quickSearchCollectionLimit / quickSearchSuggestionLimitquick-search limitsSanitized + clamped integers.
i18ndata-i18n + translations overrideResolved translation map (see Locale keys).

Treat config as read-only. Changing VIBE’s behavior must be done in the app (which updates the metafield); mutating config at runtime is unsupported and, because the object is frozen, has no effect.


VIBE’s translatable UI text comes from the theme app extension’s locale files. The default file is extensions/vibe-search/locales/en.default.json, under the top-level vibe_search namespace. The blocks emit each key into the root’s data-i18n attribute using Shopify’s {{ "vibe_search.<key>" | t | json }} translation filter, so the active storefront language’s values are used automatically.

The available keys (from en.default.json):

KeyEnglish default
productsProducts
collectionsCollections
searchSearch
exploreExplore
closeClose
clear_searchClear search
search_placeholderSearch products, collections
deselectDeselect
in_collectionsIn Collections
in_articlesIn Articles
explore_hint_slidersAdjust fine-details with sliders
resetReset
all_collectionsAll
updating_resultsUpdating results…
concept_requiredAdd a concept before adjusting the value.
load_more_resultsLoad more results
sort_bySort by
sort_relevanceRelevance
sort_price_ascPrice: low to high
sort_price_descPrice: high to low
sort_newestNewest
filtersFilters
minMin
maxMax
in_stockIn stock
out_of_stockOut of stock
no_resultsNo results
no_results_titleYour search did not return any results.
no_results_hintCheck your spelling or try again with a less specific keyword.
explore_no_results_hintTry adjusting the Explore controls or clearing filters.
search_againSearch again

Supported languages. en.default.json is the bundled widget locale. To translate the storefront widget UI, add standard Shopify extension locale files (for example fr.json) with the same vibe_search keys; the blocks’ {{ ... | t }} filters resolve them per the storefront’s active language.

The loader supports an additional override layer via the translations object in the metafield. If present, it is keyed by locale code. For the active locale (from data-locale, or window.Shopify.locale, default en), the loader looks up an exact match, then the base language (the part before -), and merges that object’s strings over the data-i18n values. Keys match the locale keys above. This lets the app override specific strings per language without editing the theme’s locale files.


VIBE uses a cookieless, anonymous session model (widget-src/core/session.js):

  • A random session id is stored in sessionStorage under vibe_sid, with a timestamp under vibe_sid_ts.
  • The session expires after 30 minutes of inactivity; a new id is generated on the next request.
  • No cookies are set, and no personally identifiable information is collected by the session layer.
  • The session id is also reflected on window.__vibe.sessionId.

The loader also installs cart-add attribution tracking: it listens for common theme cart events (cart:line_item_added, cart:line-item-added, ajaxProduct:added, product:added) and patches window.fetch to detect /cart/add (and /cart/add.js) requests, so an add-to-cart can be attributed to a prior VIBE result click recorded in sessionStorage (vibe_last_click). All tracking calls go to the App Proxy (data-proxy-url).

When gtmEventsEnabled is true in the metafield, the widget pushes events to window.dataLayer (creating it if needed) via pushGTM. If GTM events are disabled, no dataLayer writes occur. GTM is only active when the merchant enables it; VIBE does not load GTM itself.

When abTestActive is true and a session id exists, the loader derives the A/B group from the last hex digit of the session id. Even values use vibe; odd values use control. For the control group, the widget does not render. It installs attribution tracking on the theme’s native search input. The active group is exposed as window.__vibe.abGroup.


  1. Enable the app embed. Add the VIBE Search app embed to the theme (the in-app theme editor deep link is the supported path). This renders the #vibe-search-root element with the required environment attributes.
  2. Confirm the proxy URL resolves. The root must carry a data-proxy-url (the blocks set it to {{ shop.url }}/apps/search). Without it, the loader throws and does nothing.
  3. Check the search input selectors. If the theme’s search markup is non-standard, VIBE’s analyzer may not detect it. The default input selector is form[action="/search"] input[name="q"]; if your theme differs, the app’s analysis should capture the right selectors into the metafield. Avoid hand-editing the analyzer-detected fields.
  4. Decide overlay vs. native. overlayMode controls whether VIBE opens its own fullscreen overlay (overlay) or injects into the theme’s search UI (native). Set this in the app.
  5. (Optional) Enable the Search Page block on the search template if you want VIBE to power the full search results page. This adds the search-page root (data-vibe-surface="search-page"), which wins root selection on that template.
  6. Verify locale coverage. Ensure the storefront language has a matching extension locale file, or rely on the English default. Use the metafield translations override only for app-managed per-locale string changes.
  7. Test. Append ?vibe_debug to a storefront URL to enable loader debug logging during integration.

ConcernFile
Loader / config normalization / window.__vibewidget-src/loader.js
Cookieless session + GTM helperwidget-src/core/session.js
Color tokens and custom color applicationwidget-src/lib/color-scheme.js
Quick-search and results limit defaults/maxespackages/shared/src/quick-search-limits.ts
App embed blockextensions/vibe-search/blocks/vibe-embed.liquid
Search page blockextensions/vibe-search/blocks/vibe-search-page.liquid
Default locale stringsextensions/vibe-search/locales/en.default.json