Skip to content
D
Documentation

Themes — functions

reference
6 min readUpdated

Functions

Import these from @grafloria/renderer.

Functions

assertThemeContrast

Throw with a readable diff when a theme does not conform. Use in tests/CI.

ts
function assertThemeContrast(theme: Theme, textLevel: number = WCAG.AA_TEXT): void

auditThemeContrast

Every contrast pair in a theme that a reader depends on.

textLevel defaults to AA (4.5:1). Pass WCAG.AAA_TEXT to hold a theme to AAA — which is what the high-contrast themes are built and tested against.

ts
function auditThemeContrast(theme: Theme, textLevel: number = WCAG.AA_TEXT): ContrastReport

clearStyles

Drop every named style (tests, and hosts tearing a document down).

ts
function clearStyles(): void

contrastRatio

Contrast ratio between two colours, 1…21. undefined when either colour is not one we can parse (see {@link parseColor}).

ts
function contrastRatio(a: string, b: string): number | undefined

cssVarName

--grafloria-node-fill — the custom property a token maps to.

ts
function cssVarName(token: ThemeToken): string

defineStyle

Define (or redefine) a named style. Definitions are copied, so later mutation of the caller's object has no effect.

ts
function defineStyle(name: string, style: NamedStyle): void

defineStyles

Define several named styles at once.

ts
function defineStyles(styles: Record<string, NamedStyle>): void

deriveTheme

Auto-derive a theme from another one, then PROVE it conforms.

The generation step is deliberately dumb (flip lightness for 'dark', keep the palette for 'high-contrast'); the value is in the second step, which walks the SAME audit the caller can run and repairs every enforceable failure with ensureContrast. The result is asserted before it is returned, so a derived theme that could not be repaired is a loud error, never a subtly unreadable diagram.

Derived alongside the surfaces: the STATE colours (selected / highlighted / hovered / disabled / error), the SECONDARY text, and the semantic category palette — i.e. everything a theme swap has to move together for the diagram to stay coherent.

ts
function deriveTheme(options: DeriveThemeOptions): Theme

ensureContrast

Push foreground away from background until it clears minimum.

Walks in whichever direction the background is NOT (a light background darkens the foreground, a dark one lightens it) in fixed steps, and takes the first step that passes. Black/white are the endpoints, so on any real background this terminates with a passing colour — that is why the derived themes can be ASSERTED to conform rather than eyeballed.

Returns the input untouched when it already passes, or when either colour is unparseable (nothing sensible to do — the audit will report it).

ts
function ensureContrast(foreground: string, background: string, minimum: number): string

generateBaseStyleSheet

The shared, theme-INDEPENDENT stylesheet. Identical for every renderer on the page, so it is injected once and deduped by element id.

ts
function generateBaseStyleSheet(): string

generateContrastPreferenceBlock

prefers-contrast: more, in CSS.

The ColorModeController swaps in a whole high-contrast THEME when the host gave it one. This block is what a host that did NOT still gets: thicker strokes. Weight is the half of contrast that colour cannot supply — a 1px hairline is not an accessible border however dark it is — and it is the one thing we can safely strengthen without knowing anything about the host's palette.

ts
function generateContrastPreferenceBlock(instanceId: string): string

generateForcedColorsBlock

The @media (forced-colors: active) variable override for one instance.

