ProductGeniusSearchBar
The Product Genius search bar mount for React storefronts — a working native search styled as a skeleton that the live bar replaces.
Skip this page unless your store also uses Product Genius search, not just the feed.
On Shopify Liquid themes, search-bar placement ships as a theme app extension block, "PG
Search Bar" — see the Shopify section. <ProductGeniusSearchBar> is the
same thing for storefronts without a Liquid theme.
import {ProductGeniusSearchBar} from '@productgenius/react';
// full-width bar:
<ProductGeniusSearchBar placeholder="Search products…" maxWidth={400} />
// or icon-only — a compact square the live bar expands on focus:
<ProductGeniusSearchBar layout="icon" />What you are mounting
The live bar that replaces the skeleton is Product Genius search: LLM-based query understanding that handles typos, synonyms, and ambiguous terms; abstract queries like "a gift for my mom"; results that keep refining as the shopper scrolls; and a transition into interest-based suggestions as results are exhausted. Ranking policies for shopper segments, and the live bar's own styling, are configured on the Product Genius side with your contact — your storefront's job is to give it a mount point in the right place.
Progressive enhancement, not a placeholder
What the component renders is a working native search styled as a loading shimmer: a
real <form method="get"> pointed at your own search route in bar layout, or a real link
to that route in icon layout. The Product Genius bundle replaces it with the live search
bar once it mounts.
Until then — and if the bundle never loads at all — your visitors still have search that works. That is the intended posture, not a degraded one.
The component is purely presentational: no effects, no globals, no data fetching. It renders in your initial server HTML, so the skeleton paints with the page instead of after hydration, and it never causes layout shift.
The handoff is attribute-driven
When the live bar mounts into the container, the bundle sets data-pg-search-ready="true"
on it and CSS hides the skeleton. The skeleton stays in the DOM — React and the bundle
never fight over the container's children.
Prerequisites
Two things have to be true before the live bar can appear.
The bundle must be loaded on the page
<ProductGeniusSearchBar> does not load it — <ProductGeniusFeed>
does. Render the feed component on every page that shows the search bar. On non-product
routes, omit pageConfig; its mount node simply stays empty unless your store's
configuration renders a feed there.
When the live bar appears depends on the page: where your configuration renders a feed, the search bar mounts together with it (after the feed's container resolves and its first fetch completes); on pages without a feed it mounts standalone. Until then, and if it never mounts, the skeleton stays a working native search.
Your store's search configuration must be enabled
Set by your Product Genius contact, like the rest of your store configuration:
| Key | What it does |
|---|---|
search.pagePatterns | The pages where the search bundle may mount. Without it, search stays off on pages that don't render a feed. |
design.search.enabled | Enables the search bar on feed pages. |
design.search.container | Must be [".pg-search-mount"] — the class this component renders. |
design.search.action.input | Must match your layout: "open" for bar, "collapsed" for icon. |
Placement rules
One mount per page
The bundle resolves the container with querySelector — first match — and mounts one
live bar into it. A second <ProductGeniusSearchBar> never goes live; its skeleton shimmers
forever. For a desktop/mobile pair, render a single instance conditionally. Do not
render two and hide one with CSS: a display: none first match still wins querySelector.
It must exist at bundle init
The bundle looks for .pg-search-mount once, when it initializes. Render the component
somewhere that exists at page load — a persistent header — not inside lazily-mounted UI like
a search drawer or a modal. A skeleton that enters the DOM later is only picked up after a
full bundle reboot, which means <ProductGeniusFeed> unmounting and remounting.
If your design opens search in a drawer or modal, put the component in your persistent
header anyway and let the live bar handle its own expansion — that is what layout="icon"
is for: a compact square in the header that the live bar widens on focus.
Props
| Prop | Type | Default | Notes |
|---|---|---|---|
layout | 'bar' | 'icon' | 'bar' | bar renders a full search field; icon renders an icon-only square that links to your search page, which the live bar expands on focus. Must match the store's design.search.action.input ("open" / "collapsed"). |
placeholder | string | 'Search' | The input placeholder, and the accessible label of the input (or of the icon link). Match your store's configured placeholder so the text doesn't change at handoff. |
maxWidth | number | 320 | Container max-width in px. In icon layout, also the width the live bar expands to on focus. |
height | number | 38 | Bar height in px. Match your store's configured live-bar height so the handoff is shift-free; the default matches the Product Genius default. |
action | string | '/search' | Your native search page — the fallback form's GET target and the icon link's href. The default is Shopify's route; point it at your own otherwise. |
inputName | string | 'q' | The query-param name the fallback form submits (/search?q=…). Default is Shopify's param. bar layout only — the icon layout renders a plain link, with no input. |
onSubmit | FormEventHandler<HTMLFormElement> | — | Intercept the fallback form's submit, e.g. preventDefault() and push to your client-side router. Omit it to keep the plain GET, which works with no JS at all. bar layout only. |
nonce | string | — | CSP nonce for the inline <style> this component renders. See below. |
id | string | — | Passed to the container. |
className | string | — | Merged after pg-search-mount, which the bundle's container selector needs. |
style | CSSProperties | — | Merged after the sizing custom properties, so it wins per-property. |
action lands in an href verbatim
action must be a same-site path or an https: URL you control. Never feed untrusted
(CMS- or user-supplied) data into it.
RSC note: functions don't cross the server-to-client boundary, so passing onSubmit
requires the component that renders the search bar to be a Client Component (in the Next.js
App Router, for example). Without onSubmit, you can render it straight from a Server
Component.
Styling the skeleton
Styles are injected as a small inline <style> per instance — nothing to import, SSR-safe.
The skeleton's colours, radius, and sizing are CSS custom properties on the container,
declared at zero specificity so any rule of yours overrides them:
| Custom property | Default | Applies to |
|---|---|---|
--pg-skeleton-radius | 0px | Corner radius |
--pg-skeleton-bg | #ffffff | Background |
--pg-skeleton-border | 1px solid #d4d0cd | Border shorthand |
--pg-skeleton-icon-color | #8a8a8a | The magnifier icon |
--pg-skeleton-color | #1f1f1f | Typed text |
--pg-skeleton-placeholder-color | #9f9f9f | Placeholder text |
--pg-skeleton-size | from height (38px) | Bar height, and the icon square's width |
--pg-compact-expanded-width | from maxWidth (320px) | icon layout: width while focused |
Override them from your own stylesheet:
.pg-search-mount {
--pg-skeleton-radius: 8px;
--pg-skeleton-bg: #faf9f7;
}Or per instance via the style prop:
style={{'--pg-skeleton-radius': '8px'} as CSSProperties}.
Two caveats:
- If you override sizes through
stylerather than themaxWidthprop, set--pg-compact-expanded-widthalongside it — iniconlayout the expanded width is derived from the prop, so astyle={{maxWidth}}override moves the container cap but not the expanded width. - On a hybrid page that also loads the Shopify theme app extension's
pg-search.css, that stylesheet declares the same defaults at class specificity, so an equal-specificity override of yours can lose on document order. Thestyleprop override always wins there.
This styles the skeleton only. The live search bar's appearance is part of your store's Product Genius design configuration, like the feed's. The skeleton cannot read the live bar's variables — they are scoped to the bundle's own markup and don't exist until it mounts — so set the skeleton's height, colours, and radius to match your store's configured bar and the swap is seamless.
The shimmer animation is disabled under prefers-reduced-motion: reduce, and the fallback
input carries a visible :focus-visible outline, since pre-handoff it is the only focusable
control.
Placeholder contrast
The default placeholder colour (#9f9f9f on white, roughly 2.6:1) mirrors the Product
Genius default and is below WCAG AA's 4.5:1. If your store needs AA placeholder contrast,
override --pg-skeleton-placeholder-color and ask your Product Genius contact to match
the live bar's placeholder styling, so the handoff stays seamless.
CSP and the inline <style>
The component renders one inline <style> element per instance. Pass the nonce prop
whenever your style-src doesn't allow inline styles without a nonce — remember that
browsers ignore 'unsafe-inline' when the directive also carries a nonce or a hash.
This is a different nonce use from the feed's: <ProductGeniusFeed nonce> nonces an
injected <script> (script-src), while <ProductGeniusSearchBar nonce> nonces an inline
<style> (style-src). In a framework with a per-request nonce, it's usually the same
value passed to both. See Content-Security-Policy.
How is this guide?