Themes
Define a theme that controls colour, typography, spacing, and chart chrome across every doc, grid, plot, and embedded vis.
Novem is currently in closed alpha. This documentation is incomplete and subject to change.
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-accentto 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:
| Value | Behaviour |
|---|---|
novem (default) | Built-in theme — Inter font, a tasteful brand palette, dark-mode aware. |
custom | No built-in theme is injected. Your custom.css is the entire theme surface. Use this when you want full control. |
+org/yourorg | Org 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.
| Variable | Default |
|---|---|
--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).
| Variable | Default | Used by |
|---|---|---|
--novem-primary | #0d6efd | Primary brand colour |
--novem-secondary | #6c757d | Secondary elements |
--novem-accent | #4e79a7 | Callout borders, links, accents |
--novem-ok | #198754 | Success callouts |
--novem-warn | #ffc107 | Warning callouts |
--novem-bad | #dc3545 | Danger callouts, error text |
--novem-info | #0dcaf0 | Info callouts |
--novem-link | #4e79a7 | Link colour, footnote refs |
| Variable | Default | Used by |
|---|---|---|
--novem-bg | #ffffff | Page / grid background |
--novem-surface | #f8f9fa | Callouts, inline code, preview blocks |
--novem-border | #dee2e6 | Page border, thematic breaks, grid borders |
--novem-text | #212529 | Primary text |
--novem-text-secondary | #6c757d | Blockquote text, cover subtitle, footers |
--novem-text-muted | #adb5bd | Cover date, page numbers, embed loading |
| Variable | Default |
|---|---|
--novem-font-body | "Geist", -apple-system, ... |
--novem-font-heading | "Geist", -apple-system, ... |
--novem-font-mono | "Geist Mono", "SFMono-Regular", ... |
--novem-font-size | 12px |
--novem-font-size-sm | 10px |
--novem-font-size-lg | 15px |
--novem-line-height | 1.6 |
--novem-font-weight | 400 |
--novem-font-weight-heading | 600 |
--novem-font-size-h1 … h6 | 28 / 22 / 18 / 16 / 14 / 12 px |
All four directions are independent.
| Variable | Default (portrait) | Default (landscape) |
|---|---|---|
--novem-spacing | 8px | 8px |
--novem-page-pad-top | 40px | 32px |
--novem-page-pad-right | 60px | 50px |
--novem-page-pad-bottom | 40px | 32px |
--novem-page-pad-left | 60px | 50px |
| Variable | Default |
|---|---|
--novem-radius | 4px |
--novem-shadow | 0 2px 8px rgba(0,0,0,0.1) |
| Variable | Default |
|---|---|
--novem-table-header-bg | #1a1a2e |
--novem-table-header-text | #ffffff |
--novem-table-stripe-bg | #f8f9fa |
--novem-table-border | #e5e7eb |
--novem-table-cell-pad | 10px 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.
| Variable | Default | Purpose |
|---|---|---|
--novem-axis-color | #212529 | Axis line + tick colour |
--novem-axis-text | #495057 | Axis label colour |
--novem-axis-font-size | 11px | Axis label size |
--novem-axis-width | 1px | Axis line thickness |
--novem-axis-tick-size | 5px | Tick mark length |
--novem-gridline-color | #e9ecef | Chart gridline colour |
--novem-gridline-width | 1px | Gridline thickness |
--novem-gridline-dash | none | Dash pattern |
--novem-bar-radius | 0px | Bar corner radius |
--novem-pie-inner-radius | 0 | 0 = pie, 0.3–0.7 = donut |
--novem-point-size | 4px | Scatter dot radius |
--novem-line-width | 2px | Line chart stroke width |
--novem-line-curve | linear | linear, monotone, step |
--novem-tooltip-bg | #212529 | Tooltip background |
--novem-tooltip-text | #ffffff | Tooltip text |
--novem-tooltip-radius | 4px | Tooltip border radius |
--novem-annotation-color | #6c757d | Reference line colour |
--novem-annotation-dash | 4 2 | Reference 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 aware — t.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.