← Back to Blog

Brand a Calcite app the right way — theming ArcGIS apps with design tokens

A Calcite app window being re-skinned by a brand color token

There is a fast way to brand a Calcite app and a durable way, and they are not the same. The fast way — hunting through dev tools, finding the element that’s the wrong blue, and writing a CSS rule that targets its internals — works on Friday and breaks on the next Calcite release. The durable way is to brand through Calcite’s design tokens, and it is both less code and far less maintenance.

If you build on the ArcGIS Maps SDK for JavaScript, Calcite is already in your app: it’s the design system behind the panels, action bars, buttons, and inputs that frame the map. Getting its theme to match your brand — instead of shipping with the default Esri blue — is the difference between an app that looks assembled and one that looks designed. Here is the model the Calcite team actually intends you to use, and the three lines of CSS that do most of the work.

Why the “just override the CSS” reflex fails

Calcite ships as web components, and each component renders inside a shadow DOM. When you write a selector that reaches into a component’s internal structure — or worse, copy a generated class name out of the inspector — you are coupling your brand to an implementation detail the Calcite team is free to change in any release. Those overrides are invisible to Calcite’s own change process, so nothing warns you when a refactor moves the element you targeted. The brand silently reverts, usually in production, usually noticed by a client before you.

Design tokens exist precisely to give you a stable, public surface to customize. As Esri’s documentation puts it, making modifications through supported design tokens “helps to prevent potential future breaking changes.” That is the whole argument: tokens are the contract; element internals are not.

The three tiers: core, semantic, component

Calcite’s tokens are organized into three tiers, and knowing which one to touch is most of the skill:

TierWhat it isDo you edit it?
CoreThe raw palette — named hex values like --calcite-color-neutral-blk-120 with no usage guidance. Referenced by semantic tokens.No. Managed internally by the Calcite team; treat as off-limits.
SemanticPurpose-named tokens like --calcite-color-brand or --calcite-color-text-1. They reference core tokens and are used throughout every component.Yes — this is your main lever. Change a semantic token once and it propagates everywhere.
ComponentScoped overrides for one component, like --calcite-button-background-color. They reference semantic tokens by default.Sparingly — for the last 10% where one component needs to differ.

The relationship is a chain: a raw value feeds a core token, a core token feeds a semantic token, and a semantic token feeds a component token. You theme by intercepting that chain at the semantic level — high enough to change everything at once, public enough that Calcite won’t pull the rug out from under you.

Theme once, at :root, with semantic tokens

The single highest-leverage move is to override the brand color sequence at the document root. Calcite pairs the brand color with -hover and -press variants for interactive states, so set all of them together — this is exactly the pattern Esri documents for applying a custom theme across an app:

/* Brand the entire Calcite layer in one place */
:root {
  --calcite-color-brand: #2f6fff;        /* ATS electric blue */
  --calcite-color-brand-hover: #2a63e6;
  --calcite-color-brand-press: #2356cc;
  --calcite-color-brand-underline: #2356cc;
}

That handful of declarations re-skins every prioritized action in the app — primary buttons, switches, active tabs, the meter fill, focus accents — because all of those components consume --calcite-color-brand rather than a hard-coded hex. You did not touch a single component’s internals, and nothing here breaks when Calcite reorganizes a shadow tree.

The same approach extends to the rest of the palette. Calcite’s primary colors are deliberately few: one actionable brand color plus limited sets of surface, text, and border tokens, with a separate set of status colors for info, success, warning, and danger. Surfaces and text use a -1 / -2 / -3 hierarchy where -1 takes visual priority:

/* Adjust surfaces and text to match a brand neutral ramp */
:root {
  --calcite-color-surface-1: #f7f9fc;
  --calcite-color-surface-2: #ffffff;
  --calcite-color-text-1: #141b2d;
  --calcite-color-border-1: #d4dbe6;
}
Restraint is the brand. Calcite’s own guidance is to use the brand color sparingly — for prioritized actions, never as a page background — and to reserve the status colors for genuine status, not decoration. A premium GIS app reads as premium partly because color is doing work, not noise. Resist the urge to paint the brand color across surfaces.

Theme light and dark — both, or neither

Calcite provides light and dark modes. Light is the default and needs no configuration; dark mode is applied through Calcite’s mode CSS classes. The catch that trips people up: a token override placed only in light mode does nothing when the user flips to dark. If your app supports both, you must define your brand values in both contexts.

/* Light is the default scope */
:root {
  --calcite-color-brand: #2f6fff;
}

/* Repeat for dark mode so the brand survives a mode switch */
.calcite-mode-dark {
  --calcite-color-brand: #4f86ff;   /* lift the value for dark surfaces */
}

Note that the value is not identical across modes. Calcite’s own brand token is #007AC2 in light and a brighter #009AF2 in dark, because the same hex does not carry the same contrast against a near-white surface and a near-black one. Pick a slightly brighter brand value for dark mode and verify both against your text and surface tokens.

Component tokens for the last 10%

Sometimes one component genuinely needs to differ — a call-to-action button that should stand apart from every other primary button, say. That is what component tokens are for. They override a single component without disturbing the semantic layer everything else relies on. Set them at :root to affect every instance, or scope them to a class for a single instance:

/* Every button in the app */
calcite-button {
  --calcite-button-background-color: #2356cc;
}

/* Just the buttons that carry this class */
.cta-emphasis {
  --calcite-button-background-color: #10b981;
}

The discipline here is to reach for component tokens last, not first. If you find yourself overriding the same component token on dozens of elements, that is a signal the change belongs one tier up, in a semantic token. The most efficient theme uses semantic tokens for the system-wide decisions and component tokens only for the deliberate exceptions.

A four-step recipe

  1. Define your brand color sequence — brand, brand-hover, brand-press — as semantic overrides at :root.
  2. Tune the neutral ramp (surface, text, border) only as far as your brand actually requires; trust Calcite’s defaults for the rest.
  3. If you ship dark mode, duplicate every brand override under the dark mode class with values lifted for contrast, and check both modes.
  4. Use component tokens only for the handful of intentional exceptions, scoped as tightly as the design calls for.

Done this way, your entire brand lives in one small, legible stylesheet of supported tokens — no shadow-DOM spelunking, no copied class names, nothing that a Calcite upgrade can quietly undo. The tokens package is versioned and moving quickly (@esri/calcite-design-tokens reached 4.1 in May 2026), and theming at the semantic tier is exactly how you stay on the supported path as it evolves.

References

Want your ArcGIS app to look designed, not assembled?

We brand Calcite-based ArcGIS apps end to end — a token-driven theme that survives upgrades, light and dark modes that actually match, and a UI held to the same bar as a consumer SaaS product.

Book a free intro call