Skip to Content
SDKTheming

Theming

The SDK ships a light and a dark theme and follows the visitor’s OS preference by default. Everything visible — buttons, comment pins, panels, backgrounds, badges, the placement cursor — is driven by CSS custom properties, so you can restyle the whole surface without forking any CSS.

There are two ways to do it. They compose, and you can use either or both.


Option 1 — CSS custom properties

Set any --markup-* property anywhere in your page. Values inherit into the SDK’s UI and into the comment pins the SDK anchors into your DOM.

your-stylesheet.css
:root { --markup-primary-color: #7c3aed; --markup-radius: 10px; }

That single brand property themes the toolbar’s primary button, comment pins, and the placement cursor together, because they all resolve through it.

Set these on :root or html. Both trees the SDK renders into see them from there: the Shadow UI also picks them up from any ancestor of its container, but comment pins and the placement cursor live in your own DOM and resolve at :root, so a property set lower down — on body, or on a wrapper — reaches the toolbar and leaves the pins on their defaults. (If you also pass the theme option, see Precedence: a token set there is overridden from #markup-sdk-root rather than from html.)

These are the same six white-label properties the MarkUp web app honours (--markup-primary-color, --markup-primary-hover, --markup-primary-active, --markup-selected-pin-color, --markup-text-on-fill-color, --markup-text-on-selected-pin-color). If you already white-label MarkUp, your SDK embed picks up the same configuration.


Option 2 — the theme option

Pass a theme object to render(). This is the recommended path: values are validated, and text colours are contrast-corrected automatically.

Custom theme
import {MarkUpSDK} from "@ceros-dev/markup-sdk/ui"; const markup = MarkUpSDK.init({publicKey: "<YOUR_PUBLIC_KEY>", markupId: "<YOUR_MARKUP_ID>"}); markup.render({ theme: { mode: "auto", tokens: { primary: "#7c3aed", radius: "10px", pinBackground: "#7c3aed" }, dark: { primary: "#a78bfa" } } });

The string shorthand still works and is equivalent to passing only mode:

markup.render({theme: "dark"});
OptionType
mode optional"light" | "dark" | "auto" | "inverted"

auto follows the visitor's OS preference; inverted renders the opposite of it. Default: "auto".

tokens optionalMarkUpThemeTokens

Token overrides applied in every mode.

light optionalMarkUpThemeTokens

Overrides applied only in light mode. Take precedence over tokens.

dark optionalMarkUpThemeTokens

Overrides applied only in dark mode. Take precedence over tokens.

avatarPalette optionalstring[]

Background colours cycled for generated avatars, chosen by hashing the user's id. Replaces the default palette entirely. An empty array is ignored.

avatarColor optionalAvatarColorResolver

Colour one user's avatar yourself, so the embed matches your app. Called synchronously with {id, name, email}, any of which may be an empty string; return undefined or null to fall back to avatarPalette.

avatarInitials optional"one" | "two"

How many letters an avatar shows when it falls back to initials: "two" for the first and last name initial ("JD"), or "one" for just the first ("J"). Default: "two".

Matching your app’s user colours

Avatar colours are chosen per user, so they cannot be custom properties. By default the SDK hashes the user’s id into avatarPalette. If your app already assigns each user a colour — from your own hash, or from a setting they picked — supply it with avatarColor and the SDK will use yours instead:

import type {AvatarColorResolver} from "@ceros-dev/markup-sdk/ui"; const avatarColor: AvatarColorResolver = ({email}) => (email ? (mySettings[email]?.color ?? myHash(email)) : undefined); markup.render({theme: {avatarColor}});

The function is handed the whole user — {id, name, email} — so you can match on whichever of those your own colours are held by. Reach for email when a MarkUp user id means nothing on your side, which is the usual case: it is often the only identifier your app and MarkUp share.

This applies everywhere the SDK draws a user: comment pins, the thread panel, the popover, the toolbar presence stack and the mention menu — so a user is never shown in two different colours at once.

Points worth knowing:

  • id, name and email are always strings, but any of them may be empty. id is "" for a user whose record has not loaded, and email for a guest or wherever MarkUp holds no address — so check before you key on one. The SDK’s own fallback hash uses the first of the three that is present, in that order. All three empty means there is no author to colour, and your function is not called at all — an authorless pin shows the frame colour instead.

  • The address is passed through as MarkUp holds it, only trimmed. Case is not folded, since the SDK cannot know whether your own keys are. Normalise on your side if you index by a lower-cased address.

  • Return nothing for users you have no colour for. undefined, null and "" all fall back to the palette hash, so you can adopt this partially.

  • It must be synchronous, and it should be pure. It is called while rendering. Resolve async settings before render()/setTheme() and have the function read from what you already hold. Do not call setTheme() from inside it.

  • Results are cached per user until the next setTheme(), so your function is not guaranteed to run again for a user it has already coloured. Re-passing the theme is how you publish a colour that has since changed — existing pins are recoloured in place. Keep the theme object around and pass the same one, because setTheme replaces rather than merges: calling setTheme({avatarColor}) on its own would silently drop every token override you set at render().

    const theme = {mode: "auto", tokens: {pinBackground: "#7c3aed"}, avatarColor}; markup.render({theme}); // ...later, once a user picks a new colour in your app: markup.setTheme(theme);
  • Initials are made readable for you. The SDK derives a contrasting text colour — white or dark ink, whichever reads — for whichever fill an avatar ends up with, yours included.

  • An invalid colour is ignored with a console warning, and that user falls back to the palette. So is a return value that is not a string. A function that throws is reported once and treated as no opinion.

  • Screenshots and notification emails are not covered. Those pins are redrawn server-side and do not include an avatar, so they show the workspace pin colour regardless of what this returns.

One initial instead of two

An avatar without a picture falls back to the first and last initial of the user’s name — "Jane Doe" becomes "JD". Set avatarInitials to "one" for just the first letter, which reads better at pin size and matches apps that show a single letter elsewhere:

markup.render({theme: {avatarInitials: "one"}});

Points worth knowing:

  • It applies to initials you supply as well as the ones the SDK derives. Pass initials="AB" to the Avatar component in a one-letter embed and it renders "A", so the setting holds everywhere rather than only where the SDK picked the letters.
  • A user with no name still shows a placeholder — "?" in the panel, and nothing on a pin, where a lone "?" over the page would read as broken rather than as an unknown author.
  • Avatars with a picture are unaffected, since they show no letters either way.
  • An unrecognised value is ignored with a console warning and the default is kept. 1 and "single" are the easy mistakes, and treating either as “not two” would empty every avatar in the embed.

Changing the theme at runtime

setTheme() accepts the same shorthand and object forms. It replaces the active theme rather than merging into it, so pass the whole theme each time — that way removing a token is possible.

markup.setTheme({mode: "dark", tokens: {primary: "#a78bfa"}}); markup.setTheme("light");

Automatic contrast

Set a fill without its matching foreground and the SDK derives a readable one for you, using the WCAG relative-luminance formula to choose between white and its dark ink.

// Pale brand colour: white label text would be unreadable, so dark ink is used instead. markup.render({theme: {tokens: {primary: "#f5f5c0"}}});

Set primaryForeground explicitly to opt out. If neither white nor dark ink can reach WCAG AA (4.5:1) against your fill, the SDK logs a warning — at that point only a different fill can fix it.

Every fill token with a text-on-top counterpart is paired this way: primary, destructive, success, warning, pinBackground, pinSelectedBackground, and pinResolvedBackground.


Which option should I use?

Prefer the theme option. CSS has no way to reject a bad value, and a bad value does not fall back to the default — a property that is defined but invalid drops the declaration to its initial value, which for a background is transparent. One typo in a stylesheet can make a button invisible. The theme option validates every value up front and ignores (with a warning) anything the browser would reject.

Validation is per token: primary must be a colour, radius a length, shadowMd a shadow, fontFamily a font stack, and so on. A rejected value leaves that token at its default and warns on the console — the rest of the theme still applies. An unrecognised mode warns and falls back to auto.

If you pass theme values through from configuration you do not control — a tenant’s branding record, say — this validation is also what stops them injecting arbitrary CSS into your page.

Use CSS when you want zero build-time coupling, or when your brand colours already live in your own stylesheet.

Precedence

Highest to lowest:

  1. Your CSS on the SDK root — #markup-sdk-root { --markup-primary: … }
  2. The theme option
  3. Your ordinary CSS on :root or html (or, for the Shadow UI only, any ancestor of its container)
  4. The SDK defaults

The SDK renders into two trees, and that shapes rung 1. The parts that live in your DOM — comment pins, the placement cursor, the element highlighter — resolve their tokens from :root, so ordinary !important on html/:root reaches them. The Shadow UI (toolbar, panels, popovers) resolves from the #markup-sdk-root element, and the theme option declares its values there. A declared value beats an inherited one regardless of importance, so for a token you set via theme, html { … !important } will change your pins but not the toolbar.

Targeting #markup-sdk-root overrides both consistently, and it does not need !important — ordinary CSS on that element already outranks the theme option:

Overriding the theme option
#markup-sdk-root { --markup-primary: #7c3aed; }

Use !important there only if something else in your page is also targeting that element.


Dark mode

mode: "auto" follows prefers-color-scheme. Overrides you supply apply on top of whichever mode is active, so a custom primary stays yours in both. Use the light and dark keys when you want different values per mode.

markup.render({ theme: { mode: "auto", tokens: {radius: "10px"}, // both modes light: {primary: "#4c1d95"}, // light only dark: {primary: "#c4b5fd"} // dark only } });

Token reference

theme.tokens keys are the camelCase form of each CSS property name — --markup-pin-background becomes pinBackground. Tokens with no dark value use the light value in both modes.

Where a property lists aliases, any of them works; the first is canonical.

Brand

theme.tokens keyCSS custom propertyLightDark
primary--markup-primary
aliases: --markup-accent, --markup-primary-color
#3b82f6#60a5fa
primaryHover--markup-primary-hover
aliases: --markup-accent-hover
#2563eb#3b82f6
primaryActive--markup-primary-active
aliases: --markup-accent-active
#1d4ed8#2563eb
primaryMuted--markup-primary-muted
aliases: --markup-accent-soft
rgba(59, 130, 246, 0.15)rgba(96, 165, 250, 0.2)
primaryForeground--markup-primary-foreground
aliases: --markup-text-on-fill-color
#ffffff—

Surfaces & text

theme.tokens keyCSS custom propertyLightDark
foreground--markup-foreground
aliases: --markup-text
#222425#fcfcfd
mutedForeground--markup-muted-foreground
aliases: --markup-text-secondary
#4e5255#adb1b3
subtleForeground--markup-subtle-foreground
aliases: --markup-text-tertiary
#64696d#8b9093
disabledForeground--markup-disabled-foreground
aliases: --markup-text-disabled
#8a8f94#6f7477
invertedForeground--markup-inverted-foreground
aliases: --markup-text-inverted
#e7e8e9#222425
background--markup-background
aliases: --markup-bg
#fcfcfd#222425
surface--markup-surface
aliases: --markup-bg-secondary
#f6f6f6#3d4042
surfaceHover--markup-surface-hover
aliases: --markup-bg-hover
#f6f6f6#3d4042
surfaceActive--markup-surface-active
aliases: --markup-bg-active
#ededee#4e5255
inverted--markup-inverted
aliases: --markup-surface-inverted
#141515#fcfcfd
invertedHover--markup-inverted-hoverrgba(255, 255, 255, 0.08)rgba(0, 0, 0, 0.08)
inputHover--markup-input-hover#e7e8e9#3d4042
inputFocus--markup-input-focus#e7e8e9#4e5255
btnInverted--markup-btn-inverted#222425#fcfcfd
btnInvertedHover--markup-btn-inverted-hover#3d4042#e7e8e9
btnInvertedActive--markup-btn-inverted-active#64696d#d5d7d8
menuHover--markup-menu-hover#222425#f4f5f5
menuHoverText--markup-menu-hover-text#fcfcfd#222425
menuHoverInverted--markup-menu-hover-inverted#f4f5f5#222425
menuHoverTextInverted--markup-menu-hover-text-inverted#222425#fcfcfd
overlay--markup-overlayrgba(0, 0, 0, 0.5)—
mediaBackdrop--markup-media-backdroprgba(0, 0, 0, 0.6)—
scrim--markup-scrimrgba(0, 0, 0, 0.05)rgba(0, 0, 0, 0.2)
highlight--markup-highlight#0066ff#4d94ff

Status

theme.tokens keyCSS custom propertyLightDark
success--markup-success#66cc00—
successMuted--markup-success-muted
aliases: --markup-success-bg
rgba(102, 204, 0, 0.1)rgba(102, 204, 0, 0.15)
successForeground--markup-success-foreground#ffffff—
warning--markup-warning#f59e0b—
warningForeground--markup-warning-foreground#ffffff—
destructive--markup-destructive
aliases: --markup-error
#ef4444—
destructiveHover--markup-destructive-hover
aliases: --markup-error-hover
#dc2626—
destructiveForeground--markup-destructive-foreground#ffffff—
destructiveMuted--markup-destructive-muted
aliases: --markup-error-bg
rgba(239, 68, 68, 0.1)rgba(239, 68, 68, 0.2)
unread--markup-unread#0b86df—

Borders

theme.tokens keyCSS custom propertyLightDark
border--markup-border#d5d7d8#3d4042
borderSubtle--markup-border-subtle
aliases: --markup-border-light
#f0f0f0#333638
borderInverted--markup-border-inverted#3d4042#d5d7d8
textInvertedMuted--markup-text-inverted-muted#adb1b3#4e5255

Thread priority

theme.tokens keyCSS custom propertyLightDark
priorityLowBackground--markup-priority-low-background
aliases: --markup-priority-low-bg
rgba(59, 130, 246, 0.12)rgba(96, 165, 250, 0.2)
priorityLowForeground--markup-priority-low-foreground
aliases: --markup-priority-low-text
#3b82f6#93c5fd
priorityMediumBackground--markup-priority-medium-background
aliases: --markup-priority-medium-bg
rgba(245, 158, 11, 0.12)rgba(245, 158, 11, 0.22)
priorityMediumForeground--markup-priority-medium-foreground
aliases: --markup-priority-medium-text
#f59e0b#fcd34d
priorityHighBackground--markup-priority-high-background
aliases: --markup-priority-high-bg
rgba(239, 68, 68, 0.12)rgba(239, 68, 68, 0.24)
priorityHighForeground--markup-priority-high-foreground
aliases: --markup-priority-high-text
#ef4444#fca5a5
priorityCriticalBackground--markup-priority-critical-backgroundrgba(153, 27, 27, 0.14)rgba(220, 38, 38, 0.3)
priorityCriticalForeground--markup-priority-critical-foreground#991b1b#f87171

Pins & cursor

theme.tokens keyCSS custom propertyLightDark
pinBackground--markup-pin-background
aliases: --markup-primary-color
#1b00fb—
avatar variant: #141515
pinForeground--markup-pin-foreground
aliases: --markup-text-on-fill-color
#ffffff—
pinSelectedBackground--markup-pin-selected-background
aliases: --markup-selected-pin-color
#040b97—
pinSelectedForeground--markup-pin-selected-foreground
aliases: --markup-text-on-selected-pin-color
#ffffff—
pinResolvedBackground--markup-pin-resolved-background#636567—
pinResolvedForeground--markup-pin-resolved-foreground#ffffff—
pinRing--markup-pin-ring
aliases: --markup-selected-pin-color
#040b97—
pinBorder--markup-pin-border#ffffff—
pinStatusBadge--markup-pin-status-badge#979a9b—
pinShadow--markup-pin-shadow0 2px 16px 0 rgba(0, 0, 0, 0.05)—
pinSize--markup-pin-size32px—
pinRadius--markup-pin-radius16px—
cursorFill--markup-cursor-fill
aliases: --markup-primary-color
#1755e6—
cursorInk--markup-cursor-ink#ffffff—

Avatar pins

These apply only when you opt into the avatar pin variant:

markup.render({pinVariant: "avatar"});

In that variant a pin is a teardrop framing the thread author’s avatar, anchored by its point rather than its centre. The avatar’s own fill is picked per user in JavaScript (theme.avatarPalette / theme.avatarColor), so it is not a token, and its initials are coloured automatically for contrast against whichever fill they land on.

There are no avatar-specific token keys. The variant is themed through the same keys as the numbered pin — pinBackground recolours whichever pin you render. Only the defaults differ, since the two are different shapes:

theme.tokens keyNumbered defaultAvatar default
pinBackground#1b00fb#fcfcfd
pinSelectedBackground#040b97#0b86df
pinForeground#ffffff#222425
pinSize32px30px
pinRadius16px0 50% 50% 50%
pinShadow0 2px 16px 0 rgba(0, 0, 0, 0.05)0 4px 4px 0 rgba(0, 0, 0, 0.25), 0 15px 20px 0 rgba(0, 0, 0, 0.2)
cursorFill#1755e6#fcfcfd
cursorInk#ffffff#222425

In the avatar variant pinSelectedBackground is the accent: the ring around a selected pin and the fill of the draft pin that marks the spot while a comment is written — not only a background. The numbered variant’s draft pin takes pinBackground, the same fill as its numbered discs. cursorInk is what the placement cursor is outlined and cross-hatched in.

One caveat: the legacy white-label aliases (--markup-primary-color and friends) set the numbered variant’s pin and cursor colours only. They exist for compatibility with the MarkUp web app’s brand-filled pin, and applying a brand fill to the avatar variant would flatten the teardrop-and-ring construction it depends on. Use pinBackground to theme the avatar variant.

pinRadius is the per-corner shorthand that makes the teardrop; the 0 corner is the one that sits on the anchor point. Set it to 50% for a plain circular avatar pin.

pinForeground colours the numbered pin’s number and, in both variants, the priority-badge glyph. It does not colour avatar initials: those are derived for contrast against each user’s own fill — white or dark ink, whichever reads — because a single fixed colour cannot stay legible across a palette of fills. The same is true of avatars in the panel, popover, toolbar and mention menu, so there is no token for them.

pinRing applies to the numbered variant only. The avatar variant’s selection ring takes pinSelectedBackground, and its frame pinBackground — a circular ring cannot be made concentric with a teardrop, so the variant draws its own.

pinBackground is the one avatar default that changes with the mode: the teardrop carries the page’s own surface colour — #fcfcfd in light, #222425 in dark — so the frame keeps holding the avatar apart from whatever is behind it. The variant’s other defaults do not change between modes; the design specifies a single pin that reads on either. Set any of them per mode through theme.light / theme.dark.

pinBackground colours both the teardrop and the ring the avatar carries inside it, which is what makes them read as one band. The variant’s states are:

statetreatment
defaultteardrop in pinBackground
selecteda 2px pinSelectedBackground ring outside the teardrop
resolvedframe takes pinResolvedBackground, the avatar desaturates — opaque, so it never lets page content show through, and still shows who raised the thread
draftsolid pinSelectedBackground, no avatar — it marks where a comment being written will land, so there is no author yet and nothing to click
hoverno change to the pin — hovering shows the thread’s preview card and a pointer cursor instead

pinResolvedBackground colours the resolved state of both variants.

Keyboard focus draws the same ring as selection. On Safari below 15.4, which does not support :focus-visible, the focus ring is absent but every other state is unaffected.

The variant also brings its own placement cursor — a light bubble outlined in ink rather than the numbered variant’s brand-filled disc. Both read cursorFill and cursorInk; only the defaults differ.

Pin priority badges

theme.tokens keyCSS custom propertyLightDark
pinPriorityLow--markup-pin-priority-low#66ad1f—
pinPriorityMedium--markup-pin-priority-medium#e4a607—
pinPriorityHigh--markup-pin-priority-high#d11f1f—
pinPriorityCritical--markup-pin-priority-critical#991b1b—

Typography

theme.tokens keyCSS custom propertyLightDark
fontFamily--markup-font-family"Source Sans 3", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif—
fontSizeXs--markup-font-size-xs10px—
fontSizeSm--markup-font-size-sm12px—
fontSizeMd--markup-font-size-md13px—
fontSizeLg--markup-font-size-lg14px—
fontSizeHeadingSm--markup-font-size-heading-sm16px—
fontWeightNormal--markup-font-weight-normal420—
fontWeightMedium--markup-font-weight-medium500—
fontWeightSemibold--markup-font-weight-semibold600—
lineHeight--markup-line-height1.5—
lineHeightLabel--markup-line-height-label20px—
lineHeightLabelSm--markup-line-height-label-sm16px—
lineHeightHeadingSm--markup-line-height-heading-sm24px—

Spacing

theme.tokens keyCSS custom propertyLightDark
spaceXs--markup-space-xs4px—
spaceSm--markup-space-sm8px—
spaceMd--markup-space-md12px—
spaceLg--markup-space-lg16px—
spaceXl--markup-space-xl24px—

Radius

theme.tokens keyCSS custom propertyLightDark
radiusSm--markup-radius-sm4px—
radius--markup-radius6px—
radiusMd--markup-radius-md8px—
radiusLg--markup-radius-lg10px—
radiusFull--markup-radius-full9999px—
radiusCircle--markup-radius-circle50%—

Elevation

theme.tokens keyCSS custom propertyLightDark
shadowSm--markup-shadow-sm0 1px 2px rgba(0, 0, 0, 0.05)—
shadowInput--markup-shadow-input0 1px 3px 0 rgba(0, 0, 0, 0.12)—
shadowMd--markup-shadow-md0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)—
shadowLg--markup-shadow-lg0 16px 24px rgba(0, 0, 0, 0.08), 0 2px 8px rgba(0, 0, 0, 0.16)0 16px 24px rgba(0, 0, 0, 0.24), 0 2px 8px rgba(0, 0, 0, 0.32)
shadowToolbar--markup-shadow-toolbar0 4px 12px rgba(0, 0, 0, 0.15)0 4px 12px rgba(0, 0, 0, 0.4)
shadowToolbarHover--markup-shadow-toolbar-hover0 6px 16px rgba(0, 0, 0, 0.2)0 6px 16px rgba(0, 0, 0, 0.5)
shadowMenu--markup-shadow-menu0 0 3px rgba(51, 51, 51, 0.16), 0 0 6px rgba(51, 51, 51, 0.14), 0 0 8px rgba(51, 51, 51, 0.08)0 0 3px rgba(0, 0, 0, 0.24), 0 0 6px rgba(0, 0, 0, 0.2), 0 0 8px rgba(0, 0, 0, 0.16)
shadowModal--markup-shadow-modal0 0 3px 0 rgba(51, 51, 51, 0.16), 0 0 6px 0 rgba(51, 51, 51, 0.14), 0 0 8px 0 rgba(51, 51, 51, 0.08), 0 0 8px 0 rgba(51, 51, 51, 0.02)0 0 3px 0 rgba(0, 0, 0, 0.4), 0 0 6px 0 rgba(0, 0, 0, 0.32), 0 0 8px 0 rgba(0, 0, 0, 0.2)
shadowDropdown--markup-shadow-dropdown0 12px 24px -8px rgba(17, 24, 39, 0.12), 0 4px 8px -2px rgba(17, 24, 39, 0.08), 0 0 0 1px rgba(17, 24, 39, 0.04)0 12px 24px -8px rgba(0, 0, 0, 0.4), 0 4px 8px -2px rgba(0, 0, 0, 0.28), 0 0 0 1px rgba(255, 255, 255, 0.06)

Focus

theme.tokens keyCSS custom propertyLightDark
ring--markup-ringvar(--_mk-accent-soft)—
focusRing--markup-focus-ring0 0 0 2px var(--_mk-ring)—

Motion

theme.tokens keyCSS custom propertyLightDark
transitionFast--markup-transition-fast0.1s ease—
transitionBase--markup-transition-base0.15s ease—
transitionSnap--markup-transition-snap0.25s cubic-bezier(0.4, 0, 0.2, 1)—

Layering

theme.tokens keyCSS custom propertyLightDark
zDropdown--markup-z-dropdown10000—
zPanel--markup-z-panel9999—
zModal--markup-z-modal10002—

Sizing

theme.tokens keyCSS custom propertyLightDark
avatarXs--markup-avatar-xs20px—
avatarSm--markup-avatar-sm24px—
avatarMd--markup-avatar-md32px—
avatarLg--markup-avatar-lg40px—
btnSm--markup-btn-sm28px—
btnLg--markup-btn-lg40px—
ncWidth--markup-nc-width280px—

Limits

  • The placement cursor is an SVG data: URI, and a data URI cannot read CSS custom properties. The SDK resolves --markup-cursor-fill in JavaScript and regenerates the cursor whenever the theme changes — on setTheme(), and on an OS light/dark flip under mode: "auto" — so both channels work. A mid-session change made by editing a stylesheet from devtools will not repaint it until setTheme() runs.
  • Size tokens take absolute lengths only. Percentages are rejected by the theme object (32px, 2rem and calc() over absolute lengths are all fine). The pins and the comment composer are positioned by the SDK in JavaScript against a zero-sized wrapper, so a percentage has nothing meaningful to resolve against: --markup-pin-size: 50% rendered a 4px sliver of a pin. Setting one through raw CSS is not validated and will do exactly that.
  • Avatar colours and initials are decided per user in JavaScript rather than CSS, so there is no custom property for either. Use theme.avatarPalette to change the set of colours, theme.avatarColor to decide each one — see Matching your app’s user colours — and theme.avatarInitials to show one letter instead of two.
  • Modern colour syntax (oklch(), color(), color-mix()) is not usable: the SDK supports Chrome 90+, Firefox 88+, Safari 14+, and those predate it. Use hex, rgb(), or hsl().