The theme contract

A theme changes how a store LOOKS. It never changes what the merchant has authored. Everything a theme does not override falls through to the default design, which is why a new theme is usually two components and a palette rather than a fork of the storefront.

The four things a theme declares

ts
export const layali: ThemeDefinition = {
  tokens: {light: LAYALI_TOKENS, dark: LAYALI_TOKENS_DEEP},
  slots: {
    header: LayaliHeader,
    footer: LayaliFooter,
    homeDefault: LayaliHome,
    plpLayout: LayaliPlp,
    pdpLayout: LayaliPdp,
    productCard: LayaliProductCard,
    // announcementBar and productGrid are omitted on purpose —
    // they fall through to the default design.
  },
  pageVariants: {
    cart: 'compact',
    checkout: 'focused',
    account: 'pills',
    orderView: 'receipt',
  },
  defaultSettings: {
    headerStyle: 'overlay',
    heroOverlay: 'medium',
    footerVariant: 'full',
    cardAspect: 'portrait',
    showSaleBadges: true,
  },
};
  • tokens — a CSS-variable delta, light and dark. Not a full stylesheet: only what differs from the default.
  • slots — a sparse map of slot name to a server component. Header, footer, home page, listing and product arrangement, the product card.
  • blocks — a sparse map of page-builder section type to a component, so the merchant’s own sections render in the theme’s idiom.
  • pageVariants — named looks for the shared cart, checkout, account and order pages.

A theme lives in two halves. The catalogue half carries the id, the bilingual name and description, whether it is premium, the preview image and the settings schema; it is dependency-free so both the server and the merchant dashboard can import it. The rendering half carries the tokens and components. The storefront degrades safely: an id it does not recognise renders as the default rather than failing the page.

Adding one

  1. Register it in the catalogue

    Add the definition with its id, bilingual names, premium flag and settings fields.

  2. Create the theme module

    A directory exporting a ThemeDefinition, registered in the storefront’s theme registry.

  3. Add a preview image

    One SVG under the public themes directory, named for the id.

  4. Mirror the defaults

    Every settings field needs a default in the theme’s defaultSettings, or the merchant sees an empty control.

  5. Translate

    Any new storefront string goes into both message catalogues. Build the Arabic rendering first.

No migration is needed to add a theme — the store’s theme id is a validated free-form string.

The rules that fail silently

Every slot is a server component

One deployment serves every store, so the theme is a runtime value and nothing can be tree-shaken per store. Server components ship no code to the browser, so N themes cost N × 0 client bytes. A slot that becomes a client component puts every theme’s markup into every shopper’s bundle. Interactivity comes from the existing theme-neutral client leaves; a theme that genuinely needs bespoke client code loads it dynamically.

The theme id threads through caches, not the component

Components are not serializable cache arguments, so a cached scope never receives a slot component. It receives the theme id as a string and looks the component up inside the scope. The id then participates in the cache key, which is what makes switching a theme produce a clean miss instead of a half-repainted store.

Never read request data in a slot

Reading headers, cookies or search params inside a slot opts the whole store layout out of its prerendered shell, for every shopper. Anything a slot needs arrives as a prop — the locale included, for exactly this reason.

Tokens are a delta, and the merchant wins

The cascade runs from the base stylesheet, through the theme’s tokens, through the merchant’s theme settings, to their brand colour. A theme sets the character of a store — density, warmth, corner radius — and the merchant’s choices sit on top. A store whose owner picked a large corner radius keeps it on a theme that ships square corners.

Switching a theme never rewrites the home page

A theme changes how sections render, not which sections exist. It may recommend a preset, but applying it is a separate, explicitly confirmed, destructive action — because that is the thing merchants are actually afraid of.

Arabic first, and logical properties only

The default language is Arabic. Build and check the RTL rendering before the LTR one, and use logical properties throughout — start and end, never left and right. Every user-facing string exists in both catalogues.

Settings, and the two kinds of them

Theme settings are validated server-side against the theme’s declared fields at write time, for every theme present in the blob rather than only the active one — a merchant keeps configuration for themes they are not currently using. The storefront then parses defensively anyway: a hand-edited blob must never break rendering.

json
{
  "v": 1,
  "store":  { "productsPerPage": 12, "gridColsDesktop": 4 },
  "themes": { "layali": { "headerStyle": "overlay", "footerVariant": "compact" } }
}
Theme settingsStore settings
DescribeThe look the merchant pickedTheir catalogue and shoppers
Declared inEach theme’s fieldsA shared list
Survive a theme switchNo — kept per theme, restored on switching backYes
ExamplesHeader and footer layout, density, badgesProducts per page, grid columns
Removing a field is not the same as renaming one. A removed key is dropped on write rather than rejected — the dashboard resubmits whatever a store already has, so rejecting a key you deleted would lock those merchants out of saving anything at all. A rename that must carry its value over uses a versioned migration instead.