Manual Style Theming Branding

Branding

elements man style/theming/branding Read as markdown

Branding an Elements app means repointing the accent tokens. There are two levels: a light-touch accent override, and a full brand ramp. Both go in @layer tokens, imported after @elements/style.

The accent block

Five tokens carry the brand:

Token Role
--accent The brand color
--accent-deep Hover, pressed, gradient end
--accent-soft Tinted accent backgrounds (selected rows, accent callouts)
--accent-ink Text and icons sitting on an --accent fill
--accent-edge Accent borders on tinted surfaces

Every accent-bearing component reads these. Set them once and buttons, focus rings, checkboxes, selected tabs, and accent surfaces all follow.

Link color

A bare <a> is the one thing the accent block does not cover, because a link has to stay readable inside a dark panel or a brand-colored hero as well as on the page. It takes its lightness from the text around it and its color from two tokens of its own:

Token Role
--link-c Link chroma, roughly 0.10 to 0.18 for a saturated brand
--link-h Link hue in degrees, the same hue as your brand ramp
:root {
  --link-c: 0.120;
  --link-h: 235;
}

Leave them unset and links render as the surrounding ink a shade deeper, plus the underline. That is always legible, which is why it is the default, but it is not branded. See base for how the adaptive color works.

Light touch

If you just want a brand color, set the accent block. In a scaffolded app the place for this is app/shared/styles/vars.css (imported by both the page and email baselines):

:root {
  --accent:      #4a7bc4;
  --accent-deep: #2f5a9e;   /* hover / pressed / gradient end */
  --accent-soft: #eef3fb;   /* tinted accent backgrounds */
  --accent-ink:  #ffffff;   /* text on the accent fill */
}

--accent alone is enough to color the system. The soft and ink companions are needed for tinted regions (an accent callout, a selected-row background, text on an accent fill); skip them and components fall back to --accent everywhere, which works but reads flatter.

You can also point fonts here in the same file:

:root {
  --font-sans: "Your Sans", ui-sans-serif, system-ui, sans-serif;
  --font-mono: "Your Mono", ui-monospace, monospace;
}

Full brand ramp

For a polished result across light and dark mode, define a full brand ramp and point the accent block at it. Copy themes/customer-example.css into app/shared/styles/, edit the values, and import it from page.css after @elements/style. The recipe has three steps.

Step 1: your brand ramp. Anchor your brand color at 500 and fill in a 50–950 ramp. Match the lightness curve of the ramps in themes/default.css so your brand behaves like the status ramps (an OKLCH ramp generator seeded with your hex gets you there):

@layer tokens {
  :root {
    --brand-50:   oklch(0.97 0.015 0);
    --brand-100:  oklch(0.93 0.040 0);
    --brand-200:  oklch(0.87 0.080 0);
    --brand-300:  oklch(0.80 0.130 0);
    --brand-400:  oklch(0.72 0.180 0);
    --brand-500:  oklch(0.62 0.220 0);   /* your brand color */
    --brand-600:  oklch(0.54 0.215 0);
    --brand-700:  oklch(0.46 0.180 0);
    --brand-800:  oklch(0.36 0.140 0);
    --brand-900:  oklch(0.26 0.100 0);
    --brand-950:  oklch(0.16 0.065 0);
  }
}

Step 2: repoint the accent block at the ramp. This is the only block that does the branding work:

@layer tokens {
  :root {
    --accent:      var(--brand-500);
    --accent-deep: var(--brand-700);
    --accent-soft: var(--brand-50);
    --accent-ink:  var(--brand-900);
    --accent-edge: var(--brand-200);

    --focus:      var(--accent);
    --focus-ring: 0 0 0 3px color-mix(in oklch, var(--focus) 40%, transparent);
  }
}

Step 2b (optional): the checkbox glyph. The checkmark is an inline SVG data URI, so it can't read --accent-ink. If your --accent-ink isn't near-white, repoint --checkbox-check to a stroke matching it (copy the URL from themes/default.css and change the stroke='%23…' hex).

Step 3 (optional): dark-mode brand override. Use a lighter ramp step on dark backgrounds so the color stays vibrant. Follow the same two-block pattern the default theme uses, the OS-preference block and the data-theme block, kept identical:

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --accent:      var(--brand-400);
    --accent-deep: var(--brand-300);
    --accent-soft: var(--brand-950);
    --accent-ink:  var(--brand-100);
    --accent-edge: var(--brand-800);
  }
}

:root[data-theme="dark"] {
  --accent:      var(--brand-400);
  --accent-deep: var(--brand-300);
  --accent-soft: var(--brand-950);
  --accent-ink:  var(--brand-100);
  --accent-edge: var(--brand-800);
}

The shipped example: themes/elements.css

themes/elements.css is the canonical worked example: the Elements brand blue. It's opt-in, not part of the default entry, and follows exactly the recipe above:

@import "@elements/style";
@import "@elements/style/themes/elements";

It defines an Elements-blue ramp (--elements-blue-*, anchored at #49A9E7 / #164C77), repoints the accent block at it, updates --focus and --focus-ring, sets a white checkbox glyph (its --accent-ink is white in both modes), and adds the dark-mode accent override. Read it as a complete, real branding file.