Theming the site
This app consumes @evolve-storefront/*. Packages decide structure and role; the brand
decides appearance. Everything below happens in this repo — never in node_modules, never by
forking a package.
Pick the first band that does the job
Section titled “Pick the first band that does the job”Read top down and stop at the first row that covers the change.
| Band | The change you want | Mechanism |
|---|---|---|
| Values | Colour, radius, spacing, control size | Tokens |
| Values | Wording — labels, empty states, aria-labels |
Messages — see evolve-site-messages |
| Appearance | What a role looks like, or a whole colour set | Role CSS and surfaces |
| Appearance | Position, spacing, grid — within the existing DOM | CSS on a data-component hook |
| Behaviour | What the feature does | Typed options on the feature’s hook |
| Behaviour | Content, arrangement, structure | Compose — children, renderItem, slots |
| Behaviour | More or different data | Own the query |
| Markup | Genuinely different markup, everywhere in the build | Alias the subpath — escape hatch |
The bands are blast radius. Values and appearance are CSS and JSON: per-site, no code. Behaviour is typed and per-instance. Markup is build-wide.
Compose first. Anything that is content, arrangement or structure is the compose band — not the alias band. Composition is runtime, typed and per-instance.
Aliasing needs a markup problem. resolveAlias is commented out in next.config.ts and
should stay that way. It lives in one config per build, so it cannot differ per site, and if the
change is visual it buys nothing that CSS did not already buy. Reach for it only when you need
different DOM everywhere and have ruled out every band above.
Where overrides go
Section titled “Where overrides go”The brand’s global.css, unlayered. Package styles ship inside Tailwind’s
components layer, and unlayered CSS beats every layer regardless of specificity — so a plain
selector wins with no escalation.
[data-component="cart-summary-total"] { @apply text-3xl font-bold;}Never wrap an override in @layer — that re-enters the specificity contest for nothing. Never
reach for !important: it reverses layer order and hands the win back to the package.
@apply for properties, plain CSS for tokens. An unknown utility fails the build; an
undefined var() renders nothing and is miserable to debug. @apply has nothing to apply to a
custom property, so tokens are always plain CSS.
[data-component="checkout"] { --flow-gap: 2rem;} /* token: plain CSS */[data-component="cart-total"] { @apply text-3xl font-bold;} /* properties: @apply */A property is an output; a token is an input. Set the property when you are selecting the element you mean. Set a token when the value has to take effect somewhere you are not selecting, or when the component decides how to apply it.
Roles, not looks
Section titled “Roles, not looks”Components name roles and expose them in the DOM. The brand says what a role looks like.
[data-component="button"][data-variant="primary"] { background: var(--color-primary); color: var(--color-primary-foreground);}[data-component="button"][data-tone="destructive"] { --color-primary: var(--color-destructive); --color-primary-foreground: var(--color-destructive-foreground);}data-variant is primary | secondary | tertiary — how much the action stands out.
data-tone is neutral | destructive — what kind of action it is. They are orthogonal, because
prominence and consequence are: a row’s delete is quiet, the delete that confirms it is the
loudest thing on the dialog.
A rank reads a role colour, never inherit. Tone works by re-pointing role variables on the
button itself, and inherit takes the parent’s computed colour — so a quiet destructive action
would render exactly as neutral.
Outline, ghost, pill and link are results of these definitions, not options a component offers.
Buttons have no size prop: size is appearance, set through --control-*, and it varies by
region and breakpoint in a way a fixed scale cannot.
Reference
Section titled “Reference”reference/tokens.md— the token list, the two colour tiers, surfaces, and when to add one.reference/targeting.md— writing selectors that survive: container + role, state, and the names a feature may claim.reference/icons.md— the icon set and how to swap one icon or all of them.
| Rule | Because |
|---|---|
Style through data-component |
It is invariant and on every primitive; nothing else is a hook |
| Override files are unlayered | Layer order beats specificity, so plain selectors win |
No @theme value contains var() |
An alias resolves at :root and can never be re-pointed in a subtree |
| A retired token is removed, never aliased | A stale override should be inert and greppable |
| Reach specific cases by container + role, or by state | :first-child breaks the day a button is added |
Never use CSS order to move interactive elements |
Visual order must match DOM order for keyboard and screen-reader users |
Do not extend a local shadcn-derived ui package |
Its CVA/size/data-slot model conflicts with everything above |
Every @evolve-storefront/* package’s styles.css must be imported in global.css, or Tailwind
never scans that package and generates none of its classes. The failure is silent — it shows
up as one broken component rather than an obviously unstyled page.

