Web SDKv1.0.0

Theme tokens

Everything the SDK paints is driven by CSS custom properties. You restyle it by setting those properties — never by overriding our class names, which are not a stable contract and change between releases. There are four ways in, in increasing order of effort: match your brand color with theme.accentColor (one line); match your whole design system by overriding the semantic tokens (~15 lines); retune one component by overriding its component tokens (a few lines); or replace our chrome entirely with toolbar.layout: 'none' and the viewer API (you own the UI).

The three token layers. Primitives (the raw ramp, #4f46e5) → semantic (names a role, --ks-accent) → component (names a part, --ks-thumbnail-active-border, the current page's marker; #ffffff → --ks-surface → --ks-toolbar-bg). 1. Primitives are the raw ramp. They are not part of the public contract and may change in any release. Do not reference them. 2. Semantic tokens name a role, not a color. These are the contract. Set --ks-surface and every surface in the SDK follows — toolbar, panels, popovers, dialogs. 3. Component tokens each fall back to a semantic token, so you can retune one part without redefining the system. --ks-toolbar-bg defaults to var(--ks-surface); set it and only the toolbar moves. Work at the highest layer that solves your problem. Overriding --ks-surface once is better than overriding eight component backgrounds, because it also catches the components added in the next release.

Example

CSS
/* Our whole design system: override the semantic tokens once.
   Everything — toolbar, ribbon, panels, dialogs, popovers, and
   whatever we add next release — follows. */
:root {
  --ks-accent: #e91e63;
  --ks-accent-hover: #c2185b;
  --ks-accent-subtle: #fce4ec;
  --ks-accent-text: #ffffff;

  --ks-surface: #ffffff;
  --ks-surface-raised: #fafafa;
  --ks-surface-sunken: #f1f1f4;

  --ks-text: #1a1a2e;
  --ks-text-muted: #6b6b80;
  --ks-border: #e4e4ec;

  --ks-font: 'Inter', system-ui, sans-serif;
  --ks-radius: 4px;
  --ks-radius-sm: 3px;
}

/* One viewer instance, if you host several. */
#invoice-viewer {
  --ks-accent: #e91e63;
}

/* A dark theme of your own (theme.mode: 'auto'). */
:root[data-ks-theme='dark'] {
  --ks-surface: #11131a;
  --ks-surface-raised: #171a22;
  --ks-surface-sunken: #0c0e13;
  --ks-text: #e8e8f0;
  --ks-text-muted: #9a9ab0;
  --ks-border: #252833;
  --ks-accent: #f48fb1; /* lighter in dark — contrast, not hue */
  --ks-accent-text: #11131a;
  --ks-canvas-bg: #07080b;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-ks-theme='light']) {
    /* same block */
  }
}

Three ways in

You want toDo thisEffort
Match your brand colortheme.accentColorOne line
Match your whole design systemOverride the semantic tokens~15 lines
Retune one componentOverride its component tokensA few lines
Replace our chrome entirelytoolbar.layout: 'none' + the viewer APIYou own the UI

Structural classes (a contract: for placing, sizing and hiding a region, not for repainting it)

