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.
: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.
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"});| Option | Type | |
|---|---|---|
| mode optional | "light" | "dark" | "auto" | "inverted" |
|
| tokens optional | MarkUpThemeTokens | Token overrides applied in every mode. |
| light optional | MarkUpThemeTokens | Overrides applied only in light mode. Take precedence over |
| dark optional | MarkUpThemeTokens | Overrides applied only in dark mode. Take precedence over |
| avatarPalette optional | string[] | Background colours cycled for generated avatars, chosen by hashing the user's id. Replaces the default palette entirely. An empty array is ignored. |
| avatarColor optional | AvatarColorResolver | Colour one user's avatar yourself, so the embed matches your app. Called synchronously with |
| avatarInitials optional | "one" | "two" | How many letters an avatar shows when it falls back to initials: |
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,nameandemailare always strings, but any of them may be empty.idis""for a user whose record has not loaded, andemailfor 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,nulland""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 callsetTheme()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, becausesetThemereplaces rather than merges: callingsetTheme({avatarColor})on its own would silently drop every token override you set atrender().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 theAvatarcomponent 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.
1and"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:
- Your CSS on the SDK root —
#markup-sdk-root { --markup-primary: … } - The
themeoption - Your ordinary CSS on
:rootorhtml(or, for the Shadow UI only, any ancestor of its container) - 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:
#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 key | CSS custom property | Light | Dark |
|---|---|---|---|
primary | --markup-primaryaliases: --markup-accent, --markup-primary-color | #3b82f6 | #60a5fa |
primaryHover | --markup-primary-hoveraliases: --markup-accent-hover | #2563eb | #3b82f6 |
primaryActive | --markup-primary-activealiases: --markup-accent-active | #1d4ed8 | #2563eb |
primaryMuted | --markup-primary-mutedaliases: --markup-accent-soft | rgba(59, 130, 246, 0.15) | rgba(96, 165, 250, 0.2) |
primaryForeground | --markup-primary-foregroundaliases: --markup-text-on-fill-color | #ffffff | — |
Surfaces & text
theme.tokens key | CSS custom property | Light | Dark |
|---|---|---|---|
foreground | --markup-foregroundaliases: --markup-text | #222425 | #fcfcfd |
mutedForeground | --markup-muted-foregroundaliases: --markup-text-secondary | #4e5255 | #adb1b3 |
subtleForeground | --markup-subtle-foregroundaliases: --markup-text-tertiary | #64696d | #8b9093 |
disabledForeground | --markup-disabled-foregroundaliases: --markup-text-disabled | #8a8f94 | #6f7477 |
invertedForeground | --markup-inverted-foregroundaliases: --markup-text-inverted | #e7e8e9 | #222425 |
background | --markup-backgroundaliases: --markup-bg | #fcfcfd | #222425 |
surface | --markup-surfacealiases: --markup-bg-secondary | #f6f6f6 | #3d4042 |
surfaceHover | --markup-surface-hoveraliases: --markup-bg-hover | #f6f6f6 | #3d4042 |
surfaceActive | --markup-surface-activealiases: --markup-bg-active | #ededee | #4e5255 |
inverted | --markup-invertedaliases: --markup-surface-inverted | #141515 | #fcfcfd |
invertedHover | --markup-inverted-hover | rgba(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-overlay | rgba(0, 0, 0, 0.5) | — |
mediaBackdrop | --markup-media-backdrop | rgba(0, 0, 0, 0.6) | — |
scrim | --markup-scrim | rgba(0, 0, 0, 0.05) | rgba(0, 0, 0, 0.2) |
highlight | --markup-highlight | #0066ff | #4d94ff |
Status
theme.tokens key | CSS custom property | Light | Dark |
|---|---|---|---|
success | --markup-success | #66cc00 | — |
successMuted | --markup-success-mutedaliases: --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-destructivealiases: --markup-error | #ef4444 | — |
destructiveHover | --markup-destructive-hoveraliases: --markup-error-hover | #dc2626 | — |
destructiveForeground | --markup-destructive-foreground | #ffffff | — |
destructiveMuted | --markup-destructive-mutedaliases: --markup-error-bg | rgba(239, 68, 68, 0.1) | rgba(239, 68, 68, 0.2) |
unread | --markup-unread | #0b86df | — |
Borders
theme.tokens key | CSS custom property | Light | Dark |
|---|---|---|---|
border | --markup-border | #d5d7d8 | #3d4042 |
borderSubtle | --markup-border-subtlealiases: --markup-border-light | #f0f0f0 | #333638 |
borderInverted | --markup-border-inverted | #3d4042 | #d5d7d8 |
textInvertedMuted | --markup-text-inverted-muted | #adb1b3 | #4e5255 |
Thread priority
theme.tokens key | CSS custom property | Light | Dark |
|---|---|---|---|
priorityLowBackground | --markup-priority-low-backgroundaliases: --markup-priority-low-bg | rgba(59, 130, 246, 0.12) | rgba(96, 165, 250, 0.2) |
priorityLowForeground | --markup-priority-low-foregroundaliases: --markup-priority-low-text | #3b82f6 | #93c5fd |
priorityMediumBackground | --markup-priority-medium-backgroundaliases: --markup-priority-medium-bg | rgba(245, 158, 11, 0.12) | rgba(245, 158, 11, 0.22) |
priorityMediumForeground | --markup-priority-medium-foregroundaliases: --markup-priority-medium-text | #f59e0b | #fcd34d |
priorityHighBackground | --markup-priority-high-backgroundaliases: --markup-priority-high-bg | rgba(239, 68, 68, 0.12) | rgba(239, 68, 68, 0.24) |
priorityHighForeground | --markup-priority-high-foregroundaliases: --markup-priority-high-text | #ef4444 | #fca5a5 |
priorityCriticalBackground | --markup-priority-critical-background | rgba(153, 27, 27, 0.14) | rgba(220, 38, 38, 0.3) |
priorityCriticalForeground | --markup-priority-critical-foreground | #991b1b | #f87171 |
Pins & cursor
theme.tokens key | CSS custom property | Light | Dark |
|---|---|---|---|
pinBackground | --markup-pin-backgroundaliases: --markup-primary-color | #1b00fb | — avatar variant: #141515 |
pinForeground | --markup-pin-foregroundaliases: --markup-text-on-fill-color | #ffffff | — |
pinSelectedBackground | --markup-pin-selected-backgroundaliases: --markup-selected-pin-color | #040b97 | — |
pinSelectedForeground | --markup-pin-selected-foregroundaliases: --markup-text-on-selected-pin-color | #ffffff | — |
pinResolvedBackground | --markup-pin-resolved-background | #636567 | — |
pinResolvedForeground | --markup-pin-resolved-foreground | #ffffff | — |
pinRing | --markup-pin-ringaliases: --markup-selected-pin-color | #040b97 | — |
pinBorder | --markup-pin-border | #ffffff | — |
pinStatusBadge | --markup-pin-status-badge | #979a9b | — |
pinShadow | --markup-pin-shadow | 0 2px 16px 0 rgba(0, 0, 0, 0.05) | — |
pinSize | --markup-pin-size | 32px | — |
pinRadius | --markup-pin-radius | 16px | — |
cursorFill | --markup-cursor-fillaliases: --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 key | Numbered default | Avatar default |
|---|---|---|
pinBackground | #1b00fb | #fcfcfd |
pinSelectedBackground | #040b97 | #0b86df |
pinForeground | #ffffff | #222425 |
pinSize | 32px | 30px |
pinRadius | 16px | 0 50% 50% 50% |
pinShadow | 0 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:
| state | treatment |
|---|---|
| default | teardrop in pinBackground |
| selected | a 2px pinSelectedBackground ring outside the teardrop |
| resolved | frame takes pinResolvedBackground, the avatar desaturates — opaque, so it never lets page content show through, and still shows who raised the thread |
| draft | solid pinSelectedBackground, no avatar — it marks where a comment being written will land, so there is no author yet and nothing to click |
| hover | no 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 key | CSS custom property | Light | Dark |
|---|---|---|---|
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 key | CSS custom property | Light | Dark |
|---|---|---|---|
fontFamily | --markup-font-family | "Source Sans 3", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif | — |
fontSizeXs | --markup-font-size-xs | 10px | — |
fontSizeSm | --markup-font-size-sm | 12px | — |
fontSizeMd | --markup-font-size-md | 13px | — |
fontSizeLg | --markup-font-size-lg | 14px | — |
fontSizeHeadingSm | --markup-font-size-heading-sm | 16px | — |
fontWeightNormal | --markup-font-weight-normal | 420 | — |
fontWeightMedium | --markup-font-weight-medium | 500 | — |
fontWeightSemibold | --markup-font-weight-semibold | 600 | — |
lineHeight | --markup-line-height | 1.5 | — |
lineHeightLabel | --markup-line-height-label | 20px | — |
lineHeightLabelSm | --markup-line-height-label-sm | 16px | — |
lineHeightHeadingSm | --markup-line-height-heading-sm | 24px | — |
Spacing
theme.tokens key | CSS custom property | Light | Dark |
|---|---|---|---|
spaceXs | --markup-space-xs | 4px | — |
spaceSm | --markup-space-sm | 8px | — |
spaceMd | --markup-space-md | 12px | — |
spaceLg | --markup-space-lg | 16px | — |
spaceXl | --markup-space-xl | 24px | — |
Radius
theme.tokens key | CSS custom property | Light | Dark |
|---|---|---|---|
radiusSm | --markup-radius-sm | 4px | — |
radius | --markup-radius | 6px | — |
radiusMd | --markup-radius-md | 8px | — |
radiusLg | --markup-radius-lg | 10px | — |
radiusFull | --markup-radius-full | 9999px | — |
radiusCircle | --markup-radius-circle | 50% | — |
Elevation
theme.tokens key | CSS custom property | Light | Dark |
|---|---|---|---|
shadowSm | --markup-shadow-sm | 0 1px 2px rgba(0, 0, 0, 0.05) | — |
shadowInput | --markup-shadow-input | 0 1px 3px 0 rgba(0, 0, 0, 0.12) | — |
shadowMd | --markup-shadow-md | 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06) | — |
shadowLg | --markup-shadow-lg | 0 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-toolbar | 0 4px 12px rgba(0, 0, 0, 0.15) | 0 4px 12px rgba(0, 0, 0, 0.4) |
shadowToolbarHover | --markup-shadow-toolbar-hover | 0 6px 16px rgba(0, 0, 0, 0.2) | 0 6px 16px rgba(0, 0, 0, 0.5) |
shadowMenu | --markup-shadow-menu | 0 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-modal | 0 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-dropdown | 0 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 key | CSS custom property | Light | Dark |
|---|---|---|---|
ring | --markup-ring | var(--_mk-accent-soft) | — |
focusRing | --markup-focus-ring | 0 0 0 2px var(--_mk-ring) | — |
Motion
theme.tokens key | CSS custom property | Light | Dark |
|---|---|---|---|
transitionFast | --markup-transition-fast | 0.1s ease | — |
transitionBase | --markup-transition-base | 0.15s ease | — |
transitionSnap | --markup-transition-snap | 0.25s cubic-bezier(0.4, 0, 0.2, 1) | — |
Layering
theme.tokens key | CSS custom property | Light | Dark |
|---|---|---|---|
zDropdown | --markup-z-dropdown | 10000 | — |
zPanel | --markup-z-panel | 9999 | — |
zModal | --markup-z-modal | 10002 | — |
Sizing
theme.tokens key | CSS custom property | Light | Dark |
|---|---|---|---|
avatarXs | --markup-avatar-xs | 20px | — |
avatarSm | --markup-avatar-sm | 24px | — |
avatarMd | --markup-avatar-md | 32px | — |
avatarLg | --markup-avatar-lg | 40px | — |
btnSm | --markup-btn-sm | 28px | — |
btnLg | --markup-btn-lg | 40px | — |
ncWidth | --markup-nc-width | 280px | — |
Limits
- The placement cursor is an SVG
data:URI, and a data URI cannot read CSS custom properties. The SDK resolves--markup-cursor-fillin JavaScript and regenerates the cursor whenever the theme changes — onsetTheme(), and on an OS light/dark flip undermode: "auto"— so both channels work. A mid-session change made by editing a stylesheet from devtools will not repaint it untilsetTheme()runs. - Size tokens take absolute lengths only. Percentages are rejected by the
themeobject (32px,2remandcalc()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.avatarPaletteto change the set of colours,theme.avatarColorto decide each one — see Matching your app’s user colours — andtheme.avatarInitialsto 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(), orhsl().