Skip to content

Themes ​

A theme is the brand: colours, typography, logo, font faces and free-form brand details. Templates reference tokens, so one saved template renders under any theme.

ts
import type { Theme } from "@broadpaper/core";

export const meridian: Theme = {
  id: "meridian",
  name: "Meridian Wealth",
  logo: "/brands/meridian.svg",
  logoDark: "/brands/meridian-white.svg",
  typography: { headingFont: "Inter, system-ui, sans-serif", bodyFont: "Inter, system-ui, sans-serif", baseFontSize: 11, headingWeight: 700 },
  colors: {
    primary: "#1F3A5F", secondary: "#4A6FA5", accent: "#E0B04B",
    text: "#1A1D21", mutedText: "#6B7280", surface: "#FFFFFF", background: "#F5F6F8", border: "#E2E5EA",
    success: "#1E8E5A", warning: "#C77D0A", danger: "#C0392B",
    chart: ["#1F3A5F", "#4A6FA5", "#E0B04B", "#7FB3D5"]
  },
  fonts: [
    { family: "Inter", src: ["https://cdn.example.com/fonts/Inter-Regular.woff2", "https://cdn.example.com/fonts/Inter-Regular.ttf"], weight: 400 },
    { family: "Inter", src: ["https://cdn.example.com/fonts/Inter-Bold.woff2", "https://cdn.example.com/fonts/Inter-Bold.ttf"], weight: 700 }
  ],
  radius: 8,
  table: { headerBackground: "#EEF2F7", headerColor: "#1F3A5F", zebraBackground: "#FAFBFC", borderColor: "#E2E5EA" },
  meta: { company: "Meridian Wealth Management Ltd", regulator: "Authorised and regulated by the FCA (FRN 123456)" }
};

Tokens ​

TokenResolves to
$colors.primary … $colors.<any>Theme colours, including custom names you add.
$typography.headingFont, $typography.bodyFont, $typography.monoFontFont stacks.
$table.headerBackground, $table.headerColor, $table.zebraBackground, $table.borderColorTable defaults.
$logo, $logoDarkLogo URLs. The Logo block picks the dark variant on dark backgrounds automatically.

In expressions, theme.meta.company and friends expose brand details for headers, footers and disclaimers. Missing values fall back to the built-in default theme and are reported as missing-theme-value warnings.

The cascade ​

Theme → document text styles → block defaults → instance overrides. New templates start with document styles that reference tokens (h1.color = $colors.primary), so they re-brand cleanly. Designers can still choose a literal colour for a block; hosts can forbid that with features.styleOverrides = false.

Fonts ​

theme.fonts is the one place a font is declared. From that declaration BroadPaper:

  • injects @font-face rules for the canvas, waits for them before measuring, and re-measures if they arrive late;
  • offers them in the designer's font pickers — the theme's heading, body and mono fonts and these families, and nothing else, since a font the theme does not supply cannot be embedded — and counts them as available when validating;
  • embeds them in the PDF — the browserless engine, the engine measuring the canvas and the render service all read the same list, as does the Chromium backend.

Include a TrueType (.ttf) or OpenType (.otf) file among each face's sources. A browser reads any format, but the browserless PDF engine reads only those two. List a WOFF2 first if you like — the browser takes it and the engine skips it for the .ttf beside it. A face with no TrueType or OpenType source is still drawn on screen, and the PDF draws that text in a standard face and says so in warnings.

A few rules follow from where the file is read:

  • One file per weight. weight: "400 700" on a variable font works on screen, but the engine is given the file at the first weight only.
  • Sources. A data: URI works everywhere. An https:// address is fetched once per process and needs CORS in a browser, as @font-face does. Any other string is a file path in Node and an address relative to the page in a browser — so a theme used in both needs an absolute address or a data: URI.
  • The render service reads a theme that arrives in a request more carefully: web addresses only on its allowedHosts, and no file paths. See the render service.

If your app loads fonts via <link> instead, the canvas still picks them up — the editor listens for font loading and re-lays out — but the PDF engine never sees a <link>, so declare them in the theme for the file to match.

Switching at runtime ​

The toolbar's theme menu lists themes; designer.setTheme(theme) switches programmatically. For rendering, pass any theme to renderPdf({ template, data, theme }).