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.
| Option | Type | |
|---|---|---|
| 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: |
| debug optional | boolean | Enables |
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/uientry point. The headless@ceros-dev/markup-sdkentry has no UI to mount.
| Option | Type | |
|---|---|---|
| position optional | "bottom-center" | "bottom-right" | "bottom-left" | "top-center" | "top-right" | "top-left" | Toolbar anchor. Default: |
| offset optional | {x: number; y: number} | Pixel offset from the chosen anchor. Default: |
| theme optional | "light" | "dark" | "auto" | "inverted" | Color theme for the SDK UI. Default: |
| collapsed optional | boolean | Start the toolbar collapsed. Default: |
| draggable optional | boolean | Allow the user to drag the toolbar. Default: |
| persistPosition optional | boolean | Persist the dragged position across reloads. Default: |
| container optional | HTMLElement | string | Mount point for the shadow-DOM host; a string is treated as a CSS selector. Default: |
| zIndex optional | number | z-index for the shadow container. Tune it if the SDK covers your overlays. Default: a very large value. |
| keyboardShortcuts optional | boolean | Enable global shortcuts (e.g. press |
| commentableContainer optional | HTMLElement | 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. |
| commentPlacement optional | "single" | "continuous" | Whether commenting mode survives placing a comment. |
| pinVisibility optional | "always" | "never" | Whether comment pins are drawn on the page. |
| 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. |
| deviceScope optional | "auto" | "all" | "mobile" | "tablet" | "desktop" | Which devices’ comments to show. |
markup.render({
position: "bottom-right",
offset: {x: 24, y: 24},
theme: "auto",
draggable: true,
keyboardShortcuts: true
});Toggle shortcuts at runtime after render():
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.
markup.render({commentPlacement: "continuous"});The two modes differ only in how commenting mode ends:
single (default) | continuous | |
|---|---|---|
| After a comment is posted | Mode ends | Cursor re-arms for the next pin |
Escape in the composer | Mode ends | Discards the draft, back to placing |
Second Escape | — | Mode ends |
Toolbar +, or C | Toggles the mode | Toggles 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.
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:
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:
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 defaultautoscope 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 withdeviceScope: "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.
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.
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
| Import | render() available? | Notes |
|---|---|---|
@ceros-dev/markup-sdk/ui | Yes | Full UI: toolbar, pins, commenting mode. Bundles Preact. |
@ceros-dev/markup-sdk | No | Headless 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.
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
});| Field | Type | Description |
|---|---|---|
level | "debug" | "info" | "warn" | "error" | Minimum level to output. Default "warn". |
prefix | string | Prefix for all log messages. Default "[MarkupSDK]". |
enabled | boolean | Enable or disable logging entirely. Default true. |
Passing
debug: truetoMarkUpSDK.init()is a shortcut that raises the log level todebugfor that instance.
Next steps
- API reference — methods, services, events, and errors.
- Authentication — what
onTokenNeededreturns.