ClassOn
.ks-viewerThe viewer's root, inside your container; carries the data attributes
.ks-toolbarThe toolbar, in either layout (the ribbon's root carries .ks-ribbon too)
.ks-nav-barThe navigation bar
.ks-bodyWhat is under the toolbar: the sidebar and the pages
.ks-sidebarThe sidebar: its rail and the open panel
.ks-page-containerThe scrolling area the pages sit in
.ks-pageOne page: its canvas, text layer, fields and marks
.ks-portalEvery root the SDK renders outside the viewer: dialogs, sheets, popovers
.ks-dialog-backdropThe veil behind a dialog
.ks-dialogA dialog
.ks-toastA toast
.ks-loading-overlayThe document loader PDFViewer.showLoadingIndicator shows, in your container
.ks-busy-overlayThe veil shown while a tool prepares its UI, in your container

Semantic tokens: surfaces

TokenLightDarkWhere it lands
--ks-surface#ffffff#1c1c1fToolbar, panels, dialogs, popovers
--ks-surface-raised#fcfcfd#202024Raised panels: dialogs, the ribbon, cards
--ks-surface-sunken#f4f4f6#141417Button hover, ribbon tab strip, inset wells
--ks-surface-overlayrgba(24,24,27,.42)rgba(0,0,0,.6)Modal and panel backdrops
--ks-canvas-bg#f4f4f6#0e0e10The ground the pages sit on

Semantic tokens: ink

TokenLightDarkWhere it lands
--ks-text#18181b#f4f4f5Body and primary labels
--ks-text-muted#71717a#a1a1aaTool labels, secondary text
--ks-text-subtle#a1a1aa#71717aPlaceholders, disabled hints
--ks-text-inverted#fafafa#18181bText on a filled accent surface

Semantic tokens: lines

TokenLightDarkWhere it lands
--ks-border#e8e8ea#2e2e33Dividers, panel edges, toolbar rule
--ks-border-strong#d4d4d8#3f3f46Input outlines, scrollbar thumbs
--ks-select-caretgray caret imagesameThe chrome's own dropdowns' caret. A url(...): a data URI cannot read a var(), so the image is the token

Semantic tokens: accent (one accent, used for every selected, active and primary state)

TokenLightDarkWhere it lands
--ks-accent#4f46e5#a5b4fcActive tool, selected tab, primary button
--ks-accent-hover#4338ca#c7d2feHover on the above
--ks-accent-subtle#eef2ff#312e5cActive-tool wash, selected row
--ks-accent-text#ffffff#18181bText on a filled accent

Semantic tokens: status

TokenLightDarkWhere it lands
--ks-danger#b91c1c#fca5a5Destructive actions, validation errors
--ks-danger-subtle#fee2e2#3f1d1dError banner background
--ks-danger-border#fca5a5#b91c1cError banner edge
--ks-warning#b45309#fde68aRequired-field and permission notices
--ks-warning-subtle#fffbeb#3a2e12Warning banner background
--ks-warning-border#fde68a#b45309Warning banner edge
--ks-success#15803d#4ade80Save and export confirmations
--ks-guide#db2777#f472b6Snap guides while an object is moved or resized
--ks-success-subtle#dcfce7#14321fSuccess banner background

Semantic tokens: elevation

TokenLightDarkWhere it lands
--ks-shadow-sm0 1px 3px rgba(16,16,20,.07)0 1px 3px rgba(0,0,0,.5)Resting chrome, primary buttons
--ks-shadow-md0 6px 20px rgba(16,16,20,.15)0 6px 20px rgba(0,0,0,.55)Popovers, dropdowns, option bars
--ks-shadow-lg0 12px 36px rgba(16,16,20,.18)0 12px 36px rgba(0,0,0,.6)Modals

Type (does not change between themes)

Type tokenValue
--ks-fontsystem sans stack
--ks-font-monosystem mono stack
--ks-text-2xs10px
--ks-text-xs11px
--ks-text-sm12px
--ks-text-base13px
--ks-text-md14px
--ks-text-lg16px
--ks-text-xl20px
--ks-text-2xl24px

Space and shape (does not change between themes)

Space & shape tokenValue
--ks-space-14px
--ks-space-28px
--ks-space-312px
--ks-space-416px
--ks-space-524px
--ks-space-632px
--ks-radius-sm6px
--ks-radius9px
--ks-radius-lg13px
--ks-radius-full999px

Interaction and motion

Interaction & motion tokenValue
--ks-touch-target44px
--ks-focus-ring2px solid var(--ks-accent)
--ks-focus-offset2px
--ks-ease-outcubic-bezier(.2,0,0,1)
--ks-ease-incubic-bezier(.4,0,1,1)
--ks-duration-instant80ms
--ks-duration-fast140ms
--ks-duration-base200ms
--ks-duration-slow280ms

Component tokens: toolbar (layout: 'bar')

TokenDefaultControls
--ks-toolbar-bgvar(--ks-surface)Bar background
--ks-toolbar-bordervar(--ks-border)Bottom rule
--ks-toolbar-height64pxBar minimum height (48px compact, 46px narrow/tiny)
--ks-btn-hover-bgvar(--ks-surface-sunken)Tool button hover
--ks-btn-disabled-opacity0.35Disabled tool opacity

Component tokens: ribbon (layout: 'ribbon'; only injected when the ribbon is the chosen layout)

TokenDefaultControls
--ks-ribbon-bgvar(--ks-surface)Tool-row background
--ks-ribbon-tabs-bgvar(--ks-surface-sunken)Tab-strip background
--ks-ribbon-bordervar(--ks-border)Strip and bottom rules
--ks-ribbon-tabs-height40pxTab strip height (34px at tiny)
--ks-ribbon-row-height64pxTool row height (56px compact, 48px narrow/tiny)
--ks-ribbon-tool-min-width64pxTool button width (52px compact, 40px narrow/tiny)
--ks-ribbon-disabled-opacity0.35Disabled tool opacity

Component tokens: navigation bar

TokenDefaultControls
--ks-nav-bgvar(--ks-surface)Pill background
--ks-nav-textvar(--ks-text)Pill text and icons
--ks-nav-height44pxPill height

Component tokens: thumbnails

TokenDefaultControls
--ks-thumbnail-active-bordervar(--ks-accent)The current page's marker bar

Component tokens: dialogs (the shared dialog: Download, Print, "Leave the document?" and every confirmation — .ks-dialog; a confirmation also carries ks-confirm and data-ks-confirm="<action>")

TokenDefaultControls
--ks-dialog-bgvar(--ks-surface)Dialog background
--ks-dialog-bordervar(--ks-border)Dialog edge
--ks-dialog-radiusvar(--ks-radius-lg)Corner radius
--ks-dialog-shadowvar(--ks-shadow-lg)Drop shadow
--ks-dialog-width440pxWidth of the Download and Print dialogs; the viewport's on a phone
--ks-dialog-sm-width360pxWidth of a small dialog — every confirmation; the viewport's on a phone

Component tokens: page area

TokenDefaultControls
--ks-viewer-bgvar(--ks-canvas-bg)Ground behind the pages

Page-layer tokens (one value for both themes; they do not follow mode)

TokenDefaultControls
--ks-paper#ffffffThe page itself, and thumbnails
--ks-field-bgrgba(195,222,255,.42)Form field tint
--ks-field-bg-hoverrgba(195,222,255,.55)Form field tint, hovered
--ks-field-border-focus#1a5fa8A checked box's tick and a selected radio's dot
--ks-field-text#18181bWhat is typed into a field
--ks-field-text-muted#71717aA read-only field's value
--ks-field-text-subtle#a1a1aaA signature field's "Sign here"
--ks-field-text-negative#ff0000A negative number a field's format shows in red
--ks-field-readonly-bg#f4f4f6A read-only field, under its hatch
--ks-field-required-bgrgba(180,83,9,.16)Required, still-empty field tint
--ks-field-required-border#b45309Required field edge and corner dot
--ks-field-select-careta gray caret imageA form dropdown's caret (a url(...))
--ks-search-hitrgba(255,235,59,.4)Every search match
--ks-search-hit-activergba(255,152,0,.55)The current match
--ks-note-pin#f9a825Sticky-note pin on the page
--ks-block-mark#6366f1The outline Edit Text draws round an editable text block

Stacking order

TokenValueLayer
--ks-z-page1The base the annotation layer stacks on
--ks-z-field10Form field widgets
--ks-z-mark20Selection, search hits, editable-block marks
--ks-z-object30Placed-object and text-block overlays
--ks-z-handle40Resize grips and rotation handles
--ks-z-contextBar50Floating bars anchored to a selection
--ks-z-chrome100Toolbar, sidebar, navigation bar
--ks-z-popover200Popovers, dropdowns, color pickers
--ks-z-backdrop300Modal backdrops
--ks-z-modal310Modal surfaces
--ks-z-question320A confirmation asked over a modal
--ks-z-toast400Toasts and transient confirmations
--ks-z-drag500Drag ghosts

Notes

Where to put your overrides. Tokens are declared on :root and inherited, so a value you set on :root reaches the viewer and the surfaces that render into document.body (the signature dialog, the security modal, every popover), and a value on your own container reaches the viewer inside it: :root { --ks-accent: #e91e63; } for everything, including portalled dialogs (usually what you want); #invoice-viewer { --ks-accent: #e91e63; } for one viewer instance, if you host several. Your stylesheet loads after ours, so an override at equal specificity wins without !important. Two :root rules tie on specificity and source order decides — yours is later, so yours applies.

theme.mode and theme.accentColor are applied to the viewer's own root element inside your container, and mirrored to the portal layer so dialogs follow. With mode: 'light' (the default) your :root and container values reach the viewer and its dialogs; only on a dark page — a dark OS, or data-ks-theme="dark" on <html> — does the viewer declare the SDK's light values on itself, so it stays light. mode: 'dark' always declares the SDK's dark values on the viewer, over the page and over your :root values (your accentColor still applies). For a dark palette of your own, use mode: 'auto' (see the dark-theme recipe below). To style portalled surfaces directly, target .ks-portal — it is on every root the SDK renders outside the viewer. Dialogs, sheets and popovers live in one portal layer shared by the whole page. With several viewers in different themes or accents, they take the theme of the viewer created last. See Theme.

What you may target. Three things in the SDK's markup are a contract, kept across releases: the tokens (--ks-*): the colors, type, spacing and stacking, listed in the tables above — they are how a theme is written; the data attributes on the viewer element: data-ks-theme, data-ks-size, data-ks-pointer, data-ks-height, data-ks-toolbar, data-ks-nav, data-ks-panels, data-ks-touch, data-ks-text-edit, data-ks-phone, data-ks-rails, data-ks-tablet, data-ks-tool, data-ks-touch-drag and data-ks-print — see Layout; and the structural classes in the table above, for placing, sizing and hiding a region, not for repainting it. Every other class (.ks-tb-btn, say) is internal: it may change in any release without notice. Set the tokens instead; they are what we promise not to break.

Semantic tokens — the contract. These are the names to set. Every value in the tables is the light-theme default; the dark column is what the same token becomes under mode: 'dark' or a dark OS. The dark accent gets lighter, not darker: indigo-600 on near-black fails contrast, the same hue at 300 passes. If you override the accent for dark mode, do the same. With theme.accentColor, the other shades are derived for you from the theme itself: hover mixes toward --ks-text (darker on light, lighter on dark), and the wash mixes toward --ks-surface, so one color works in both themes.

Type, space, shape, motion do not change between themes. Spacing is a 4px step. Most type sizes the chrome draws are --ks-text-* steps — set --ks-font or a step to retune them in one place. A few stay fixed on purpose: the 16px an input needs on a touch screen (below it iOS zooms the page), text that depicts content such as a typed signature's preview, and a handful of optical sizes tuned to a specific cell (the phone dock and toolbar strip labels, some signature-dialog captions). --ks-text-xl, --ks-text-2xl, --ks-space-6 and --ks-ease-in are part of the scale but no component uses them yet; setting them has no visible effect today. --ks-accent-border (derived from accentColor) and --ks-field-border are still declared, but nothing reads them either.

Component tokens — per-component control. Each falls back to a semantic token. Set one to move a single component. The toolbar, ribbon, navigation bar, thumbnails, dialogs and page-area tables above list them.

What the theme does not touch. Some colors belong to the document, not to your UI, and deliberately ignore mode and your overrides of the chrome tokens. The page stays white in dark mode. A PDF is white because the document is white. Inverting it would change the content, turn a scan into a negative, and break the match between what is on screen and what will print. Only the ground around the page darkens. The page-layer tokens exist in one value for both themes. You can still retune them; they just do not follow mode. A focused or hovered field is outlined in --ks-accent. Also unthemed, because they become part of the saved file: annotation ink, highlight colors and stamp presets. What the user draws is content. Those are chosen in the tool's own options bar, and exporting the document keeps them exactly as drawn.

Stacking order. Every layer the SDK paints has a named z-index, so you can predict what sits above what and place your own chrome around it. To float your own header above the viewer's chrome but below its dialogs, sit between --ks-z-chrome and --ks-z-popover. The docked chrome, popovers, modals, toasts and the on-page marks, objects and handles use these layers; --ks-z-field and --ks-z-drag are reserved and not read by any layer. The signature dialog and Edit Text's floating sheet and hints sit at fixed values of 10000–10002, so only a value above 10002 guarantees your own chrome sits above everything of ours — except the license-required notice, which always stays on top.

Accessibility the SDK already handles. Keep these if you restyle; they are cheap and they are what the viewer is designed around so that everyone can use it — with a skip link, a roving toolbar, focus-trapped dialogs, live regions, the text layer and 44 px touch targets. Focus: every control shows a :focus-visible ring built from --ks-focus-ring and --ks-focus-offset. If you override the accent, the ring follows automatically. Do not set outline: none. The rule is scoped to .ks-viewer and .ks-portal, so it reaches dialogs too but never leaks into your own page. Reduced motion: under prefers-reduced-motion: reduce transitions drop to 1ms — not 0, so transitionend handlers still fire and nothing that waits on one stalls. Forced colors: under forced-colors: active the focus ring switches to the system Highlight color, and the ribbon re-states its selected tab and active tool with system keywords — a background wash alone disappears in that mode. If you restyle a selected state with color only, add a border or outline for this case. Contrast: the defaults are designed for WCAG AA contrast against their own surfaces. If you change the accent, check --ks-accent-text against it — a light accent needs dark text on it.

Recipe: just our brand color. new PDFViewer(container, doc, { theme: { accentColor: '#e91e63' } }); — hover, subtle and border shades are derived from it. Nothing else to set.

Recipe: follow the OS. new PDFViewer(container, doc, { theme: { mode: 'auto' } });

Recipe: switching theme live. Keep theme.mode: 'auto' and set the attribute on <html> from your own theme toggle. The viewer and its dialogs switch immediately, with no rebuild: document.documentElement.dataset.ksTheme = isDark ? 'dark' : 'light';. Remove the attribute to follow the OS again.

Recipe: our whole design system. Override the semantic tokens once, as in the example above. With the default mode: 'light' these values apply wherever the page around the viewer is light; with mode: 'auto' they apply in both themes. To give dark its own values, add a dark block too.

Recipe: a dark theme of your own. Leave theme.mode at 'auto' and redefine the tokens under both selectors, as in the example above: the first (:root[data-ks-theme='dark']) for a page that sets data-ks-theme="dark" on <html> itself, the second (@media (prefers-color-scheme: dark) { :root:not([data-ks-theme='light']) { … } }) for a dark OS. (An explicit theme.mode: 'dark' uses the SDK's own dark palette — your accentColor still applies.)

Recipe: a denser toolbar. :root { --ks-toolbar-height: 56px; --ks-space-2: 6px; }. In a compact or narrow viewer the toolbar uses its own smaller heights.

Recipe: replace our chrome entirely. new PDFViewer(container, doc, { toolbar: { layout: 'none' }, sidebar: { rail: false }, navigation: { pageControls: false, zoomControls: false } }) (rail: false for your own Pages button, not ours beside it), then your buttons, our SDK: activateTool and download are async — viewer.activateTool('highlight').catch(...) rejects with INVALID_ARGUMENT (the tool is off here), FEATURE_NOT_LICENSED (not in the license) or PERMISSION_DENIED (the document forbids it); myHighlightButton.hidden = !viewer.isToolEnabled('highlight'); myUndoButton.onclick = () => void viewer.undo(); mySaveButton.onclick = () => void viewer.createExporter().download(). Which button is pressed comes from toolChanged: between tools the resting tool runs — Select by default — so it reports 'selectText' from the first paint, and Search and Manage Pages are panels (the exported PANEL_TOOL_NAMES): activating them leaves activeTool as it was. Your Pages button opens or closes through viewer.openPanel(viewer.activePanel === 'pages' ? null : 'pages') and follows panelChanged, whoever made the change — the drawer's backdrop, Escape inside the drawer, a resize. The page area, panels and every tool still work; only our chrome is gone. On a phone the active tool's options still rise in the inspector sheet above your own bar (see Layout). See PDFViewer for what you can drive, and Bring your own chrome.