Skip to main content

Shopify Storefront Styling Architecture

Overview

The storefront search widget uses a three-layer CSS architecture that separates merchant-configurable design tokens from template structure and custom overrides. This makes the styling system non-brittle — merchants can edit templates without breaking admin UI controls, and developers can add new controls without touching templates.

The Three Layers

Layer 1: CSS Variables (auto-generated from settings)
↓ injected first
Layer 2: Template CSS (editable in Advanced editor, uses var() references)
↓ injected second
Layer 3: Custom CSS (merchant escape hatch)
↓ injected last, highest priority

Layer 1 — CSS Variables

Generated by: storefront_search/src/css-variables.ts Source of truth: UIComponent.value fields in DynamoDB/metafields Scoped to: .marqo-search-layout (not :root)

Layer 1 reads design tokens (colors, sizes, fonts, radii) from window.MarqoUIConfig.uiComponents and generates a CSS variable block:

.marqo-search-layout {
--marqo-sale-badge-bg: #dc2626;
--marqo-card-border-radius: 8px;
--marqo-vendor-font: Georgia, serif;
/* ... ~53 variables total */
}

It also generates dynamic @media rules for responsive columns and the configurable filter mobile breakpoint.

Key rule: Layer 1 only contains values, never structure or layout rules.

Layer 2 — Template CSS

Stored in: UIComponent.css fields (e.g., product_card.css, filters.css) Editable by: Merchants via the Advanced Template Editor Uses: var(--marqo-*, fallback) references

