API

Themes

Define a theme that controls colour, typography, spacing, and chart chrome across every doc, grid, plot, and embedded vis.

Often you want to create several different assets, styles and documents that all look similar. It's possible to do this hardcoded in code, but it's easier if this information can live in a separate structure and be re-used.

To facilitate this, novem offers themes which let you define your visual identity once and reuse it everywhere.

This brings three concrete wins:

  • Brand once, apply everywhere. Set --novem-accent to your corporate blue and every callout border, link, footnote ref, and chart accent follows. No per-vis configuration.
  • Plots inherit doc theming for free. A custom JavaScript plot embedded in a doc can read the parent's variables — both through CSS (var(--novem-text)) and through a JS object (render.theme.text). It behaves like a native part of the document.
  • Dark mode flips automatically. The engine keeps a parallel set of dark-mode defaults for every chrome variable. A theme that doesn't explicitly handle dark mode still looks correct in both modes.

Styles compose top-down through three layers. Each layer can override the previous one.

1. Skeleton CSS    ← structural layout + sane --novem-* defaults
                     (built into the engine, not user-editable)

2. Theme           ← brand: novem (default) | custom | +org/yourorg
                     overrides --novem-* values + selector refinements

3. Per-vis CSS     ← author overrides for one specific vis
                     same primitives, scoped to this document

You almost never need !important. Skeleton sets defaults; the theme refines them; per-vis CSS refines further. Specificity follows naturally from the cascade.

Every doc, grid, plot, or mail has a /config/theme value:

ValueBehaviour
novem (default)Built-in theme — Inter font, a tasteful brand palette, dark-mode aware.
customNo built-in theme is injected. Your custom.css is the entire theme surface. Use this when you want full control.
+org/yourorgOrg theme. Bundled CSS + assets (logos, fonts) authored once by your org admin and reused across every vis.

Set it via the CLI, the API, or the editor:

# CLI
novem -p my-plot --config theme custom

# API
curl -X POST -d "custom" \
  https://api.novem.io/v1/vis/plots/my-plot/config/theme

When theme = custom, the only style sources are the engine skeleton plus your custom.css. When theme = novem, the built-in theme sits between those two — it sets brand defaults that your custom CSS can still override.

These are the variables every theme overrides. Names are stable: changing their values reskins everything that consumes them. The full default values live in the registry at vislib/lib/theme/variables.ts.

Ten ordered colours for data series. Any chart that draws by category picks them up automatically.

VariableDefault
--novem-color-1#4e79a7
--novem-color-2#f28e2c
--novem-color-3#e15759
--novem-color-4#76b7b2
--novem-color-5#59a14f
--novem-color-6#edc949
--novem-color-7#af7aa1
--novem-color-8#ff9da7
--novem-color-9#9c755f
--novem-color-10#bab0ab

In a custom plot, the same palette is exposed as render.theme.colors (an array, indexed from 0).

VariableDefaultUsed by
--novem-primary#0d6efdPrimary brand colour
--novem-secondary#6c757dSecondary elements
--novem-accent#4e79a7Callout borders, links, accents
--novem-ok#198754Success callouts
--novem-warn#ffc107Warning callouts
--novem-bad#dc3545Danger callouts, error text
--novem-info#0dcaf0Info callouts
--novem-link#4e79a7Link colour, footnote refs

VariableDefaultUsed by
--novem-bg#ffffffPage / grid background
--novem-surface#f8f9faCallouts, inline code, preview blocks
--novem-border#dee2e6Page border, thematic breaks, grid borders
--novem-text#212529Primary text
--novem-text-secondary#6c757dBlockquote text, cover subtitle, footers
--novem-text-muted#adb5bdCover date, page numbers, embed loading

VariableDefault
--novem-font-body"Geist", -apple-system, ...
--novem-font-heading"Geist", -apple-system, ...
--novem-font-mono"Geist Mono", "SFMono-Regular", ...
--novem-font-size12px
--novem-font-size-sm10px
--novem-font-size-lg15px
--novem-line-height1.6
--novem-font-weight400
--novem-font-weight-heading600
--novem-font-size-h1h628 / 22 / 18 / 16 / 14 / 12 px

All four directions are independent.

VariableDefault (portrait)Default (landscape)
--novem-spacing8px8px
--novem-page-pad-top40px32px
--novem-page-pad-right60px50px
--novem-page-pad-bottom40px32px
--novem-page-pad-left60px50px

VariableDefault
--novem-radius4px
--novem-shadow0 2px 8px rgba(0,0,0,0.1)

VariableDefault
--novem-table-header-bg#1a1a2e
--novem-table-header-text#ffffff
--novem-table-stripe-bg#f8f9fa
--novem-table-border#e5e7eb
--novem-table-cell-pad10px 12px

These are hints for chart authors. The rendering engine doesn't consume them directly — they exist so themes can express chart preferences and custom plots can adopt them.

VariableDefaultPurpose
--novem-axis-color#212529Axis line + tick colour
--novem-axis-text#495057Axis label colour
--novem-axis-font-size11pxAxis label size
--novem-axis-width1pxAxis line thickness
--novem-axis-tick-size5pxTick mark length
--novem-gridline-color#e9ecefChart gridline colour
--novem-gridline-width1pxGridline thickness
--novem-gridline-dashnoneDash pattern
--novem-bar-radius0pxBar corner radius
--novem-pie-inner-radius00 = pie, 0.30.7 = donut
--novem-point-size4pxScatter dot radius
--novem-line-width2pxLine chart stroke width
--novem-line-curvelinearlinear, monotone, step
--novem-tooltip-bg#212529Tooltip background
--novem-tooltip-text#ffffffTooltip text
--novem-tooltip-radius4pxTooltip border radius
--novem-annotation-color#6c757dReference line colour
--novem-annotation-dash4 2Reference line dash

