Skip to content

Compass Web SDK: Theming Reference

Last updatedView as Markdown

Every SDK-rendered Experience shares one theming contract. Set your brand once and all three pick it up:

Each Experience renders inside a shadow root: your page’s CSS cannot reach its internals, and its internals are not part of the API. You theme it through two public surfaces only:

  1. CSS custom properties (--arc-compass-*), which cascade through the shadow boundary. Set them on the tag, a wrapping selector, or :root.
  2. ::part() hooks on named elements, for restyling an element outright. Each component page lists its own part names.

Everything else — the markup between the parts, class names, and internal custom properties (anything prefixed --_) — is internal and can change in any release.

Tokens

One table for all three Experiences. The Applies to column says where a token has a visible effect; setting a token on an Experience that does not use it is harmless, so a single brand block can target all three.

PropertyDefaultWhat it controlsApplies to
--arc-compass-surfaceCanvas (system)Background where Compass owns the surface — the full-screen Catch Up takeover. Must be opaque.Catch Up Card
--arc-compass-on-surfacecurrentColorInk on the surface: text, plus the neutral ramp (muted text, hairlines, subtle fills) derived from it.Catch Up Card
--arc-compass-accentcurrentColorPrimary accent. Colors the kicker above each recommendation headline, and the Catch Up Card’s links, active states, and filled controls (“Catch me up,” “I’m caught up”).All three
--arc-compass-on-accent#fffForeground on an accent fill (button text and icons). Set it when your accent is light.Catch Up Card
--arc-compass-accent-secondarythe primary accentPremium badge on recommendation cards, including the Catch Up Card’s “You might also be interested in” list. Deprecated — see The secondary accent.All three
--arc-compass-font-familyinherited page fontPrimary typeface — headlines and running text.All three
--arc-compass-font-family-secondarythe primary typefaceSecondary typeface — kickers, badges, labels, and meta.All three
--arc-compass-radius.5remCorner rounding — cards, images, and buttons. See Corner radius.All three
--arc-compass-card-min-width220pxMinimum card width in the recommendations grid — the one density knob. See Sizing the cards with one value.Recommendations

The recommendations Experiences are transparent and draw text in your page’s own color, so they do not read the surface/on-surface pair. The Catch Up Card is the only Experience that paints a surface of its own.

Zero-config appearance

With no tokens set, Experiences render “native”: text follows your page’s color, both accents fall back to that same color, and running text inherits your page’s font, so an unthemed component is monochrome but never clashes. The one exception is the Catch Up Card’s typography: until you set the font tokens, its headlines use Georgia and its intro line uses Inter, its built-in look.

The SDK never ships or @font-faces a typeface. Load your fonts as you already do and point the tokens at them. Set only --arc-compass-font-family and the secondary roles use that one face too.

Copy-paste brand block

Set the tokens once on :root and every Experience on the page follows. Fill in your own values:

:root {
/* Typefaces — load them yourself; the SDK never ships a font. */
--arc-compass-font-family: "Publico", Georgia, serif;
--arc-compass-font-family-secondary: "Franklin", Arial, sans-serif;
/* Accent: recommendation kickers and the Catch Up Card's controls. */
--arc-compass-accent: #b00020;
--arc-compass-on-accent: #ffffff; /* text on an accent fill */
/* Surface pair: the Catch Up Card's full-screen takeover. Opaque. */
--arc-compass-surface: #ffffff;
--arc-compass-on-surface: #1a1a1a;
/* Shape and density. */
--arc-compass-radius: 8px;
--arc-compass-card-min-width: 240px;
}

Leave --arc-compass-accent-secondary out of a new theme: the premium badge takes the primary accent.

Light and dark recipe

Compass’s colors come from your tokens, not the reader’s OS. Two pairs define the whole palette: surface/on-surface set the background and ink, and every neutral (muted text, hairlines, subtle fills) is derived from them; accent/on-accent do the same for filled controls. To support a dark edition, override those four for whatever selector marks it, and set color-scheme so native browser chrome (the takeover’s scrollbar) matches:

/* Light — the default edition. */
:root {
--arc-compass-surface: #ffffff;
--arc-compass-on-surface: #1a1a1a;
--arc-compass-accent: #b00020;
--arc-compass-on-accent: #ffffff;
}
/* Dark — scope to however your site marks its dark edition. */
.night-edition {
--arc-compass-surface: #14161c; /* opaque page background */
--arc-compass-on-surface: #eef1f6; /* light ink */
--arc-compass-accent: #f2b705; /* amber */
--arc-compass-on-accent: #14161c; /* dark text on amber, not white */
}
.night-edition :is(
arc-compass-recommendations-carousel,
arc-compass-recommendations-list,
arc-compass-catchup-card
) {
color-scheme: dark;
}

