@sorb/leaf reference
When you finish this page you know every export of the Sorb™ React SDK, which group it belongs to, and the smallest working example for each. You need a React 18 app (or any page that can run an ES module) and a generated token set. For the step-by-step setup, read React SDK first — this page is the reference.
npm install @sorb/leaf
@sorb/leaf has one runtime dependency, @sorb/core, and one peer dependency,
react@^18. React is only needed for the components and hooks; sorbInit,
the sanitizer, the legacy map and the target adapters all run without it.
The eight groups
The package exports 30 names. They fall into eight groups, and most apps only ever touch the first.
| Group | Exports | You need it when |
|---|---|---|
| Provider, hooks, banner | SorbProvider, useTokens, useToken, useIsPreview, usePreviewState, PreviewBanner | You have a React app. This is the whole setup. |
| Framework-free | sorbInit | You are not on React, or you drive Sorb from a plain script. |
| Dark mode | useTheme, ThemeToggle, buildModeStylesheet, injectModeStylesheet, clearModeStylesheet, MODE_STYLESHEET_ID, tailwindDarkMode, dataThemeDarkMode | Your token set ships light and dark values. |
| Target adapters | reactBootstrapTarget, mantineTarget, tailwindV4Target, shadcnTarget, primevueTarget, muiTarget, angularMaterialTarget | You want to see which UI kit a build targets, or register your own. |
| Legacy map | applyLegacyMap, clearLegacyMap, computeLegacyOverride, indexLegacyMap, normalizeProp, normalizeValue | Your app has hardcoded literals you have not tokenized yet. |
| Security | sanitizeCssValue | You inject token values yourself instead of through the provider. |
| Verification | verifyResolved | You want to assert the running DOM matches the committed resolved map. |
| Diagnostics | (config only — diagnostics.allowedOrigins) | You are debugging which project an app is bound to. |
Provider, hooks, and banner
SorbProvider is the only thing most apps mount. It takes one required prop,
config, applies the committed token values as CSS custom properties on
document.documentElement, and swaps them for a proposed set while a preview
is active. The generated variables.css still has to be imported — the
provider overrides those variables, it does not create them.
// main.jsx — lifted from the reference app, sorb-demo/main.jsx
import React from "react";
import { createRoot } from "react-dom/client";
import { SorbProvider, PreviewBanner } from "@sorb/leaf";
import { tokens } from "./src/tokens/generated/tokens";
import "./src/tokens/generated/variables.css";
import { App } from "./src/App";
const sorbConfig = {
namespace: "my-app",
tokens,
preview: {
enabled: import.meta.env.MODE !== "production",
origin: "http://localhost:7777",
pollInterval: 1500,
expectPrefixes: ["bs-"],
},
};
createRoot(document.getElementById("root")).render(
<React.StrictMode>
<SorbProvider config={sorbConfig}>
<App />
<PreviewBanner />
</SorbProvider>
</React.StrictMode>,
);
Rendering SorbProvider without config throws
Cannot read properties of undefined (reading 'tokens') at mount. The full
SorbConfig shape — darkTokens, resolved, orgKey, cloudBase,
diagnostics, legacyMap — is in the types table below.
PreviewBanner is safe to render unconditionally. It renders nothing when
there is no preview, and otherwise shows one of three states: blue while a
preview is live, amber when the preview loaded but matched none of your
expectPrefixes, red when a requested ?preview= could not be loaded. The red
state renders even though isPreview is false, because a failed preview falls
back to committed tokens. Each state's cause and fix is on
Troubleshooting.
Read the active values with the hooks:
import { useToken, useTokens, useIsPreview, usePreviewState } from "@sorb/leaf";
function Swatch() {
const primary = useToken("color-primary"); // → '#3B5BDB'
const all = useTokens(); // the whole active TokenSet
const isPreview = useIsPreview();
const { previewId, previewMismatch, previewError, clearPreview } =
usePreviewState();
return <div style={{ background: primary }}>{isPreview ? previewId : "committed"}</div>;
}
All five hooks must be called inside a mounted SorbProvider.
Framework-free: sorbInit
sorbInit(config) is the same runtime without React — connection resolution,
committed and preview loading, mode-aware injection, polling or SSE. It returns
a small store, so you can drive it from a plain module script, a Vue or Svelte
app, or a legacy page.
<script type="module">
import { sorbInit } from "@sorb/leaf";
import { tokens } from "./tokens/generated/tokens.js";
const sorb = sorbInit({
namespace: "my-app",
tokens,
preview: { enabled: true, origin: "http://localhost:7777" },
});
sorb.subscribe(() => {
const { isPreview, previewId } = sorb.getState();
document.title = isPreview ? `preview ${previewId}` : "my app";
});
</script>
The instance exposes getState, subscribe, setMode and clearPreview —
see SorbInstance in the types table. SorbProvider is a thin React
shell over exactly this object, so the DOM behavior is identical either way.
Dark mode
Dark mode activates when your config carries a darkTokens set alongside
tokens. The provider then injects a sorb-tokens stylesheet holding both
value sets instead of writing flat inline custom properties, and the mode
hooks start having a visible effect. Without darkTokens, setMode still
works but has nothing to switch.
import { SorbProvider, ThemeToggle, useTheme } from "@sorb/leaf";
import { tokens } from "./tokens/generated/tokens";
import { tokens as darkTokens } from "./tokens/generated/tokens.dark";
const config = { namespace: "my-app", tokens, darkTokens };
function Header() {
const { mode, setMode, resolvedScheme } = useTheme();
// mode: 'auto' | 'light' | 'dark' — the manual selection
// resolvedScheme: 'light' | 'dark' — what is actually on screen right now
return <ThemeToggle />;
}
The convention used to express dark mode defaults to the react-bootstrap
adapter's data-bs-theme attribute. Override it with darkModeConvention when
you target a different kit: tailwindDarkMode is Tailwind's .dark class, and
dataThemeDarkMode is the generic [data-theme] attribute.
import { dataThemeDarkMode } from "@sorb/leaf";
const config = { namespace: "my-app", tokens, darkTokens, darkModeConvention: dataThemeDarkMode };
buildModeStylesheet, injectModeStylesheet, clearModeStylesheet and
MODE_STYLESHEET_ID are the pieces the provider uses internally. They are
exported so a build step can produce or assert the exact same CSS; you do not
need them to use dark mode.
Target adapters
Importing @sorb/leaf registers seven TargetAdapter records into the
@sorb/core connector registry as a side effect. Each one names the Style
Dictionary format a build should emit for that UI kit, the custom-property
prefixes the vocabulary guard expects, and the kit's dark-mode convention.
| Export | Connector id | Emits | Dark mode |
|---|---|---|---|
reactBootstrapTarget | react-bootstrap | sorb/tokenset-esm | data-bs-theme |
mantineTarget | mantine | sorb/mantine-vars | Mantine color scheme |
tailwindV4Target | tailwind-v4 | sorb/tailwind-theme | .dark class |
shadcnTarget | shadcn | sorb/shadcn-theme | .dark class |
primevueTarget | primevue | sorb/primevue-preset | PrimeVue preset |
muiTarget | mui | sorb/mui-vars | MUI color scheme |
angularMaterialTarget | angular-material | sorb/mat-sys-vars | --mat-sys-* |
You rarely import these by name. Read one when you need its prefixes:
import { mantineTarget } from "@sorb/leaf";
import { getTarget } from "@sorb/core";
console.log(mantineTarget.expectPrefixes); // feed into config.preview.expectPrefixes
console.log(getTarget("tailwind-v4")); // same record, from the registry
The registry itself — registerTarget, getTarget (plus the registerSource/getSource and registerCodeSource/getCodeSource pairs for the other two connector axes),
resolveConnectorIds — lives in @sorb/core.
Legacy map
An app that still has hardcoded colors can be re-skinned before it is
tokenized. sorb-seed adapt writes .sorb/adapt-report.json; its auto rows
are a legacy map. Pass them to the provider and every element whose computed
value equals a row's raw gets an inline var(--<cssVar>, <raw>) override —
additive, and restored on unmount.
import report from "./.sorb/adapt-report.json";
const legacyMap = report.rows.filter((r) => r.status === "auto");
<SorbProvider config={sorbConfig} legacyMap={legacyMap}>
<App />
</SorbProvider>;
Drive it yourself outside React with applyLegacyMap(root, legacyMap) and
clearLegacyMap(handle). computeLegacyOverride, indexLegacyMap,
normalizeProp and normalizeValue are the pure decision helpers underneath —
useful when you build your own remapping pass or test one.
Security
Every token value the provider writes goes through sanitizeCssValue first.
It is deny-by-default: it rejects control characters, the context-break
characters {, } and ;, @import, javascript: and </, and any CSS
function outside its allowlist — which is what stops url(, image-set( and
expression(.
import { sanitizeCssValue } from "@sorb/leaf";
sanitizeCssValue("#f26722"); // { ok: true, value: '#f26722' }
sanitizeCssValue("url(https://evil.example/x.png)"); // { ok: false, reason: ... }
Call it yourself only if you inject values through your own code path. Two
further guards are config, not exports: preview.allowedOrigins is the
allowlist of non-localhost bridge origins the SDK will accept a preview from,
and preview.expectPrefixes declares the custom-property vocabulary your app
actually reads, so a preview that would change nothing is flagged instead of
failing silently.
Verification
verifyResolved reads each token's value back off :root and asks the bridge
whether the running app matches the committed resolved map.
import { verifyResolved } from "@sorb/leaf";
const result = await verifyResolved(["button-primary-bg-default"], {
origin: "http://localhost:7777",
});
// { ok: true, checked: 1, matched: 1 }
Call it from inside a mounted provider. Without one, custom properties read
back as unresolved var(...) references and the result is
{ ok: false, reason: 'provider-not-applied' } rather than a misleading
mismatch. Omit key for the local bridge; pass config.preview.key for a
hosted one.
Diagnostics
The SDK answers a sorb-ping postMessage with a sorb-hello fingerprint —
namespace, the last four characters of the key, the SDK version, the bridge
origin, and the outcome of the last preview attempt. It never posts
unsolicited, it replies only to the exact origin that pinged, and it never
sends a full key.
Only allowlisted origins get an answer. The Sorb Cloud dashboard is allowlisted by default; extend the list for a self-hosted dashboard:
const config = {
namespace: "my-app",
tokens,
diagnostics: { allowedOrigins: ["https://dashboard.example.com"] },
};
Nothing about authorization, entitlement or routing may be derived from a
sorb-hello. It exists so you can tell which project an app is bound to when a
preview does not appear.
Exports
| Export | Kind | Description | Source |
|---|---|---|---|
SorbProvider | function | SorbProvider — the React shell over sorbInit (./core.js, the framework-free injector; component-compat-roadmap P0). | src/TokenProvider.jsx:44 |
sorbInit | function | Framework-free Sorb entry point. | src/core.js:132 |
PreviewBanner | function | Drop-in banner that appears at the bottom of the screen for a Sorb preview. | src/PreviewBanner.jsx:23 |
useTokens | function | Returns the full active token set (committed or preview). | src/hooks.js:7 |
useToken | function | Returns a single token value by key. | src/hooks.js:19 |
useIsPreview | function | Returns whether a preview token set is currently active. | src/hooks.js:33 |
usePreviewState | function | Returns full preview state — useful for building a preview banner. | src/hooks.js:53 |
useTheme | function | Real-dark-mode (spec D3): the manual mode selection + the live-resolved scheme actually in effect. | src/hooks.js:71 |
ThemeToggle | function | Drop-in Light / Dark / Auto mode toggle (real-dark-mode spec D3). | src/ThemeToggle.jsx:26 |
sanitizeCssValue | function | Validate an untrusted CSS token value before it is injected via setProperty. | src/sanitize.js:69 |
verifyResolved | function | Read each token's resolved value off :root and ask the bridge whether the running app matches the committed resolved map. | src/verify.js:37 |
buildModeStylesheet | function | Builds the mode-aware CSS text carrying both a light and (optionally) a dark value-set for the same token ids — real-dark-mode spec D2/D3. | src/modeStylesheet.js:42 |
injectModeStylesheet | function | Upserts a <style id="sorb-tokens"> tag in <head> carrying mode-aware CSS (real-dark-mode spec D3) — the injection path used when a theme has both a light and a dark value-set (see buildModeStylesheet, ./modeStylesheet.js). | src/apply.js:89 |
clearModeStylesheet | function | Removes the <style id="sorb-tokens"> tag injected by injectModeStylesheet, if present. | src/apply.js:106 |
MODE_STYLESHEET_ID | value | The id of the <style> tag injectModeStylesheet upserts. | src/apply.js:64 |
tailwindDarkMode | value | Tailwind's darkMode: 'class' convention — a .dark class toggled on documentElement (typically <html>). | src/darkModeConventions.js:26 |
dataThemeDarkMode | value | A generic [data-theme="..."] attribute convention — the same shape as react-bootstrap's data-bs-theme but under the more common data-theme attribute name, for hosts that don't use Bootstrap's specific convention. | src/darkModeConventions.js:39 |
applyLegacyMap | function | Walk root (and its descendants), and for every element whose computed value for a mapped property equals a raw in the legacyMap, override that element's INLINE style for that property to var(--<cssVar>, <raw>). | src/legacyDom.js:25 |
clearLegacyMap | function | Restore every inline style override recorded by applyLegacyMap, returning each element to its original (usually empty) inline value. | src/legacyDom.js:77 |
computeLegacyOverride | function | PURE decision logic. | src/legacyMap.js:138 |
indexLegacyMap | function | Index a legacyMap into a prop → [{ normValue, cssVar, raw }] lookup so the decision function is O(1)-per-prop instead of scanning the whole array. | src/legacyMap.js:108 |
normalizeProp | function | Map a CSS property name (camelCase from JS style objects, or kebab-case from computed style) to a canonical kebab-case form for comparison. | src/legacyMap.js:23 |
normalizeValue | function | Normalize a style value so an authored literal ("#0F65EF", "4", "4px") compares equal to its computed-style form. | src/legacyMap.js:80 |
reactBootstrapTarget | value | TargetAdapter react-bootstrap (the default target): emits sorb/tokenset-esm, expects the bs- prefix, dark mode via data-bs-theme. | src/targets/reactBootstrap.js:31 |
mantineTarget | value | TargetAdapter mantine: emits sorb/mantine-vars and declares the kit prefixes the vocabulary guard expects. | src/targets/mantine.js:28 |
tailwindV4Target | value | TargetAdapter tailwind-v4: emits sorb/tailwind-theme; dark mode via Tailwind's .dark class. | src/targets/tailwindV4.js:25 |
shadcnTarget | value | TargetAdapter shadcn: emits sorb/shadcn-theme for shadcn/ui's CSS-variable theme. | src/targets/shadcn.js:27 |
primevueTarget | value | TargetAdapter primevue: emits sorb/primevue-preset, the first JS-emitting target format. | src/targets/primevue.js:35 |
muiTarget | value | TargetAdapter mui: emits sorb/mui-vars for MUI v6's CSS-variables mode. | src/targets/mui.js:29 |
angularMaterialTarget | value | TargetAdapter angular-material: emits sorb/mat-sys-vars for Angular Material 20's M3 system variables. | src/targets/angularMaterial.js:55 |
SorbProvider
const SorbProvider = ({ config, legacyMap, children })
SorbProvider — the React shell over sorbInit (./core.js, the framework-free injector; component-compat-roadmap P0). ALL runtime logic (connection resolution, committed/preview loading, mode-aware injection, SSE/poll, dark-mode state) now lives in sorbInit; this component's only job is to bridge that instance's pub-sub store into React state and expose the same TokenContext shape as before — non-breaking, byte- identical behavior to the pre-extraction implementation. sorbInit is created in the mount useEffect (not during render) so timing — and StrictMode double-invoke safety — matches the original implementation, which did all its DOM work in a mount-only effect too. The optional legacyMap (Legacy-React adapter, roadmap §6) is an ADDITIVE, non-destructive DOM overlay layered on top of the shell — it never touches sorbInit. When present, after tokens apply it remaps any element whose hardcoded literal matches a row's raw to var(--<cssVar>, <raw>), and restores the originals on unmount.
| Parameter | Type | Description |
|---|---|---|
props | { config: import('./types').SorbConfig, legacyMap?: import('./types').LegacyMapRow[], children: React.ReactNode, } |
sorbInit
function sorbInit(config)
Framework-free Sorb entry point. Resolves the connection, loads committed/preview tokens onto document.documentElement, and returns a small store (getState/subscribe) plus setMode/clearPreview. No React, no JSX — safe to call from a plain <script type="module">. Byte-identical DOM behavior to SorbProvider: same guard/vocab/mode-aware injection logic, just driven by a manual pub-sub store instead of React state.
| Parameter | Type | Description |
|---|---|---|
config | import('./types').SorbConfig |
Returns SorbInstance.
PreviewBanner
const PreviewBanner = ()
Drop-in banner that appears at the bottom of the screen for a Sorb preview. Renders in three states (see previewBannerModel): - blue "active" — a healthy live preview, - amber "mismatch" — preview active but likely re-skins nothing (B4), - red "error" — a deliberately-requested ?preview= couldn't be loaded (it may belong to a different project). The error state renders even though isPreview is false, since a failed preview falls back to committed tokens (spec jj-demo-rebind-and-diagnosis D2 — kill the silent-404). Safe to include unconditionally — renders nothing when there's no preview and no preview error.
// In your app root, after <SorbProvider>
<PreviewBanner />
useTokens
const useTokens = ()
Returns the full active token set (committed or preview).
Returns import('./types').TokenSet.
useToken
const useToken = (key)
Returns a single token value by key.
| Parameter | Type | Description |
|---|---|---|
key | string |
Returns string.
const primary = useToken('color-primary') // → '#3B5BDB'
useIsPreview
const useIsPreview = ()
Returns whether a preview token set is currently active. Useful for showing a preview indicator in your app.
Returns boolean.
usePreviewState
const usePreviewState = ()
Returns full preview state — useful for building a preview banner. previewMismatch is true when a preview loaded but its tokens don't match the app's preview.expectPrefixes (vocabulary mismatch — see B4); use it to render a warning state. Always false unless the guard is opted into. previewError is { id, outcome } (outcome: 'not_found'|'unauthorized'|'network') when a deliberately-requested ?preview= fetch failed and the SDK fell back to committed tokens — the case that used to be totally silent. null otherwise. A not_found typically means the preview id belongs to a different project than this app's key is bound to.
const { isPreview, previewId, previewMismatch, previewError, clearPreview } = usePreviewState()
useTheme
const useTheme = ()
Real-dark-mode (spec D3): the manual mode selection + the live-resolved scheme actually in effect. mode is meaningful for every app; setMode('light'|'dark') always works. It only visibly changes anything once the consumer's SorbConfig carries a darkTokens set (otherwise there's no dark stylesheet for the attribute toggle to select).
Returns { mode: 'auto'\|'light'\|'dark', setMode: (mode: 'auto'\|'light'\|'dark') => void, resolvedScheme: 'light'\|'dark': }
const { mode, setMode, resolvedScheme } = useTheme()
ThemeToggle
const ThemeToggle = ({ className } = {})
Drop-in Light / Dark / Auto mode toggle (real-dark-mode spec D3). Purely a thin useTheme() view — three buttons that call setMode, with the active one highlighted. Renders unconditionally (safe even in a single-mode app, where setMode still works but has nothing to visibly toggle since there's no injected dark stylesheet). Unstyled beyond minimal inline layout — bring your own CSS/className to match your app, same philosophy as PreviewBanner.
| Parameter | Type | Description |
|---|---|---|
[props] | { className?: string } |
// In your app root, alongside <PreviewBanner>
<ThemeToggle />
sanitizeCssValue
const sanitizeCssValue = (value)
Validate an untrusted CSS token value before it is injected via setProperty. Pure — does not touch the DOM. Rules (deny-by-default): - non-string / empty input is rejected. - reject ASCII control chars \x00-\x1f. - reject the context-break chars { } ;. - reject (case-insensitive, whitespace-tolerant) @import, javascript:, and the markup-break </. - extract every identifier( and reject if ANY is not in the allowlist (this is what stops url(, image-set(, expression(, paint(, …).
| Parameter | Type | Description |
|---|---|---|
value | unknown |
Returns { ok: boolean, value: string, reason?: string: }
verifyResolved
const verifyResolved = async (tokens, { origin = 'http://localhost:7777', key, fetch: fetchImpl } = {})
Read each token's resolved value off :root and ask the bridge whether the running app matches the committed resolved map. Precondition: call from inside a mounted <SorbProvider> — it applies the resolved token literals onto :root. Without it, custom props read back as var(...) refs (outputReferences css) and the result is { ok:false, reason:'provider-not-applied' } rather than a misleading mismatch.
| Parameter | Type | Description |
|---|---|---|
tokens | string[] | Token names or --cssVars to check (e.g. 'button-primary-bg-default'). |
[opts] | { origin?: string, key?: string, fetch?: typeof globalThis.fetch } | key is the hosted-bridge bearer key (config.preview.key). Omit for the no-auth localhost bridge — no Authorization header is then sent. |
Returns Promise<{ok:boolean, reason?:string, checked?:number, matched?:number, mismatches?:Array<{cssVar:string,expected:any,got:any: >, unknown?:string[], error?:string}>}
buildModeStylesheet
const buildModeStylesheet = (lightVars, darkVars, darkMode)
Builds the mode-aware CSS text carrying both a light and (optionally) a dark value-set for the same token ids — real-dark-mode spec D2/D3. Pure — no DOM. Returns a CSS string meant to be upserted into a <style id="sorb-tokens"> tag by injectModeStylesheet (apply.js). Contract (must stay byte-shape-stable — the demo/cloud emit agents match this exact shape): ```css :root { --a: 1; color-scheme: light; }
| Parameter | Type | Description |
|---|---|---|
lightVars | import('./types').TokenSet | Light-mode token map. Keys may be bare ('primary') or already ---prefixed ('--primary') — normalized here. |
darkVars | import('./types').TokenSet | null | undefined | Dark-mode token map, same key shape. null/undefined/{} ⇒ single-mode. |
darkMode | import('@sorb/core').DarkModeConvention | null | undefined | The active TargetAdapter's dark-mode convention (e.g. reactBootstrapTarget.darkMode). Undefined ⇒ single-mode. |
Returns string: CSS text, ready to inject verbatim.
injectModeStylesheet
const injectModeStylesheet = (css)
Upserts a <style id="sorb-tokens"> tag in <head> carrying mode-aware CSS (real-dark-mode spec D3) — the injection path used when a theme has both a light and a dark value-set (see buildModeStylesheet, ./modeStylesheet.js). DELIBERATELY SEPARATE from applyTokens/clearTokenOverrides (inline style.setProperty, above): those two stay untouched and are still what TokenProvider calls for a light-only theme, so a single-mode app's output is byte-identical to today (back-compat gate, spec §3 D3). This function is only reached when a theme actually has a dark mode — a <style> tag is required (not inline styles) because only a stylesheet can carry a @media (prefers-color-scheme: dark) block and attribute-selector rules; inline styles on documentElement can express neither. The css argument is expected to already be sanitized (buildModeStylesheet runs every value through sanitizeCssValue before it reaches here) — this function does no further validation, it only manages the tag's lifecycle.
| Parameter | Type | Description |
|---|---|---|
css | string | CSS text, e.g. from buildModeStylesheet(...). |
Returns void.
clearModeStylesheet
const clearModeStylesheet = ()
Removes the <style id="sorb-tokens"> tag injected by injectModeStylesheet, if present. Counterpart to clearTokenOverrides for the mode-aware (dual-mode) path.
Returns void.
tailwindDarkMode
const tailwindDarkMode
Tailwind's darkMode: 'class' convention — a .dark class toggled on documentElement (typically <html>). Tailwind has no canonical "light" class (light is just the absence of .dark), so lightSelector is omitted: a manual "light" choice cannot out-rank an OS dark preference under this convention (see modeAction.js's resolveModeAction) — a known limitation of class-only theming without a light marker.
applyLegacyMap
const applyLegacyMap = (root, legacyMap)
Walk root (and its descendants), and for every element whose computed value for a mapped property equals a raw in the legacyMap, override that element's INLINE style for that property to var(--<cssVar>, <raw>). Returns a handle that clearLegacyMap uses to restore the original inline values. Non-destructive: only inline element.style[prop] is touched, and the prior inline value (often empty) is captured so it can be restored exactly.
| Parameter | Type | Description |
|---|---|---|
[root=document.body] | Element|Document|null | subtree to remap |
legacyMap | LegacyMapRow[] | the report's auto rows |
Returns LegacyMapHandle.
clearLegacyMap
const clearLegacyMap = (handle)
Restore every inline style override recorded by applyLegacyMap, returning each element to its original (usually empty) inline value.
| Parameter | Type | Description |
|---|---|---|
handle | LegacyMapHandle|null |
Returns void.
computeLegacyOverride
const computeLegacyOverride = (prop, computedValue, legacyMap)
PURE decision logic. Given a property, the element's computed value for that property, and the legacyMap (array or pre-built index), return the override string var(--<cssVar>, <raw>) when the value matches a mapped raw, else null. This is the unit-tested core of the shim.
| Parameter | Type | Description |
|---|---|---|
prop | string | CSS property (camelCase or kebab-case) |
computedValue | string|number | the element's computed value for prop |
legacyMap | LegacyMapRow[]|Map<string, any[]> | rows, or an index from indexLegacyMap |
Returns string\|null.
indexLegacyMap
const indexLegacyMap = (legacyMap)
Index a legacyMap into a prop → [{ normValue, cssVar, raw }] lookup so the decision function is O(1)-per-prop instead of scanning the whole array.
| Parameter | Type | Description |
|---|---|---|
legacyMap | LegacyMapRow[] |
Returns Map<string, Array<{ normValue: string, cssVar: string, raw: string: >>}
normalizeProp
const normalizeProp = (prop)
Map a CSS property name (camelCase from JS style objects, or kebab-case from computed style) to a canonical kebab-case form for comparison.
| Parameter | Type | Description |
|---|---|---|
prop | string |
Returns string.
normalizeValue
const normalizeValue = (value)
Normalize a style value so an authored literal ("#0F65EF", "4", "4px") compares equal to its computed-style form. Trims, lowercases, collapses whitespace, canonicalizes colors to rgb()/rgba() (so hex raw matches the computed rgb()), and treats a bare unitless number as its px form (covers borderRadius: 4 → "4px").
| Parameter | Type | Description |
|---|---|---|
value | string|number |
Returns string.
Types
JSDoc typedefs. Import them in your own JSDoc with import('@sorb/leaf').Name.
TokenValue
type TokenValue = string | number
Source: src/types.js:4
TokenSet
A flat map of token name → value.
type TokenSet = Object.<string, TokenValue>
Source: src/types.js:8
PreviewConfig
| Property | Type | Description |
|---|---|---|
enabled | boolean | Whether to allow preview mode at all. Set to false in production builds. e.g. enabled: process.env.NODE_ENV !== 'production' |
origin (optional) | string | Where the local Sorb CLI is running. Defaults to http://localhost:7777. Only localhost/127.0.0.1/[::1] origins (any port) are trusted by default; any other origin must be listed in allowedOrigins or preview is blocked. |
allowedOrigins (optional) | string[] | Extra exact origins (e.g. a staging or hosted bridge) to trust in addition to localhost. Never enable preview against an untrusted origin in production. |
key (optional) | string | Bearer key for a hosted bridge (Sorb Cloud). When set, preview/verify requests send Authorization: Bearer <key>; when unset (localhost sorb dev) no header is sent. Use a read-only publishable sorb_pk_… key in anything distributable — supply it via env/config at deploy time, never hardcoded in source. |
pollInterval (optional) | number | How often to poll for token updates while a preview is active, in milliseconds. Defaults to 1500. |
expectPrefixes (optional) | string[] | Vocabulary/contract guard (B4). Token-key prefixes this app actually consumes (e.g. ['bs-']). When set, a loaded preview that applies tokens but matches NONE of these prefixes is flagged (previewMismatch context state) and a console.warn is emitted — catching the silent-no-op where the banner lights but nothing re-skins. Omit/empty ⇒ guard disabled (default). |
Source: src/types.js:13
ResolvedToken
A resolved token with full metadata, as produced by sorb-seed. The optional deprecated / replacedBy fields are only present when the DTCG source carries $deprecated: true / $extensions.sorb.replacedBy.
| Property | Type | Description |
|---|---|---|
id | string | |
cssVar | string | |
value | * | |
tier | string | |
type | string | |
deprecated (optional) | true | |
replacedBy (optional) | string |
Source: src/types.js:42
ResolvedConnection
The effective connection sorb-cloud resolved for an org/publishable key (E1 — hosted-bridge-modes, config-migration.md). See src/connection.js for the assumed GET <cloudBase>/api/orgs/resolve?key= contract — TODO, reconcile against the real sorb-cloud endpoint when it lands.
| Property | Type | Description |
|---|---|---|
bridgeMode | 'A'|'B'|'C'|string | The org's configured bridge mode. |
bridgeUrl | string | The bridge origin to preview against. |
orgId | string|null | Needed to build the SSE subscribe URL. |
tokenSource | string|null | |
previewPersistence | boolean|null | |
transport | 'sse'|'poll' | Which preview-update transport to use. |
Source: src/types.js:56
LegacyMapRow
A single row of the legacy-map shim — a subset of the engine's auto row from .sorb/adapt-report.json (roadmap §6). The full engine row also carries file/loc/tokenId/confidence/candidates/status; the runtime shim only consumes { raw, prop, cssVar }, so any auto row is a valid LegacyMapRow.
| Property | Type | Description |
|---|---|---|
raw | string | The original hardcoded value as authored, e.g. "#0F65EF" or "4px". Doubles as the var() fallback so removing the provider restores it exactly. |
prop | string | The CSS property the value applies to, e.g. "background" or "borderRadius" (camelCase or kebab-case both accepted). |
cssVar | string | The target token's custom-property name WITHOUT the leading --, e.g. "button-primary-bg-default". |
Source: src/types.js:70
LegacyMapHandle
Opaque handle returned by applyLegacyMap, passed to clearLegacyMap to restore the original inline styles. Internal shape may change.
| Property | Type | Description |
|---|---|---|
restores | Array<{ el: HTMLElement, prop: string, prev: string }> |
Source: src/types.js:88
SorbConfig
| Property | Type | Description |
|---|---|---|
namespace | string | Your app or design system namespace. |
tokens | TokenSet | Committed token set — bundled at build time. Always used in production. Used as fallback if preview fails. |
darkTokens (optional) | TokenSet | Committed DARK-mode token set (real-dark-mode spec D3) — same token ids as tokens, dark values. When present, SorbProvider injects a mode-aware <style id="sorb-tokens"> stylesheet (buildModeStylesheet) instead of the flat inline applyTokens path, and setMode/useTheme become meaningful. Omit for a single-mode (light-only) app — unchanged, byte-identical behavior to today. |
darkModeConvention (optional) | import('@sorb/core').DarkModeConvention | Override the dark-mode convention used to build the mode-aware stylesheet. Defaults to the react-bootstrap TargetAdapter's darkMode (data-bs-theme) — override only for a non-default target. |
resolved (optional) | ResolvedToken[] | Full resolved token array from sorb-seed output. When provided, SorbProvider will emit a dev-mode console.warn for any token flagged as deprecated. |
preview (optional) | PreviewConfig | Preview configuration. Omit or set enabled: false to disable entirely. An explicit preview.origin always wins over org-key resolution (below) — this is today's file-mode / Mode C path and is never overridden. |
orgKey (optional) | string | Org/publishable key (E1). Like an analytics SDK key: when set (and no explicit preview.origin is pinned), SorbProvider resolves bridge mode/url, token source, and preview persistence from sorb-cloud instead of requiring a local sorb.config.json. Purely additive — omit for today's file-mode behavior, unchanged. |
publishableKey (optional) | string | Alias for orgKey — either field name works; orgKey is checked first when both are set (see getOrgKey in connection.js). |
cloudBase (optional) | string | Override the sorb-cloud base URL used for org-key resolution. Defaults to connection.js's DEFAULT_CLOUD_BASE. Mainly for tests/staging. |
diagnostics (optional) | { allowedOrigins?: string[] } | Diagnosis channel (spec jj-demo-rebind-and-diagnosis D2). The leaf answers a { type:'sorb-ping' } postMessage with a sorb-hello fingerprint (namespace + key last4 + version + bridge origin + preview outcome) — but ONLY when the ping's event.origin is allowlisted. Baked defaults are Sorb Cloud's dashboard (https://app.sorbcloud.com + staging); set diagnostics.allowedOrigins to extend the allowlist for a self-hosted dashboard. The leaf never posts unsolicited and replies only to the exact pinging origin — see src/diagnostics.js. |
legacyMap (optional) | LegacyMapRow[] | Legacy-React adapter shim: the auto rows from .sorb/adapt-report.json. When present, after committed tokens are applied the provider remaps any element whose hardcoded computed style matches a row's raw to var(--<cssVar>, <raw>) — non-destructive, reversible on unmount. |
Source: src/types.js:95
TokenContextValue
| Property | Type | Description |
|---|---|---|
tokens | TokenSet | Currently active token set (committed or preview). |
isPreview | boolean | True when a preview token set is loaded. |
previewId | string | null | The active preview ID, or null. |
previewMismatch | boolean | True when the active preview applied tokens but none matched the app's preview.expectPrefixes (vocabulary mismatch — the app likely won't re-skin). Always false when the guard is not opted into. |
previewError | { id: string, outcome: 'not_found'|'unauthorized'|'network' }|null | Set when a deliberately-requested ?preview= fetch failed and the SDK silently fell back to committed tokens (the previously-invisible failure — spec jj-demo-rebind-and-diagnosis D2). outcome classifies the HTTP/network cause: not_found (404 — cross-tenant id or expired preview), unauthorized (401/403), network (unreachable/parse). null on the normal path. |
clearPreview | () => void | Clears the preview, removes the query param, loads committed tokens. |
mode | 'auto'|'light'|'dark' | The current MANUAL mode selection (real-dark-mode spec D3). 'auto' (default) defers to the OS prefers-color-scheme via the injected media query — no data-bs-theme attribute is set. 'light'/'dark' are a manual override that always wins (sets data-bs-theme). |
setMode | (mode: 'auto'|'light'|'dark') => void | Change the manual mode selection. |
resolvedScheme | 'light'|'dark' | The scheme actually in effect right now: mode itself when it's 'light'/'dark', otherwise the live-tracked OS prefers-color-scheme result while mode === 'auto'. |
Source: src/types.js:147
SorbState
type SorbState = { tokens: import('./types').TokenSet, isPreview: boolean, previewId: string|null, previewMismatch: boolean, previewError: { id: string, outcome: 'not_found'|'unauthorized'|'network' }|null, mode: 'auto'|'light'|'dark', resolvedScheme: 'light'|'dark', }
Source: src/core.js:99
SorbInstance
type SorbInstance = { getState: () => SorbState, subscribe: (listener: (state: SorbState) => void) => (() => void), setMode: (next: 'auto'|'light'|'dark') => void, clearPreview: () => void, destroy: () => void, }
Source: src/core.js:99
PreviewBannerModel
| Property | Type | Description |
|---|---|---|
visible | boolean | Render the banner at all? |
variant (optional) | 'active'|'mismatch'|'error' | |
id (optional) | string|null | The preview id to show in the chip. |
title (optional) | string | Bold headline. |
message (optional) | string | Secondary explanatory line. |
buttonLabel (optional) | string | Action-button label. |
background (optional) | string | Banner background (token-bindable var()). |
accent (optional) | string | Top-border accent colour. |
Source: src/previewBannerModel.js:11
Next
- React SDK — the task-shaped setup guide.
@sorb/core— the contract these adapters register into.- Troubleshooting — what each banner state means.
Works with Figma. Not affiliated with, or endorsed by, Figma. Figma is a trademark of Figma, Inc.