Skip to Content
SDKConfiguration

Configuration

This page is the reference for every option you can pass when initializing and rendering the SDK.


MarkUpSDK.init(options)

init validates your options and constructs the SDK singleton. It throws a ConfigurationError if publicKey or markupId is missing, or if markupId is not a valid UUID.

OptionType
publicKey string

Your SDK installation public key. See Create an SDK installation.

markupId string

The MarkUp resource ID to load comments for. Must be a valid UUID.

onTokenNeeded optional() => Promise<string>

Returns a signed JWT for bring-your-own-token auth. Omit to use the built-in Sign in with MarkUp flow.

sessionStorage optional"memory" | "sessionStorage" | "localStorage"

Where to persist the resolved session. Default: "memory".

debug optionalboolean

Enables debug-level console logging. Default: false.

Full init example
import {MarkUpSDK} from "@ceros-dev/markup-sdk/ui"; const markup = MarkUpSDK.init({ publicKey: "<YOUR_PUBLIC_KEY>", markupId: "<YOUR_MARKUP_ID>", sessionStorage: "localStorage", debug: false });

markup.render(options?)

render mounts the shadow-DOM container, toolbar, pins, and panels, then runs the authentication gate to resolve a session and load live data. It throws an SDKStateError if the SDK isn’t initialized, and is a no-op (with a warning) if called twice. It returns the SDK instance, so calls can be chained.

render() is only available on the @ceros-dev/markup-sdk/ui entry point. The headless @ceros-dev/markup-sdk entry has no UI to mount.

OptionType
position optional"bottom-center" | "bottom-right" | "bottom-left" | "top-center" | "top-right" | "top-left"

Toolbar anchor. Default: "bottom-center".

offset optional{x: number; y: number}

Pixel offset from the chosen anchor. Default: {x: 16, y: 16}.

theme optional"light" | "dark" | "auto" | "inverted"

Color theme for the SDK UI. Default: "auto".

collapsed optionalboolean

Start the toolbar collapsed. Default: false.

draggable optionalboolean

Allow the user to drag the toolbar. Default: true.

persistPosition optionalboolean

Persist the dragged position across reloads. Default: false.

container optionalHTMLElement | string

Mount point for the shadow-DOM host; a string is treated as a CSS selector. Default: document.body.

zIndex optionalnumber

z-index for the shadow container. Tune it if the SDK covers your overlays. Default: a very large value.

keyboardShortcuts optionalboolean

Enable global shortcuts (e.g. press C to toggle commenting mode). Default: true.

commentableContainer optionalHTMLElement | string | Array<HTMLElement | string>

Restrict comment placement to one or more containers (CSS selector and/or element). Selectors resolve at click-time; the restriction fails closed — if none of the containers exist, commenting is blocked — and dims the page outside the allowed zone. Default: unrestricted.

pinVariant optional"numbered" | "avatar"

How comment pins are drawn. numbered is a disc carrying the thread number; avatar is a teardrop framing the thread author's avatar. Both mark the spot with a contentless draft pin while a comment is being written — there is no thread yet to have a number or an author. Fixed for the instance's lifetime — a second render() is a no-op, so changing it means destroy() then init() then render(), which also clears the cached auth session. Default: "numbered". See Avatar pins.

commentPlacement optional"single" | "continuous"

Whether commenting mode survives placing a comment. single leaves the mode as soon as a comment is posted; continuous keeps the cursor armed for the next pin. Default: "single". See Continuous commenting.

pinVisibility optional"always" | "never"

Whether comment pins are drawn on the page. never leaves the page unmarked without switching commenting off — threads still load and stay readable in the comment panel. Replaceable at runtime, unlike pinVariant. Default: "always". See Pin visibility.

deviceBreakpoints optional{tablet?: number; desktop?: number}

Minimum viewport widths, in CSS pixels, at which a viewport counts as a tablet and a desktop. Either bound may be given on its own; the other keeps its default. tablet must be below desktop, and an unusable value falls back with a warning rather than throwing. Default: {tablet: 768, desktop: 1024}. See Device types.

deviceScope optional"auto" | "all" | "mobile" | "tablet" | "desktop"