The webapp signals dark mode by setting data-dark-mode on the <html> element. The rendering engine keeps a parallel set of dark defaults covering only the chrome that needs to flip — surface, text, border, axis, grid, tooltip. Categorical palette, fonts, spacing, and shape tokens stay the same in both modes.

You don't have to do anything for dark mode to work. If you write a theme that doesn't define dark overrides at all, the engine's dark defaults fill in the gaps automatically.

If you want to refine dark mode further, override the same variables inside a [data-dark-mode] block:

.novem--doc--page {
  --novem-bg: #ffffff;
  --novem-text: #212529;
  --novem-accent: #003366;
}

&[data-dark-mode] .novem--doc--page {
  --novem-bg: #1a1a2e;
  --novem-text: #d0d4dc;
  --novem-accent: #6e90c8;
}

The skeleton CSS reads var(--novem-bg) etc., so every element that follows the variable adapts automatically — including embedded plots.

A small custom.css that rebrands a doc and every plot embedded inside it. Set /config/theme to custom first, then push this CSS:

.novem--doc--page {
  /* Brand tokens (private to this CSS) */
  --brand-navy: #0c2340;
  --brand-gold: #c5a55a;
  --brand-light: #f4f1eb;

  /* Re-route engine primitives → brand tokens */
  --novem-font-body:    "Inter", "Segoe UI", system-ui, sans-serif;
  --novem-font-heading: "Inter", "Segoe UI", system-ui, sans-serif;
  --novem-accent:       var(--brand-navy);
  --novem-link:         var(--brand-navy);
  --novem-surface:      var(--brand-light);
  --novem-table-header-bg: var(--brand-navy);

  /* Categorical chart palette → brand sequence */
  --novem-color-1: var(--brand-navy);
  --novem-color-2: var(--brand-gold);
  --novem-color-3: #6e8aab;
  --novem-color-4: #a5895a;
  --novem-color-5: #2c4a6e;
}

/* Selector refinement on top of the variable layer */
.novem--doc--heading--2 {
  border-bottom: 2px solid var(--brand-gold);
  padding-bottom: 6px;
}

/* Dark-mode overrides — same primitives, different values */
&[data-dark-mode] .novem--doc--page {
  --brand-navy: #8ba4d4;
  --brand-gold: #d4b86a;
  --brand-light: #1a1a2e;
}

Anything embedded in this doc — plots, grids, tables, callouts — inherits the navy/gold palette. Tables use navy headers. Dark mode flips the brand tokens, which cascades into every consumer.

A custom plot runs in a sandboxed iframe. The parent document's --novem-* values are propagated into the iframe two ways:

Variables are injected into the iframe's :root before your stylesheet runs. Use them directly:

body {
  font-family: var(--novem-font-body);
  font-size:   var(--novem-font-size);
  color:       var(--novem-text);
  background:  var(--novem-bg);
}

.tooltip {
  background:    var(--novem-tooltip-bg);
  color:         var(--novem-tooltip-text);
  border-radius: var(--novem-tooltip-radius);
}

Every variable is also exposed on render.theme as a camelCase value — useful when you build SVG or canvas content from JS. Categorical colours are an array.

const t = render.theme;

const svg = d3.select(node).append("svg")
  .attr("width", width)
  .attr("height", height)
  .style("font-family", t.fontBody)
  .style("background", "transparent");

const color = d3.scaleOrdinal(t.colors);  // 10 categorical entries

svg.append("text")
  .attr("fill", t.text)
  .attr("font-weight", t.fontWeightHeading)
  .text("My chart");

const arc = d3.arc()
  .innerRadius(parseFloat(t.pieInnerRadius) * r)
  .outerRadius(r);

render.theme is dark-mode awaret.text already reflects whichever mode is active. You don't need to branch on render.dark to pick a foreground colour. (The boolean is still there if you need it for finer distinctions.)

For organisations, an admin can publish a reusable theme that any vis authored under the org can opt into:

novem -p my-plot --config theme +acme/corporate

An org theme bundles custom.css, fonts, and image assets together — authored once, applied everywhere. Per-vis custom.css still layers on top, so authors can fine-tune individual visualisations without forking the brand.

See the Themes overview (you're reading it) for the variable contract; org-theme authoring is documented separately as the feature ships.

Use the variables, not hardcoded values. A chart that hardcodes stroke: "#212529" looks invisible in dark mode. Reach for var(--novem-axis-color) (in CSS) or render.theme.axisColor (in JS) — the engine flips them for you.

Test in dark mode. The webapp toggles dark mode with the moon icon in the top-right. A theme that only handles light mode is half a theme.

Don't abuse !important. The cascade gives you all the layering you need: skeleton → theme → per-vis. If you find yourself reaching for !important, you're usually fighting the cascade in the wrong direction.

Brand tokens vs primitives. It's idiomatic to keep your brand colours as private variables (--brand-navy) and re-route --novem-* to point at them. That way the brand has a single source of truth, and primitives keep their semantic meaning.

Variables propagate into iframes. Anything you set on .novem--doc--page shows up in embedded plots automatically. You don't need to repeat your theme inside each plot.