Guide 03 · Developer docs

The theme repo and the asset pipeline

Everything a developer changes on a Core dna site lives in its theme repository. This guide covers the repository and its build, printing records in templates, stylesheets and client-side code, and the parts of a site around the main content.

L1–L2
Unit 03 · Stylesheets and SCSS

Design tokens as custom properties

On a multi-brand site, colours and fonts belong in CSS custom properties, set per brand or per page at render time from values an administrator controls, so one stylesheet serves every brand. The override pattern in both engines, the per-brand build it replaces, and what custom properties cannot do.

Who this is for. The developer styling a site that serves several brands, or pages an editor can recolour. Afterwards you can keep one stylesheet for all of them and change a brand's look without a build. Choosing a multi-brand architecture in the first place is Several hostnames on one site, or several sites in Guide 1.

Why custom properties, not SCSS variables

An SCSS variable is resolved when the build runs: $primary becomes a fixed colour in the output, and changing it means building and deploying again. A CSS custom property is resolved by the browser, per element, and inherited: set --primary on body and every component inside uses the new value, with the same stylesheet. That is what lets one build serve several brands, and lets a value an administrator changes take effect on the next render.

The pattern

Define every token with the default brand's value in SCSS, and have components use only the tokens:

SCSS generic/_tokens.scss
:root {
  --primary: #21468b;
  --heading-font: 'Figtree', sans-serif;
}

.button { background: var(--primary); }
h1, h2 { font-family: var(--heading-font); }

Then print the overrides into the page from values that live outside the repository. A production multi-brand site keeps each site's tokens in site data, as a JSON entry styles mapping names to values, and prints them as custom properties on body:

Template code
{$tokens = $_siteData->get('styles')}
<body{if is_array($tokens)} style="{foreach $tokens as $name => $value}--{$name|escape}:{$value|escape};{/foreach}"{/if}>
{% set tokens = _siteData.get('styles') %}
<body{% if tokens is iterable %} style="{% for name, value in tokens %}--{{ name|e('html_attr') }}:{{ value|e('html_attr') }};{% endfor %}"{% endif %}>

The same site lets a template, and then a single record, override the site's tokens: it merges three sets in that order before printing, so a campaign page can change its accent without touching the brand. Values on body or a wrapper override :root, and a value set on an element overrides one set on its parent, so the most specific source wins without any extra rules.

Site data is edited by backend users only, which is why its values can be printed into CSS. A value an editor types into a page's layout area is still escaped for the attribute, as above. How site data returns structures is on Site data and feature flags.

The alternative: a stylesheet per brand

The same repository also shows the older shape: one SCSS entry per brand, each setting its variables and importing the shared partials. The build writes five brand stylesheets of 208 to 226 KB each (29 to 32 KB gzipped) that share nearly all their rules. Every shared fix has to be built into all five, every brand is another entry to keep in step, and a visitor never benefits from a cached copy of a sibling brand's file. Use it only when brands genuinely differ in structure, not in colour and type.

What custom properties cannot do

  • Feed Sass functions. darken(var(--primary), 10%) fails at build time: Sass cannot compute with a value the browser decides. Define the variant as a token of its own (--primary-dark), or use CSS's own color-mix().
  • Appear in media query conditions. @media (min-width: var(--bp)) is invalid CSS. Breakpoints stay SCSS variables.
  • Replace a design decision. A token per colour used once is noise; keep tokens to what a brand or page can actually change.
Go deeper

Was this page helpful?

Your answer goes to the docs team.