Emitted for every renderer, unconditionally: it costs one inert media block and it is the FLOOR — it protects hosts that never supplied a high-contrast theme and never touched colorMode. (The ColorModeController's high-contrast upgrade is the ceiling: a better-looking, fully-checked theme when one exists.)

forced-color-adjust: none is deliberately NOT set: we are opting IN to the user's colours, not out of them.

ts
function generateForcedColorsBlock(instanceId: string): string

generateInstanceOverrideCSS

Everything that must sit AFTER the theme's variable block, in cascade order. One string, one <style> element, one guaranteed ordering.

ts
function generateInstanceOverrideCSS(
  bridge: TokenBridge | undefined,
  instanceId: string
): string

generateInstanceVarBlock

One renderer's variable block: the ONLY place a theme's values are written. Scoped to that renderer's root, so a second diagram with a different theme cannot be repainted by it.

ts
function generateInstanceVarBlock(theme: Theme, instanceId: string): string

generateTokenBridgeBlock

The bridge's CSS block for one instance, or '' when there is nothing to map.

ts
function generateTokenBridgeBlock(bridge: TokenBridge | undefined, instanceId: string): string

getStyle

The raw definition, or undefined when the name was never defined.

DIAGRAM-FIRST, then process-global — see ext/registry-scope.ts. A diagram that defined its own critical sees its own; one that did not still sees the app-wide definition.

ts
function getStyle(name: string): NamedStyle | undefined

getStyleRegistryVersion

Bumped on every mutation — renderers key cache invalidation off this.

ts
function getStyleRegistryVersion(): number

hasStyle

ts
function hasStyle(name: string): boolean

hslToRgb

ts
function hslToRgb({ h, s, l }: Hsl): Rgb

instanceScopeSelector

[data-grafloria-instance="grafloria-3"] — selects one diagram's root (and scoped hosts).

ts
function instanceScopeSelector(instanceId: string): string

isThemeRef

True when a style value is a theme reference rather than a literal.

ts
function isThemeRef(value: unknown): value is ThemeRef

lightnessOf

HSL lightness of a colour, 0-1. Undefined for unparseable input.

ts
function lightnessOf(hex: string): number | undefined

linkTypeKey

A link's "type" for theme.links[type]: an explicit type metadata key when the host sets one, else the path type (smooth / orthogonal / …), which is the structural analogue of React Flow's edge.type.

ts
function linkTypeKey(link: LinkModel): string

listStyles

ts
function listStyles(): string[]

meetsContrast

Does this pair clear a threshold? Unparseable colours are NOT a pass.

ts
function meetsContrast(a: string, b: string, minimum: number): boolean

muiBridge

MUI (Material UI) with CSS-variable theming enabled (extendTheme / CssVarsProvider, which publishes --mui-palette-*).

ts
function muiBridge(prefix = '--mui'): TokenBridge

onStyleRegistryChange

Subscribe to registry mutations. Returns the unsubscribe function.

ts
function onStyleRegistryChange(listener: () => void): () => void

parseColor

Parse the colour forms a theme can actually hold: #rgb, #rgba, #rrggbb, #rrggbbaa, rgb()/rgba().

undefined for anything else — a CSS system colour (CanvasText), a var(--x), a gradient spec. Callers must SKIP those rather than pretend a ratio: a contrast claim about a value we cannot see is worse than no claim.

ts
function parseColor(value: string): Rgb | undefined

readColorPreferences

Read the three OS preferences right now.

ts
function readColorPreferences(): ColorPreferences

relativeLuminance

WCAG relative luminance (0 = black, 1 = white).

ts
function relativeLuminance(color: Rgb): number

removeStyle

ts
function removeStyle(name: string): boolean

resolveBindableVars

Every BINDABLE token of a theme, as { '--grafloria-colors-primary': '#2563eb', … }.

This is the second half of the instance variable block (the first being the 33 chrome vars). Together they are the complete set of values a themeRef can point at — which is exactly what makes a theme swap expressible as "rewrite this instance's variables" instead of "rebuild every VNode".

ts
function resolveBindableVars(theme: Theme): Record<string, string>

resolveLinkStyle

Resolve a link's effective style. ONE ordered spread — see the header.

ts
function resolveLinkStyle(
  link: LinkModel,
  theme: Theme,
  options: CascadeOptions = {}
): Partial<LinkStyle>

resolveNodeSelectionLook

How this node shows it is selected, resolved through the cascade: its own style, a named style, its type's theme defaults, the theme-wide default — else 'both'. The renderer reads it for the ring and the CSS state class.

ts
function resolveNodeSelectionLook(node: NodeModel, theme: Theme): NodeSelectionLook

resolveNodeStyle

Resolve a node's effective style. ONE ordered spread — see the header.

ts
function resolveNodeStyle(
  node: NodeModel,
  theme: Theme,
  options: CascadeOptions = {}
): Partial<NodeStyle>

resolveStyleClasses

Merge a styleClass list into ONE partial style. Names are applied left-to-right (later names win); unknown names are ignored.

ts
function resolveStyleClasses<T extends NamedStyle>(styleClass: string | undefined): Partial<T>

resolveThemeFromPrefs

THE resolution rule, as one pure function.

  1. colorMode picks the light/dark AXIS ('system' → the OS's answer).
  2. An explicit contrast preference (prefers-contrast: more, or a forced- colors mode) then upgrades that axis to the high-contrast theme, when the caller supplied one. It is an UPGRADE, not a replacement: a user in dark mode who wants more contrast gets high-contrast DARK, not a light flash.

Note forced-colors ALSO gets a pure-CSS treatment (the variable block emits a system-colour override under @media (forced-colors: active)), because the OS palette must win even for a host that never passed a high-contrast theme. The two are complementary: this picks the best THEME we have; that guarantees the floor.

ts
function resolveThemeFromPrefs(
  mode: ColorMode,
  prefs: ColorPreferences,
  themes: ThemeSet
): Theme

resolveThemeRef

Resolve a token against a theme. undefined when the theme does not define it — callers must treat that as "this property was never set" (the cascade layer below it, or the stylesheet, then wins) rather than painting undefined.

ts
function resolveThemeRef(token: string, theme: Theme): string | number | undefined

resolveThemeVars

The whole theme as { '--grafloria-node-fill': '#ffffff', … }. Also the natural shape for design-token export / Canvas-side lookups.

ts
function resolveThemeVars(theme: Theme): Record<string, string>

rgbToHsl

ts
function rgbToHsl({ r, g, b }: Rgb): Hsl

shadcnBridge

ts
function shadcnBridge(options: { space?: 'hsl' | 'oklch' | 'raw' } = {}): TokenBridge

tailwindBridge

Tailwind v4, whose theme IS a set of CSS variables (--color-slate-200, …). scale picks the neutral ramp so a host can stay on its own greys.

ts
function tailwindBridge(options: { scale?: string; accent?: string } = {}): TokenBridge

themeRef

Bind a property to a theme token.

node.setStyle({ fill: themeRef('category.critical') })

The any return is what lets a ThemeRef sit in a fill?: string | Gradient | … slot: the engine's style types are the PUBLIC model contract and stay free of any renderer type, so the renderer — the only thing that ever resolves a ref — owns the marker and detects it structurally with {@link isThemeRef}.

ts
function themeRef(token: ThemeToken | string): any

themeRefCssValue

How a bound property is emitted in CSS mode when the target property accepts var() (i.e. it is written into an inline CSS style string): var(--grafloria-category-critical, #b91c1c).

The literal FALLBACK matters: a theme that simply does not define the token would otherwise make the declaration invalid at computed-value time and the element would lose the property entirely (an SVG shape with no fill paints black). With the fallback it degrades to the value the token had when the VNode was built, which is the closest thing to "unchanged" available.

ts
function themeRefCssValue(token: string, theme: Theme): string | undefined

themeRefToken

The token a reference points at.

ts
function themeRefToken(ref: ThemeRef): string

themeRefVar

node.selected.fill → --grafloria-node-selected-fill.

Mechanical, not a lookup: the same rule reproduces every entry of THEME_VARS (pinned by a test), so chrome tokens and caller tokens share ONE naming law and nothing has to be registered ahead of time.

ts
function themeRefVar(token: string): string

themeVar

var(--grafloria-node-fill) — the reference the shared stylesheet is written in.

ts
function themeVar(token: ThemeToken): string

themeVarValue

Serialize one token's value for CSS (adds the unit for numeric bindings).

ts
function themeVarValue(token: ThemeToken, theme: Theme): string

toHex

{r:255,g:0,b:0} → #ff0000.

ts
function toHex({ r, g, b }: Rgb): string

withLightness

Same hue and saturation, new lightness. The primitive the dark-flip is built on.

ts
function withLightness(hex: string, lightness: number): string

Was this page helpful?

Themes — functions — Grafloria