Theming
What the editor sets
Section titled “What the editor sets”Settings → Look holds the theme: a primary colour, an accent, an optional page
background, light or dark scheme, a corner radius from 0 to 4 px, a logo that
replaces the wordmark, a font stack, and a font stylesheet URL. All of it is one
theme object on the store, validated by themeSchema in shared/schema.ts, and
sent to every shopper in /api/store.
The radius is capped at 4 on purpose. Past that the storefront stops reading as a shop and starts reading as a dashboard, and the range 4 to 24 was almost entirely occupied by looks no shop wanted.
There is one scheme per store, not a per-viewer toggle. A shop’s look is the same in every screenshot anyone takes of it.
Where it lands
Section titled “Where it lands”ThemeVars in the storefront injects the saved theme onto :root as custom
properties. src/lib/theme.ts holds both base palettes and derives the rest:
--beluga-on-primary and --beluga-on-accent are picked by WCAG contrast against
the chosen colour, so white or near-black text is chosen for you.
| Token | Meaning |
|---|---|
--beluga-ink |
Body text |
--beluga-muted |
Secondary text |
--beluga-line |
Hairlines and borders |
--beluga-surface |
Cards, panels |
--beluga-page |
Page background |
--beluga-primary / --beluga-on-primary |
Buttons and the text on them |
--beluga-accent / --beluga-on-accent |
Highlights, sale badges |
--beluga-radius |
Corner radius |
--beluga-gutter / --beluga-measure |
Layout rhythm and line length |
src/index.css carries the light values as a first-paint fallback only. The
dark palette lives only in theme.ts, so there is nowhere for it to drift.
src/lib/theme.test.ts checks the dark palette’s own contrast as arithmetic,
because no browser in CI renders it.
Web fonts and the CSP
Section titled “Web fonts and the CSP”A font stack alone only renders on a machine that already has the face installed.
Font stylesheet URL holds the stylesheet that defines the faces: the Google
Fonts css2?family=… href, or a self-hosted sheet under /assets/.
The store’s Content-Security-Policy allows stylesheets and fonts from 'self'
only, so before this field a Google Fonts <link> added by hand was refused by the
browser with nothing on screen to say why. Saving a URL widens the policy by
exactly the origins that stylesheet needs: Beluga fetches the sheet once and reads
them out of it, which is how fonts.gstatic.com gets allowed. A URL that cannot be
fetched is refused at save. Clearing the field puts the header back exactly as it
was. The preview cannot show a typeface you have not saved yet, because the policy
naming it is the one the store is currently serving.
Writing a component that keeps up
Section titled “Writing a component that keeps up”.card { background: var(--beluga-surface); color: var(--beluga-ink); border: 1px solid var(--beluga-line); border-radius: var(--beluga-radius);}.card button { background: var(--beluga-primary); color: var(--beluga-on-primary);}That is the whole rule. A component styled this way renders correctly today and
still matches after a merchant changes the palette, switches to dark, or sets a
radius. One that hardcodes #fff does not.
The admin has its own theme in src/admin/adminTheme.ts, deliberately not the
store’s. Its blue is meant to look like a tool, not a shop.
When to fork instead
Section titled “When to fork instead”The editor cannot change layout, typography scale, or what a section contains. For that, replace the component. The seams says which ones are safe, and Restyle the storefront does it.