Which devices’ comments to show. auto shows only those left on the device the viewport is currently in, and re-evaluates as the window crosses a breakpoint; all drops the filter; a device name pins the view to that band. Replaceable at runtime with setDeviceScope. Default: "auto". See Device types.

Full render example
markup.render({ position: "bottom-right", offset: {x: 24, y: 24}, theme: "auto", draggable: true, keyboardShortcuts: true });

Toggle shortcuts at runtime after render():

Toggle shortcuts at runtime
markup.setKeyboardShortcutsEnabled(false);

Continuous commenting

By default, placing a comment ends commenting mode — every pin starts with the toolbar’s + button or the C shortcut. For a review pass that leaves a dozen comments in one go, commentPlacement: "continuous" keeps the cursor armed after each post, so the reviewer only enters the mode once.

Continuous commenting
markup.render({commentPlacement: "continuous"});

The two modes differ only in how commenting mode ends:

single (default)continuous
After a comment is postedMode endsCursor re-arms for the next pin
Escape in the composerMode endsDiscards the draft, back to placing
Second Escape—Mode ends
Toolbar +, or CToggles the modeToggles the mode

commenting:start and commenting:stop bracket the whole run either way, so a continuous session that places ten pins emits one start, ten pin:placed, and one stop. Pausing the project mid-session ends commenting mode as usual.

Pin visibility

By default every thread on the page gets a pin. pinVisibility: "never" leaves the page unmarked instead — useful when the host surfaces comments through its own UI, or wants them revealed only on demand. Nothing else changes: threads still load, the toolbar still counts them, and they stay readable in the comment panel.

Start with an unmarked page
markup.render({pinVisibility: "never"});

Unlike pinVariant, this is not fixed at render time. Drive it from your own control, and read markup.pinsVisible to label that control rather than tracking the calls yourself:

A host-owned show-comments toggle
const button = document.querySelector("#show-comments"); button.addEventListener("click", () => { if (markup.pinsVisible) { markup.hidePins(); } else { markup.showPins(); } button.textContent = markup.pinsVisible ? "Hide comments" : "Show comments"; });

showPins(), hidePins() and setPinVisibility(mode) are all callable before render(), and a call made there is kept — render() only writes the option when you actually pass one.

Hiding takes the pin elements off the page rather than making them transparent, so nothing is left behind to intercept a click. Two consequences worth knowing:

  • Opening a thread from the comment panel still works, but the scroll-to-pin that normally accompanies it does nothing while pins are hidden — there is no pin to scroll to.
  • While a comment is being written, the marker beside the composer is still drawn. It shows where that comment will land and disappears when the comment is posted or discarded, so it follows the composer rather than this option.

Device types

Every comment records the device it was left on — mobile, tablet or desktop — and by default a reader sees only the comments belonging to the device they are currently on. A pin dropped on a phone layout is anchored to a phone layout; surfacing it on a 1440px desktop would put it somewhere that no longer means anything.

The device is decided by viewport width alone, never by the user agent, so a desktop browser narrowed to 600px counts as a mobile — which is the point when someone is reviewing a responsive layout by resizing the window. It is re-evaluated as the window crosses a breakpoint, and both the pins and the comment panel follow.

The default bands are mobile below 768px, tablet from 768px, desktop from 1024px. Override them when your own layout breaks elsewhere, so a comment left on what your CSS calls a tablet is filed and found as one:

Your own breakpoints
markup.render({deviceBreakpoints: {tablet: 640, desktop: 1280}});

Both bounds are minimum widths, matching mobile-first CSS, and either may be given on its own — {tablet: 640} leaves desktop at 1024. markup.setDeviceBreakpoints(...) moves them after render(), for a host whose breakpoints are not known until after mount.

A comment’s device is fixed when it is created and cannot be changed afterwards, including by moving its pin. Comments on image and PDF markups carry no device and are shown at every width.

The device indicator

Each comment header carries a small device glyph — monitor, tablet or phone — in place of the device’s name: in the comment panel, in the pin popover, and on the card shown while the pointer rests on a pin. In the first two, hovering or focusing it reveals the browser the comment was left in: Browser (Google Chrome 151), OS (macOS 10.15.7), Screen (1728 × 1117) and Viewport (1413 × 962). The hover-preview card shows the glyph alone — it is read-only and closes on its own timer, so a panel opened inside it would outlive it.

