Litestore · Developer guide
Storefront

Theming

See how Litestore's themes are defined and kept separate for the storefront and the admin, so you know where a visual change belongs.

A theme decides how the storefront or the admin looks. Read this page before you change a colour, add a theme, or style a single block.

A theme is a complete visual definition. It sets colours, typography, spacing, borders, shadows, effects and layout, and a Zod schema in lib/theme/theme-schema.ts validates it.

Themes are files in config/themes/, and a store setting picks which one is active.

NewLinen shirtLight, breathable, made to last.$48.00Add to cart
Changing the theme's colour or corner radius restyles every component that reads its tokens.

The idea behind this design is that a theme is data, not code. A component never asks which theme is active. It reads the tokens that the active theme provides. Because of that, you can add a new theme without editing components all over the codebase.

The four themes

config/theme.ts collects the theme files into one registry:

  • starter is for the admin and is the admin default.
  • starter-web is for the storefront.
  • amelia is for the storefront and is the storefront default.
  • brutal is for the storefront.

Every theme declares a target, which is either web or admin. Three of the four themes are for the storefront. Only starter styles the dashboard.

The storefront and the admin are separate

Litestore has two independent settings for the active theme:

storefront_active_theme
admin_active_theme

Changing one does not change the other. lib/theme/settings.ts reads them with getStorefrontActiveThemeId() and getAdminActiveThemeId(), so each side looks up its theme under its own key.

The code enforces this split. assertThemeTarget throws if you apply a theme to a target it was not written for:

Theme "amelia" is a storefront theme and cannot be used for the admin dashboard.

Applying an admin theme to the storefront fails in the same way. The theme-switching action in server/admin/theme/actions.ts calls assertThemeTarget before it writes the setting, so an invalid combination never reaches the database.

There is no dark mode

Each theme has a single palette. Nothing in config/themes/ or lib/theme/ generates a dark version, and shoppers have no theme toggle.

What a palette declares

lib/theme/theme-schema.ts
export const colorPaletteSchema = z.object({
  primary: z.string(),
  primaryForeground: z.string(),
  secondary: z.string(),
  secondaryForeground: z.string(),

  background: z.string(),
  foreground: z.string(),
  card: z.string(),
  cardForeground: z.string(),
  muted: z.string(),
  mutedForeground: z.string(),

  border: z.string(),
  input: z.string(),
  ring: z.string(),

  success: z.string(),
  // …warning, danger, info, each with a foreground
  accent: z.string().optional(),
  accentForeground: z.string().optional(),
})

The semantic colours are required and the accents are optional. A theme can't ship without a colour for the danger state.

The palette is one part of the full theme. themeSchema also requires typography, spacing, borders, shadows and effects. It requires metadata such as id, name and target too. A theme can also have optional customCss, and storefront themes can have optional webTokens.

If a theme is missing a required group, it doesn't render badly. It fails Zod validation.

The boundary the harness enforces

scripts/check-theme-boundaries.ts makes sure that theme-specific CSS selectors and theme ids appear only in config/theme.ts and config/themes/.

The script reads every file in the repository with one of these extensions, skipping node_modules and the build directories:

.css  .ts  .tsx  .js  .jsx  .json  .md  .mjs

For each file, it asks one question:

Does this file name a specific theme?

It flags four cases:

  1. A [data-theme="amelia"] selector outside the theme config.
  2. Any theme-specific selector or Amelia override in app/styles.css.
  3. Storefront theme CSS in config/theme.ts that targets data-admin-theme.
  4. The bare theme id amelia outside the theme config, the settings defaults, tests or the theme detail route.

The script removes block comments before scanning. A comment that only mentions a theme is therefore not reported as a leak.

A short allowlist covers files where naming a theme is legitimate. These are the theme CSS test, the settings schema, lib/defaults.ts, prisma/seed.ts and the discipline scripts that describe theme rules in prose.

As a result, a component can't behave differently depending on which theme is active. If components could do that, every new theme would mean editing each of them.

A separate rule covers hard-coded colours. The web-hardcoded-color rule in scripts/check-discipline.ts scans components/web and app/(web) for a Tailwind arbitrary value that contains a raw hex, rgb() or hsl() value. A var(--…) reference is fine, because it contains no literal colour. OG images are allowed, since Satori cannot read CSS variables.

Block palettes

Litestore derives block colour palettes from the active theme, and operators can't edit them.

If an operator wants a palette of their own, they duplicate the derived palette into custom_color_palettes instead of editing the theme.

A block palette overrides the same --color-* tokens the block already reads from the theme, but only for that block. The values are CSS hex strings so that the admin colour inputs can read and save them unchanged.

Where a visual change goes

If the change should follow whichever theme is active, use theme tokens. This is the usual case, and you don't need a new file.

If the change really belongs to one theme only, put it in that theme's customCss in config/themes/. That is the only place a [data-theme="…"] selector is allowed.

If the change belongs to one block on one page rather than to the whole store, use a block palette. Duplicate the derived palette instead of editing the theme it came from.

If the change is a whole new look, add a new file under config/themes/ and set its target to the side it was written for.

Before you add a colour, don't ask where you can put it so that it renders. Ask this:

If someone switches the theme tomorrow, does this change follow them, or does it become the one thing that still looks wrong?

On this page