Custom Storefront UI
Customize an existing PSYKHE AI Shopify integration with theme CSS and supported storefront hooks.
Use the approaches on this page when the PSYKHE AI Shopify app and theme blocks are already installed and configured. These examples change presentation and theme behaviour; they do not replace the integration's API configuration, search state, or analytics.
Add custom code to a draft or unpublished theme first. Keep the customization scoped to the theme container that holds the PSYKHE AI blocks.
Choose The Smallest Customization
Start with the least invasive option that meets the storefront requirement:
| Requirement | Supported approach |
|---|---|
| Change fonts, colours, spacing, radii, or grid gaps | CSS custom properties |
| Style a specific element inside a PSYKHE AI component | ::part() |
| Replace a small piece of supplied content | A documented slot |
| Configure product cards beyond CSS alone | renderCard(product, ctx, meta) |
| Add theme-owned search or collection links | suggestionsFetcher and renderQueryChip |
| Reinitialize custom JavaScript when Shopify reloads a section in the theme editor | shopify:section:load |
Prefer CSS custom properties and ::part() before replacing markup with
JavaScript.
Theme CSS Custom Properties
CSS custom properties apply across the PSYKHE AI components inside their container. The following example aligns the integration with a neutral Shopify theme while keeping the rules scoped:
.psykhe {
--psykhe-ui-font-family: var(--font-body-family, inherit);
--psykhe-ui-text-color: var(--color-foreground, #121212);
--psykhe-ui-surface-color: var(--color-background, #ffffff);
--psykhe-ui-border-color: color-mix(
in srgb,
var(--color-foreground, #121212) 18%,
transparent
);
--psykhe-ui-accent-color: var(--color-foreground, #121212);
--psykhe-ui-radius-md: 0;
--psykhe-ui-button-radius: 0;
--psykhe-ui-card-aspect-ratio: 4 / 5;
--psykhe-ui-grid-gap: 1.5rem;
}Use literal fallback values because Shopify themes do not all expose the same CSS variables.
Styling Component Parts
PSYKHE AI components use Shadow DOM. Supported ::part() selectors let the
theme style specific component elements without depending on internal class
names or DOM structure.
/* Product cards rendered inside the product grid */
.psykhe psykhe-product-grid::part(vendor) {
color: color-mix(
in srgb,
var(--color-foreground, #121212) 65%,
transparent
);
font-size: 0.75rem;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.psykhe psykhe-product-grid::part(title) {
font-size: 0.875rem;
font-weight: 400;
line-height: 1.35;
}
.psykhe psykhe-product-grid::part(price) {
font-size: 0.875rem;
}
.psykhe psykhe-product-grid::part(compare-price) {
opacity: 0.6;
}
/* Filters and active refinements */
.psykhe psykhe-filter-panel::part(pill-trigger),
.psykhe psykhe-active-filters::part(chip) {
border-radius: 999px;
}
/* Search */
.psykhe psykhe-search-input::part(input) {
border-radius: 0;
border-width: 0 0 1px;
}
.psykhe psykhe-search-input::part(suggestions) {
border: 1px solid color-mix(
in srgb,
var(--color-foreground, #121212) 18%,
transparent
);
box-shadow: none;
}
.psykhe psykhe-search-input::part(query-chip) {
border-radius: 999px;
}
/* Mobile drawer */
.psykhe psykhe-drawer::part(panel) {
width: min(92vw, 28rem);
}
.psykhe psykhe-drawer::part(header),
.psykhe psykhe-drawer::part(footer) {
border-color: color-mix(
in srgb,
var(--color-foreground, #121212) 18%,
transparent
);
}Commonly useful parts include:
| Component | Parts |
|---|---|
psykhe-product-grid | grid, item, plus product-card parts re-exported by its renderer |
psykhe-product-card | card, media, image, vendor, title, pricing, price, compare-price, badge |
psykhe-filter-panel | base, pill-trigger, option-label, input |
psykhe-active-filters | base, chips, chip, chip-label, remove |
psykhe-search-input | base, form, input, clear, suggestions, query-chip, vendor, title, price |
psykhe-drawer | backdrop, panel, header, heading, body, footer, close-button |
Only target documented part names. Do not traverse a component's shadow root or
depend on internal classes. Product cards inside psykhe-product-grid cross two
shadow boundaries, so target the parts re-exported by the grid as shown above.
Use psykhe-product-card::part(...) only when the card is a direct child of
theme-owned markup.
Custom Product Cards
Use a render callback when the theme needs per-card configuration or slotted
content that CSS cannot provide. A robust Shopify integration normally keeps
psykhe-product-card for its images, pricing, navigation, accessibility, and
tracking, then configures it from the callback.
The third callback argument contains the card's position. Pass that index to the product card and re-export any parts the theme needs to style:
async function installThemeProductCards(root = document) {
await Promise.all([
customElements.whenDefined("psykhe-product-grid"),
customElements.whenDefined("psykhe-product-card"),
]);
root.querySelectorAll("psykhe-product-grid").forEach((grid) => {
if (grid.dataset.themeCardInstalled === "true") return;
grid.dataset.themeCardInstalled = "true";
grid.renderCard = (product, ctx, meta) => ctx.html`
<psykhe-product-card
class="theme-product-card"
exportparts="card,link,media,image,vendor,title,pricing,price,compare-price,badge"
.product=${product}
.index=${meta.index}
.showVendor=${false}
.showCompareAtPrice=${true}
.showSalePriceFirst=${true}
></psykhe-product-card>
`;
grid.requestUpdate?.();
});
}Keep the normalized product object intact rather than reconstructing it from
platform-specific metadata. This lets the product card retain the normal SDK
behaviour while the theme controls its presentation through properties, slots,
and exported parts.
Collection Quick Links In Search
Quick links do not require a separate predictive-search theme component. The
theme can wrap the suggestion fetcher already assigned to
psykhe-search-input, then add store-owned collection URLs to the query-chip
row. Production integrations can derive these links from fetched product
metadata; this smaller example uses a merchant-approved list of store URLs.
async function installSearchQuickLinks(root = document) {
await customElements.whenDefined("psykhe-search-input");
root.querySelectorAll("psykhe-search-input").forEach((input) => {
if (input.dataset.themeQuickLinksInstalled === "true") return;
if (typeof input.suggestionsFetcher !== "function") return;
input.dataset.themeQuickLinksInstalled = "true";
const fetchSuggestions = input.suggestionsFetcher.bind(input);
const quickLinks = [
{ value: "new-arrivals", label: "New arrivals", url: "/collections/new-arrivals" },
{ value: "dresses", label: "Dresses", url: "/collections/dresses" },
{ value: "accessories", label: "Accessories", url: "/collections/accessories" },
];
input.suggestionsFetcher = async (query, options) => {
const result = await fetchSuggestions(query, options);
const products = Array.isArray(result) ? result : result.products;
const existingQueries = Array.isArray(result) ? [] : result.queries ?? [];
return {
products,
queries: existingQueries.length ? existingQueries : quickLinks,
};
};
input.renderQueryChip = (suggestion, ctx) => {
const label = suggestion.label ?? suggestion.value;
if (!suggestion.url) {
return ctx.html`
<button
part="query-chip"
type="button"
@mousedown=${(event) => event.preventDefault()}
@click=${() => ctx.actions.selectSuggestion(suggestion)}
>${label}</button>
`;
}
return ctx.html`
<a
part="query-chip"
href=${suggestion.url}
@mousedown=${(event) => event.preventDefault()}
>${label}</a>
`;
};
input.requestUpdate?.();
});
}The collection handles above are examples. Use URLs that exist in the Shopify store, or render merchant-managed collection links into the theme before installing the callback. The PSYKHE AI hook controls presentation; Shopify remains the source of truth for collection URLs. Localize any labels displayed to shoppers through the theme's normal translation or merchant-setting flow.
Shopify Theme-Editor Lifecycle
Theme sections can be re-rendered without a full page load. Keep installation idempotent and run it again for newly loaded sections:
async function installPsykheThemeCustomizations(root = document) {
await Promise.all([
installThemeProductCards(root),
installSearchQuickLinks(root),
]);
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => {
installPsykheThemeCustomizations();
}, { once: true });
} else {
installPsykheThemeCustomizations();
}
document.addEventListener("shopify:section:load", (event) => {
installPsykheThemeCustomizations(event.target);
});If a customization adds its own long-lived event listeners, remove those
listeners on shopify:section:unload.
Tracking Boundary
The examples above extend PSYKHE AI components and preserve their supplied actions. Do not publish duplicate list-view or product-click events from these callbacks.
Use Custom UI Tracking only when custom theme code owns the complete product grid and PSYKHE AI components are not managing the list experience.
Integration Guidance
- Keep selectors scoped to the theme's PSYKHE AI container.
- Wait for a custom element before assigning JavaScript properties to it.
- Make installation idempotent so Shopify section reloads do not wrap callbacks repeatedly.
- Preserve supplied
ctx.actionshandlers when replacing interactive markup. - Use real Shopify product and collection URLs rather than reconstructing routes from labels.
- Test keyboard navigation, mobile drawers, filtering, sorting, pagination, and browser-back behaviour in a draft theme.
- Avoid internal package paths, shadow-root traversal, and copied integration source.