Screen and viewport are listed separately on purpose. A pin that looks misplaced is almost always a viewport difference, and that number appears nowhere else in the UI.

Rows are omitted individually when a value was not recorded, and the panel is skipped entirely when nothing was — the glyph still names the device through its accessible label, so the indicator never becomes an empty box. Comments on image and PDF markups record no browser context at all. One thing worth knowing when reading it: a pin that has been moved carries the browser of whoever moved it last, not of the comment’s author.

thread.browserContext (typed BrowserContext) carries the same data on every entry point, for an embed drawing its own UI. thread.deviceType is resolved against the markup’s view modes by the /ui entry, so a headless embed sees it absent — join thread.viewModeId against markup.projects.listViewModes() there.

Upgrading an existing installation? Comments created by earlier SDK versions are all recorded as desktop — those builds sent the desktop view mode whatever the viewport was. Under the default auto scope a reader on a phone or tablet will not see any of them, and there is no migration, since a thread’s device cannot be changed after it is written. Render with deviceScope: "all" if you need the previous behaviour.

Showing every device

deviceScope: "all" switches the filter off; a device name pins the view to that band regardless of the viewport, for a host driving the choice from its own responsive-preview control.

Every comment, whatever device it came from
markup.render({deviceScope: "all"});

A pinned device also decides which device new comments are filed under, not only which are shown. That is deliberate: a reviewer working in a pinned mobile view is commenting on the mobile layout, and filing against the window they happen to have open would both mis-file the comment and hide it the moment it was posted. auto and all file against the measured viewport.

The prebuilt comment panel exposes the same choice: its filter menu carries a Device section with All devices, Desktop, Tablet and Mobile. auto has no row of its own — it is the default, and it is what no row ticked means — so a reader picks a row to pin the view to that band and picks the ticked row again to clear it back to following the window. The rows are there for someone triaging another device’s comments without resizing to it; anyone reproducing on the device itself is already served by auto. device:changed fires whichever way the scope moves — from the menu, from the API, or from the window crossing a breakpoint.

Like pinVisibility, this is replaceable at runtime and callable before render() — a call made there is kept, because render() only writes the option when you pass one.

A host-owned device filter
markup.setDeviceScope("all"); // every comment markup.setDeviceScope("mobile"); // the mobile band, whatever the window is doing markup.setDeviceScope("auto"); // back to following the viewport markup.deviceType; // "mobile" | "tablet" | "desktop" — what the viewport measures as markup.deviceScope; // what was asked for, so "auto" reads back as "auto" markup.on("device:changed", ({deviceType, scope, visible}) => { // `visible` is what the filter is actually using: a device name, or "all". button.textContent = visible === "all" ? "All devices" : `${visible} only`; });

The page unread counts follow the scope, for the same reason the panel does: a badge counting comments the reader has no way to open is a badge they cannot clear. The project counts in the same unread:changed payload deliberately do not — they answer “is there anything unread in this markup”, a question the current viewport does not narrow.


Entry points

Importrender() available?Notes
@ceros-dev/markup-sdk/uiYesFull UI: toolbar, pins, commenting mode. Bundles Preact.
@ceros-dev/markup-sdkNoHeadless API only: services, auth, events. Render your own UI.

Everything else — init, on/off/once, the projects / threads / comments / uploads services, and destroy — is identical on both entries.


Logging

Configure the SDK’s logger globally with configureLogger. Useful for routing SDK logs into your own telemetry or quieting them in production.

configureLogger
import {configureLogger} from "@ceros-dev/markup-sdk/ui"; configureLogger({ level: "warn", // "debug" | "info" | "warn" | "error" — default "warn" prefix: "[MarkupSDK]", // prefix on every log line enabled: true // set false to silence the SDK entirely });
FieldTypeDescription
level"debug" | "info" | "warn" | "error"Minimum level to output. Default "warn".
prefixstringPrefix for all log messages. Default "[MarkupSDK]".
enabledbooleanEnable or disable logging entirely. Default true.

Passing debug: true to MarkUpSDK.init() is a shortcut that raises the log level to debug for that instance.


Next steps

Last updated on