To follow the reader’s OS setting instead of a class, put the dark block inside @media (prefers-color-scheme: dark) { :root { … } }.

You do not need color-scheme, prefers-color-scheme, or system colors to make Compass’s own UI dark — the badge, pills, hairlines, muted text, and takeover all derive from the two pairs. In the recommendations Experiences, the dark edition comes from your page: the cards are transparent and draw text in your page’s color, so only the accent needs a dark-friendly value.

Corner radius

--arc-compass-radius is the base every rounded surface derives from, so one brand radius rounds the recommendation cards and the Catch Up Card alike. Set it to a single CSS length with a unit. Anything else fails silently: a unitless number (8) is not a length, so every corner goes square, and a multi-value radius (8px 16px) rounds only the buttons. For square corners throughout, write 0px.

Buttons take the token as-is, so a pill-sized value (999px) gives you pill buttons. Containers — frames, images, rows — follow the token up to 1rem and hold there, so they stay rectangles however large the value. Pills, dots, and the round carousel arrows are shapes rather than rounding and do not follow it. Rely on the token moving every Experience’s rounding together, not on a given surface getting a given fraction of it — the proportions can be retuned in any release.

The secondary accent

--arc-compass-accent-secondary colors only the premium badge, on the recommendation cards and in the Catch Up Card’s “You might also be interested in” list. It is deprecated: when unset, the badge takes --arc-compass-accent, so you can drop it from your theme. A site that already sets both accents renders unchanged, and the token keeps working until a future major version removes it.

What the shadow boundary does and does not protect

Selectors do not cross a shadow boundary. A global reset (* { margin: 0 }), an aggressive framework rule, an !important on a class: none of them can match anything inside an Experience. You do not need to scope your CSS away from the Experiences, and they do not need !important to defend against your page.

Inheritance does cross it. Inherited properties set on an ancestor flow into the shadow tree, so that is the only real vector. The Experiences reset the inherited properties that can distort a layout rather than restyle it: line-height, letter-spacing, word-spacing, text-transform, text-indent, white-space, text-align, direction, and visibility.

font-family and color are deliberately not reset. Inheriting them is the zero-config story — an Experience should look like the page it sits in — and neither can break geometry. The practical consequence: if your page sets text-transform: uppercase on body, your own text is uppercase and the Experience’s is not. Use the tokens to theme it.

Styling internals with ::part()

Tokens change values Compass anticipated. When you need to restyle an element outright — a different thumbnail aspect ratio, your own type scale — use the part hooks each component page lists:

Part names are versioned public surface: they will not be removed or moved to a different element within a major version.

Two rules apply to every Experience:

  • ::part() selects the named element and nothing below it. ::part(card) img matches nothing — style ::part(thumbnail) instead. Every element you can restyle has its own name; if something you need is not in a part table, it is not stylable.
  • A ::part() declaration beats the Experience’s own rules, so stick to visual properties. Layout properties such as display and position on a part can break the component — each component page notes the parts where this matters.

Setting tokens and parts from Tailwind

Tokens and parts are plain CSS, so they work whatever your styling stack is — you are setting a custom property and matching a ::part() selector, not reaching for a component API. Two ways to do it from Tailwind:

A stylesheet block (clearest for ::part() rules), colocated with the rest of your CSS:

.rec-rail::part(card) {
@apply rounded-lg border border-gray-200 bg-white;
}
.rec-rail::part(headline) {
@apply text-base font-bold;
}

Arbitrary variants, straight on the tag, when you would rather stay in markup. The tokens are a class; the parts need the [&::part(...)] escape hatch because Tailwind has no built-in part variant:

<arc-compass-recommendations-list
class="[--arc-compass-accent:theme(colors.red.600)]
[&::part(card)]:rounded-lg [&::part(card)]:border
[&::part(headline)]:font-bold"
>
</arc-compass-recommendations-list>

Either reaches the shadow tree — a ::part() rule is not blocked by the boundary the way a descendant selector is. Utility resets do not leak in either (see What the shadow boundary does and does not protect), so you never need to scope Tailwind’s preflight away from the element.

See also