On this page
01What a theme is
The Impulse Cart storefront is server-rendered HTML with stable, well-named CSS classes. Its engine stylesheet (store.css) owns all the structure: the grid, the nav, where everything sits. Every colour, size and effect in it is a placeholder, for example background: var(--bg) or border-radius: var(--radius).
A theme is nothing more than a block that fills those placeholders in. You set the variables, the browser substitutes them everywhere, and the shop is reskinned. Because a theme cannot change structure, a bad theme can only ever look wrong, never break a store, and every theme survives platform updates.
02The two files
A theme is two files, dropped into theme/css/themes/:
theme/css/themes/
sunrise.css the skin: sets the variables in :root
sunrise.json the manifest: name, author, preview, licence
sunrise is a lowercase slug and must match both file names; it becomes the theme's id. Drop the two files in and the theme appears in the shop's Admin → Appearance automatically. No code change anywhere.
03How to read this reference
Every variable you can set is in the tables below, grouped by what it affects. Each row gives you everything in place:
- What it sets, and where in the shop it lands.
- Values: the actual values you can use. For anything with a fixed set (like a hover effect) every option is listed right in the row; the default is marked.
You only set the variables you want to change; every other one keeps its default. Set them all inside your theme's :root. Anything not listed here is engine internal (see What not to set).
04Colour
Twelve role-based colours. Each accepts any CSS colour: #hex, rgb(), or rgba() (use the alpha channel for translucent "glass" surfaces). The table says exactly what each one paints.
| Variable | What it paints | Default (light) |
|---|---|---|
--bg | The page background, behind everything | #FFFFFF |
--text | Body copy, headings, most text | #1C1C1F |
--text-muted | Secondary text: captions, sub-labels, help | #6B6B72 |
--accent | Buttons, links, prices, active states | #2F2F33 |
--accent-hover | The accent's hover / emphasis shade | #48484E |
--on-accent | Text and icons on top of the accent fill | #FFFFFF |
--panel | Large panels and section backgrounds | #FFFFFF |
--card | Product cards and tiles | #FCFCFD |
--border | Borders and hairlines | rgba(0,0,0,.14) |
--nav | Nav bar and footer chrome | #F6F6F5 |
--menu | Dropdowns, menus, toasts | #FFFFFF |
--field | Form inputs and selects | #FFFFFF |
The soft washes behind hovers and selected rows are computed for you with color-mix() from --accent and --text. You get matching tints free and never write them.
05Type
Two typefaces (body and headings), a body size and leading, a size for every heading level, and heading weight and tracking. Load the fonts yourself; the engine loads none for you. Heading sizes are optional ceilings: leave one unset and that heading keeps the shop's built-in responsive size; set it to cap the desktop size (the value shown is the built-in ceiling).
| Variable | What it sets | Values |
|---|---|---|
| Fonts | ||
--font | Body typeface (and headings, unless overridden) | any font stack · system-ui, sans-serif |
--heading-font | Heading typeface, h1 to h6 | any font stack · inherits --font |
--font-mono | Monospace (order numbers, code) | any font stack · ui-monospace |
| Body text | ||
--font-size | Base body text size | 14px · 15px · 16px · 17px · 18px |
--line-height | Body leading | 1.4 tight · 1.6 · 1.8 loose |
| Headings | ||
--h1 | Page / product title | a size, e.g. 2rem to 3.5rem · 2.5rem |
--h2 | Section headings | a size · 2rem |
--h3 | Sub-sections | a size · 1.5rem |
--h4 | Card titles | a size · 1.25rem |
--h5 | Small headings | a size · 1.1rem |
--h6 | Smallest headings | a size · 1rem |
--heading-weight | Heading font weight | 400 · 500 · 600 · 700 · 800 |
--heading-tracking | Heading letter-spacing | 0 · -0.02em · -0.04em tight |
06Shape, depth and spacing
How round things are, how much they lift off the page, and how tightly the shop is packed.
| Variable | What it sets | Values |
|---|---|---|
| Corners and borders | ||
--radius | Corner radius on cards, panels, inputs, menus | 0 sharp · 6px · 12px · 14px · 20px · 999px pill |
--card-radius | Card corners (own knob, overrides --radius) | same values · 20px |
--border-width | Hairline thickness on borders | 0 none · 1px · 1.5px · 2px bold |
--glass | Backdrop blur on nav and cards | none · blur(8px) · blur(16px) frosted |
| Depth | ||
--shadow | Card elevation (cards are flat by default) | none · any box-shadow |
--shadow-lift | Raised / hover elevation | none · raised default · any box-shadow |
| Spacing | ||
--density | Overall padding + gap multiplier | .85 compact · 1 normal · 1.15 airy · 1.3 spacious |
07Motion and focus
| Variable | What it sets | Values |
|---|---|---|
--hover-speed | How fast hover transitions run | 0s instant · .12s snappy · .18s · .3s smooth |
--focus-ring | Keyboard focus outline colour | any colour · inherits --accent |
You can colour the focus ring, but you cannot remove it. A visible focus outline is required for keyboard and accessibility, so the engine always draws one.
09Cards and images
Cards and product images take their shape from --radius / --card-radius and their depth from --shadow. Each also has one hover effect, picked from its own list right here.
Card hover: --card-hover
Set --card-hover to one of these. Default is lift.
| Value | What happens on hover |
|---|---|
none | No effect |
lift | The card rises, with a soft shadow (default) |
grow | The card scales up slightly |
glow | An accent halo appears |
outline | The border turns accent |
brighten | The card lightens a touch |
Image hover: --thumb-hover
Set --thumb-hover to one of these. Default is zoom.
| Value | What happens on hover |
|---|---|
none | No effect |
zoom | The image scales up inside its frame (default) |
grow | The whole thumbnail scales up |
brighten | The image lightens |
glow | An accent halo appears behind it |
10Contrast rules
A theme is only valid if the text is readable. Hit these before you ship:
| Pair | Minimum | Why |
|---|---|---|
--text vs --bg | 4.5:1 | Body text on the page |
--on-accent vs --accent | 4.5:1 | Button labels on the accent fill |
--accent vs --bg | 4.5:1 | The accent is also link + price text |
text vs --panel/--card/--nav/--menu/--field | 4.5:1 (muted 3:1) | Text sits on these surfaces too |
--border vs --bg | visible | A faint hairline just needs to show |
The accent does double duty: it is a button fill and it is link + price text. So it must be dark or saturated enough to read on the background, and also have an --on-accent colour that reads on top of it.
11What not to set
Do not set the --z-* stacking variables (they order the nav, drawers and toasts) or --content-width (the max content width is engine layout), and never change structure: the DOM, grid tracks, or layout.
Status colours (success green, error red, sale) are fixed engine literals, not variables, so they stay legible in every theme. The focus ring can be recoloured but never removed.
12The manifest
The .json file names the theme and describes its preview swatches. Minimum keys are id, name and licence.
{
"id": "sunrise",
"name": "Sunrise",
"author": "Your Studio",
"description": "A warm, bright light theme.",
"version": "1.0.0",
"licence": "domain",
"preview": {
"bg": "#FFFDFB", "nav": "#FFF3EC", "surf": "#FFFFFF",
"border": "rgba(0,0,0,.12)", "thumb": "#F1E4DB",
"text": "#241C17", "sub": "#7A6E64",
"accent": "#C2410C", "on": "#FFFFFF"
}
}
13A complete example
A full, valid theme. It only sets what it wants to change; every other variable keeps its default.
sunrise.css
/* Sunrise: a warm light theme for Impulse Cart. Skin only. */
:root {
/* colour */
--bg: #FFFDFB;
--text: #241C17;
--text-muted: #7A6E64;
--accent: #C2410C; /* dark enough to read as text */
--accent-hover: #9A3412;
--on-accent: #FFFFFF;
--card: #FFFFFF;
--border: rgba(0,0,0,.12);
--nav: #FFF3EC;
/* type */
--font: "Inter", system-ui, sans-serif;
--heading-font: "Fraunces", Georgia, serif;
--h1: 3rem;
--heading-tracking: -0.03em;
/* shape + depth */
--radius: 12px;
--density: 1.1; /* a little airy */
/* effects: pick one per area */
--btn-hover: glow;
--card-hover: lift;
--thumb-hover: zoom;
}
14Selling your theme
Every marketplace theme is sold, not given away. Set the manifest's licence to "domain"; the theme then needs a licence key tied to the buyer's shop domain, using the same signing scheme as the store licence, so there is almost no new plumbing.
How a sale works: the buyer purchases the theme from the marketplace and gives their shop domain. A key bound to that theme and domain is issued with the sale, and the buyer pastes it into Admin → Appearance. You never handle keys: you build the theme, submit it, and keep 70% of every sale.
With no valid key on the shop's real domain, the theme's CSS is not served at all: the shop falls back to its built-in default skin and shows a persistent "unlicensed" badge, so the paid look is genuinely withheld, not watermarked. A key issued for one domain will not validate on another, so a copied theme and key cannot be resold or shared. On a local dev host the check is bypassed, so you can build freely.
15Ship checklist
<id>.csssets your variables in:root;<id>.jsonhas at leastid,name,licence.idmatches both file names and is a lowercase slug.- Each
--*-hoveris set to one keyword from its list. - All five contrast rules pass.
- You set none of the
--z-*variables and changed no structure. - Checked on both mobile and desktop.
"licence": "domain"is set in the manifest.