.marqo-sale-badge {
background: var(--marqo-sale-badge-bg, #dc2626);
position: absolute !important;
top: 8px !important;
}

Every var() must include a fallback value that matches the default in ui_settings.py. This ensures the template works even if Layer 1 fails to load.

Key rule: Layer 2 contains structure and layout. It references variables but never sets them.

Layer 3 — Custom CSS

Stored in: custom_css.value.css Editable by: Merchants via the Custom CSS editor Purpose: Override anything. Escape hatch for power users.

.marqo-sale-badge { background: hotpink !important; }

Injection Order

In storefront_search/src/utils.tsloadStylesFromSettings():

  1. generateCssVariables(uiComponents) → Layer 1
  2. Loop through uiComponents[key].css → Layer 2
  3. custom_css.value.css → Layer 3

All concatenated and injected into a single <style id="marqo-search-styles"> tag.

Note: marqo-loader.js also injects styles early (FOUC prevention). The bundle's loadStylesFromSettings() overwrites the loader's injection with the full three-layer output.

Adding a New Admin Control

Step 1: Define the setting

File: ecom_admin/app/lib/types.ts

Add the field to the appropriate interface:

export interface ProductDisplaySettings {
// ... existing fields
newProperty: string; // Add here
}

Step 2: Add the default value

File: admin_server/admin_server/models/ui_settings.py

Add to the backend defaults so it exists from the start:

"product_display_config": UIComponent(
value={
# ... existing fields
"newProperty": "default_value",
},
),

File: ecom_admin/app/hooks/use-settings.ts

Add the same default to the frontend:

productDisplay: {
// ... existing fields
newProperty: "default_value",
},

Step 3: Add the CSS variable

File: storefront_search/src/css-variables.ts

Add to generateCssVariables():

vars.push(`--marqo-new-property: ${sanitize(dc.newProperty, 'default_value')}`);

Naming convention: --marqo-{component}-{property}

  • Components: sale-badge, cta, card, vendor, title, price, filter, sort-dropdown, page-dropdown, results, star, review
  • Always use kebab-case

Step 4: Use the variable in template CSS

File: admin_server/admin_server/constants/templates.py

Replace the hardcoded value with a var() reference:

/* Before */
.marqo-element { some-property: default_value; }

/* After */
.marqo-element { some-property: var(--marqo-new-property, default_value); }

Important: The fallback in var() MUST match the default in ui_settings.py.

Step 5: Add the UI control

File: ecom_admin/app/components/settings/{section}-section.tsx

Add a color picker, slider, toggle, etc.:

<FieldGroup label="New Property">
<ColorPicker
value={display.newProperty}
onChange={(color) => updateDisplay({ newProperty: color })}
label="New Property"
/>
</FieldGroup>

Step 6: Update the settings converter

File: ecom_admin/app/lib/settings-converter.ts

The converter translates between frontend types and backend UIComponent format. If your new field is inside an existing component's .value, it usually passes through automatically. But verify:

  • backendToFrontend(): reads the value from the backend component
  • frontendToBackend(): writes the value back to the backend component

Step 7: Update the admin preview

File: ecom_admin/app/components/preview/search-preview.tsx

If applicable, add the setting to the live preview.

Step 8: Update the variables preview list

File: ecom_admin/app/components/settings/advanced-template-editor.tsx

Add the new variable name to the CssVariablesPreview component so merchants can see it when editing Advanced CSS.

What Goes Where

Type of valueLayerExample
ColorsLayer 1 (variable)--marqo-sale-badge-bg: #dc2626
Font sizesLayer 1 (variable)--marqo-vendor-size: 12px
Font familiesLayer 1 (variable)--marqo-vendor-font: Georgia, serif
Font weightsLayer 1 (variable)--marqo-vendor-weight: 500
Border radiiLayer 1 (variable)--marqo-card-border-radius: 8px
Border widthsLayer 1 (variable)--marqo-card-border-width: 1px
SpacingLayer 1 (variable)--marqo-column-spacing: 16px
Column counts per breakpointLayer 1 (dynamic @media)repeat(4, 1fr) at desktop
Filter mobile breakpointLayer 1 (dynamic @media)@media (max-width: 768px)
Positioning (absolute, flex, grid)Layer 2 (template CSS)position: absolute; top: 8px
Element visibilityLayer 2 (template CSS)display: none
Transitions/animationsLayer 2 (template CSS)transition: all 0.3s ease
Responsive scaling ratiosLayer 2 (template CSS)calc(var(--size) * 0.9)
Content (text, labels)Handlebars context{{saleBadgeLabel}}
Behavioral togglesHandlebars context / JS{{#if ctaEnabled}}
Per-item dynamic valuesInline styles (exceptions)style="width:{{reviewPercentage}}%"

Inline Style Exceptions

These inline styles MUST remain as Handlebars variables — they are per-item dynamic values, not design tokens:

StyleReasonLocation
style="width:{{reviewPercentage}}%"Per-product star fill.marqo-stars-fg
style="display:none"Okendo widget hydrationdata-oke-reviews div
style="{{filterToggleStyle}}"Conditional visibility.marqo-filter-toggle

Rule: If the value differs per product card or depends on runtime JS logic, it stays as an inline style. If it's a design token that the merchant configures once, it becomes a CSS variable.

Responsive Design

Layer 1 handles (values differ per breakpoint):

  • Column counts: repeat(${mobile}, 1fr) / repeat(${tablet}, 1fr) / repeat(${desktop}, 1fr)
  • Filter mobile breakpoint: @media (max-width: ${filterMobileBreakpoint}px)

Layer 2 handles (proportional scaling):

  • Font sizes: calc(var(--marqo-vendor-size) * 0.9) at tablet, * 0.8 at mobile
  • Spacing: calc(var(--marqo-column-spacing) * 0.75) at tablet, * 0.5 at mobile

Merchants control the base values (desktop). The scaling ratios are in template CSS. Power users can change ratios in the Advanced editor.

HTML Templates and Data Attributes

HTML templates use class names and data attributes, not inline styles for design tokens:

<!-- Good: class + data attribute, colors from CSS variables -->
<span class="marqo-sale-badge" data-position="{{saleBadgePosition}}">
{{saleBadgeLabel}}
</span>

<!-- Bad: inline styles for design tokens (old pattern, don't do this) -->
<span style="background:{{saleBadgeBg}};color:{{saleBadgeText}}">

Position-based logic uses data attributes + CSS:

.marqo-sale-badge { left: 8px; right: auto; }
.marqo-sale-badge[data-position="top-right"] { left: auto; right: 8px; }

Alignment uses data attributes on the grid container (set by JS in ui-rendering.ts):

.marqo-results-grid[data-align="center"] .marqo-product-card-content { text-align: center; }

Advanced Template Editor

The admin's Advanced Template Editor lets merchants edit raw HTML/CSS. To keep this non-brittle:

Validation Warnings

When merchants edit HTML, the editor scans for expected CSS class names and warns if any are missing:

Missing elements: .marqo-sale-badge — Sale badge controls won't have any effect

Expected elements are defined in EXPECTED_ELEMENTS in advanced-template-editor.tsx.

Reset to Default

Each template has a "Reset" button that:

  • Fetches defaults from /api/v1/storefront/defaults
  • Replaces .html and .css on the backendSnapshot
  • Preserves .value — merchant's color/toggle settings are kept

CSS Variables Preview

A collapsible panel shows all available --marqo-* variables so merchants know what they can reference when editing CSS.

CSS Namespacing

All CSS class names MUST use the marqo- prefix to avoid conflicts with Shopify themes:

marqo-product-card, marqo-sale-badge, marqo-filter-sidebar, marqo-active

Never use generic class names like active, primary-image, hidden.

Suppression

The suppression system hides native Shopify elements (search grids, pagination, filters) so Marqo can replace them. Suppression is applied in three places:

  1. Liquid template (marqo-search-embed.liquid) — server-side, only on search/collection pages via request.page_type
  2. Loader (marqo-loader.js) — client-side, gated by isSearchPage() || isCollectionPage()
  3. Bundle (suppression-manager.ts) — client-side, gated by getPageType()

All three must be gated to search/collection pages. Suppression on homepage/product/cart pages hides unrelated content.

File Reference

FilePurpose
storefront_search/src/css-variables.tsLayer 1: generates CSS variables + dynamic responsive rules
storefront_search/src/utils.tsloadStylesFromSettings(): three-layer injection
admin_server/constants/templates.pyLayer 2: HTML/CSS templates with var() references
admin_server/models/ui_settings.pyBackend defaults for all settings
ecom_admin/app/lib/types.tsTypeScript interfaces for all settings
ecom_admin/app/lib/settings-converter.tsFrontend ↔ backend format translation
ecom_admin/app/hooks/use-settings.tsFrontend defaults + save/load hook
ecom_admin/app/components/settings/advanced-template-editor.tsxValidation, reset, variables preview
extensions/marqo-search-theme/assets/marqo-loader.jsEarly style injection, prefetch, suppression
extensions/marqo-search-theme/blocks/marqo-search-embed.liquidServer-side config injection + suppression