Theme Portability & Design-Fidelity Playbook

This page is the practical companion to Theme Development. That page covers kit structure and the submit pipeline; this one covers the single most important thing to get right — making your components render correctly on the live storefront — plus how to convert a design faithfully and QA it before you submit.

The #1 reason a theme renders unstyled after approval

The storefront compiles your theme's .tsx with its own Tailwind configuration. It does not load your theme's tailwind.config.js, design tokens, styles.css, or fonts. A component that styles itself with config-token classes (bg-primary, text-on-surface, font-display, or any theme-named token like bg-brand, text-ink) will render completely unstyled on the storefront — even though it looked perfect in your local preview and npm run build passed with zero errors. TypeScript cannot catch this. Only a text search for token-named classes can.


1. The six portability laws

Every shipped component must be self-contained in its styling, and every image field must resolve to a real URL before it reaches <Image>.

Law 1 — Colors & sizes are arbitrary Tailwind literals with real hex/px

// ✅ portable — renders identically on the storefront
<h1 className="text-[64px] leading-[72px] tracking-[-0.02em] text-[#700053]">…</h1>

// ❌ NOT portable — renders as unstyled default text on the storefront
<h1 className="text-display-lg text-primary font-display">…</h1>

Standard layout utilities are always fine: flex grid gap-4 px-6 aspect-[3/4] absolute rounded-[12px]. It is only color / typography / font token classes that break.

Law 2 — Fonts are set inline, loaded once by a shared <ThemeHead/>

Do not add a Google-Fonts <link> inside a component, and do not rely on a font-* token class. Create one shared module per theme (e.g. src/themes/custom/<theme>-shared.tsx) that exports a <ThemeHead/> component rendering the font <link> + a <style> block (icon settings, keyframes), and render it once at the top of header.tsx. Then in components:

import { THEME_SERIF, THEME_SANS } from "./<theme>-shared";
<h2 style={{ fontFamily: THEME_SERIF }}>…</h2>
<p  style={{ fontFamily: THEME_SANS }}>…</p>

<ThemeHead/> in the header is not enough — inject the fonts on import too

A shop can replace your header.tsx with a custom header block built in the dashboard (many do — and your own header/footer presets seed one). Your <ThemeHead/> then never mounts, the font <link> is never added, and every heading silently falls back to Georgia or the system font while the CSS still asks for your family. Nothing errors; it just looks wrong.

Make font loading independent of the header: export an idempotent ensureThemeFonts() from your shared module and call it at module scope, so importing any component of your theme loads the fonts (every component already imports that module for the palette/font constants):

```tsx let injected = false; export function ensureThemeFonts() { if (injected || typeof document === "undefined") return; // SSR-safe injected = true; const link = document.createElement("link"); link.rel = "stylesheet"; link.href = FONTS_HREF; document.head.appendChild(link); // …plus a