---
audience: developers
summary: One file controls typography, palette, page geometry, components, and chart
  colours across all formats.
title: Themes
path: concepts/themes
status: published
---


# Themes

A **theme** is one JSON file that says how a tenant's documents look. It carries fonts, colours, typography scales, page geometry, component styles (callouts, tables), and a chart palette. ScaiScribe compiles each theme into format-specific artefacts — a Word `styles.xml`, a PPTX masters config, an XLSX styles block, an ECharts theme JSON — and uses those compiled forms when rendering.

A tenant pins their documents to a theme by name. Change the theme; re-render; the document picks up the new look without any spec change.

## Theme structure

```json
{
  "theme_id": "office",
  "version": "1.0.0",
  "extends": null,
  "fonts": {
    "heading": { "family": "Aptos Display", "weight": 700 },
    "body":    { "family": "Aptos",         "weight": 400 },
    "mono":    { "family": "Cascadia Mono", "weight": 400 },
    "fallbacks": ["Calibri", "Segoe UI", "Helvetica Neue", "Helvetica", "Arial", "sans-serif"]
  },
  "colors": {
    "primary":    "#2B579A",
    "secondary":  "#44546A",
    "accent":     "#5B9BD5",
    "background": "#FFFFFF",
    "text":       "#000000",
    "muted":      "#7F7F7F",
    "palette":    ["#2B579A", "#5B9BD5", "#70AD47", "#FFC000", "#ED7D31", "#A5A5A5"]
  },
  "typography": {
    "h1": { "size_pt": 28, "weight": 700, "color": "primary",   "spacing_after_pt": 12 },
    "h2": { "size_pt": 22, "weight": 700, "color": "secondary", "spacing_after_pt": 10 },
    "h3": { "size_pt": 18, "weight": 700, "color": "text",      "spacing_after_pt": 8 },
    "body":    { "size_pt": 11, "line_height": 1.45 },
    "code":    { "font": "mono", "size_pt": 10, "background": "#F2F2F2" }
  },
  "components": {
    "callouts": [
      { "id": "info", "left_bar": "primary",  "fill": "#DEEBF7" },
      { "id": "warn", "left_bar": "#D97706",  "fill": "#FFF7ED" },
      { "id": "note", "left_bar": "muted",    "fill": "#F2F2F2" }
    ],
    "chart_palette": ["#2B579A", "#5B9BD5", "#70AD47", "#FFC000", "#ED7D31", "#A5A5A5"]
  },
  "page": {
    "size": "A4",
    "margins_mm": { "top": 25, "right": 20, "bottom": 25, "left": 20 }
  },
  "pptx": {
    "size": "16:9",
    "typography": {
      "slide_title":    { "size_pt": 36 },
      "slide_subtitle": { "size_pt": 22 },
      "bullet_body":    { "size_pt": 18 }
    },
    "masters": [
      { "id": "title",   "zones": { "title": { "x": 0.5, "y": 2.2, "w": 12.33, "h": 1.2 },
                                    "subtitle": { "x": 0.5, "y": 3.5, "w": 12.33, "h": 0.8 } } },
      { "id": "content", "zones": { "title":    { "x": 0.5, "y": 0.3, "w": 12.33, "h": 0.9 },
                                    "subtitle": { "x": 0.5, "y": 1.2, "w": 12.33, "h": 0.5 },
                                    "body":     { "x": 0.5, "y": 1.9, "w": 12.33, "h": 5.1 } } }
    ]
  },
  "xlsx": {
    "header_row": { "fill": "primary", "font_color": "#FFFFFF", "bold": true },
    "alternating_row": { "fill": "#F2F2F2" },
    "totals_row": { "top_border": "primary", "bold": true },
    "number_formats": {
      "currency_eur": "€#,##0.00",
      "percent":      "0.0%"
    }
  }
}
```

## Format-specific compilation

A single theme produces five compiled artefacts. Each renderer consumes the one it needs:

| Compiled form | Consumed by | Notes |
|---|---|---|
| `docx_styles` (XML fragment) | render-docx.js | Injected via dolanmiu/docx's `externalStyles`. Carries Normal, Heading1–4, CalloutInfo/Warn/Note, CodeBlock, PageBreak. |
| `pptx_master` (JSON config) | render-pptx.js | Drives `pres.defineSlideMaster` calls + slide colours/fonts + per-master layout zones (title/subtitle/body rectangles). |
| `xlsx_styles` (JSON config) | render-xlsx.js | Carries the `header_row`/`alternating_row`/`totals_row` cell blocks + the `number_formats` catalogue. |
| `html_css` (CSS bytes) | render-html-preview.js | Wraps the mammoth-emitted HTML for DOCX preview. |
| `echarts` (JSON theme) | render-chart-image.js | Theme palette for the SVG chart fallback. |

Compilation is cached by content hash. The same theme, compiled twice, hits the cache the second time — so theme changes propagate but don't re-cost the CPU.

## Inheritance via `extends`

A theme can inherit from a parent:

```json
{
  "theme_id": "acme_variant",
  "extends": "office",
  "colors": { "primary": "#1F4ED8" }
}
```

The child overrides what it specifies; everything else is inherited from the parent. v1.0 supports single-level extension (no chains). Override semantics are per-property: setting `colors.primary` doesn't blow away the rest of `colors`; it merges in.

## Slide layout zones

PPTX masters declare per-layout `zones` — rectangles for title, subtitle, and body content. The renderer uses these to place slide elements: title and subtitle land in their fixed slots, and body-shaped elements (`paragraph`, `bullet_list`, `image`, `chart`) stack sequentially inside the body zone with heights proportional to estimated content weight. This is what stops the classic "paragraph plus bullet list on the same slide render on top of each other" problem.

Themes that omit `zones` fall back to the renderer's defaults (sized for a 13.33″ × 7.5″ LAYOUT_WIDE canvas).

## Tenant fonts

Themes can reference fonts by family name. If the tenant has uploaded the matching face via `POST /v1/fonts`, the renderer picks it up automatically — font upload extracts family/weight/style via FontTools and indexes the binary for the workers. If no tenant-uploaded face matches, the renderer falls back to the theme's `fonts.fallbacks` array.

For PDF specifically, embedded fonts get baked into the output via Gotenberg + LibreOffice.

## How themes apply by format

- **DOCX**: theme styles become Word styles (`Heading1`, `CalloutInfo`, `CodeBlock`). Authors writing `{"type":"heading","level":1,...}` get `Heading1` automatically.
- **PPTX**: each `layout` in a slide references a master id from `pptx.masters[]`. Render-pptx calls `pres.addSlide({masterName: layout})` and lays elements out using the master's `zones`.
- **XLSX**: cells with `style_id` get the matching `xlsx.{header_row,alternating_row,totals_row}` block applied. Numbers with `number_format` use the theme's format string.

## Built-in themes

| Theme | Use case |
|---|---|
| `office` | **Default.** Aptos with Calibri fallback and a neutral palette — the same look Microsoft Office 2024+ ships with. Right for almost every request; the theme every caller gets when no `theme` is passed. |
| `minimal` | Helvetica, monochrome. Good baseline for white-label resellers. |
| `acme_variant` | Example of `extends` — inherits from `office`, overrides primary colour. Intended as a sample for tenants building their own brand theme. |

## Tenant-owned themes

Brand themes live on tenants, not in the builtin bundle. A tenant registers its theme via the admin path and gets a scoped `theme_id` addressable in any spec created under that tenant. Cross-tenant theme references are refused — a caller asking for a brand theme that isn't theirs gets `404 theme not found`.

`scribe_list_themes` returns the visible set for the caller's tenant (builtins plus their own tenant themes, each stamped `scope: "builtin"` or `scope: "tenant"`).

## Where to next

- [Templates](/docs/scaiscribe/concepts/templates) — themes vs templates (themes are styling, templates are pre-filled binary skeletons).
- [Spec format](/docs/scaiscribe/concepts/specs) — how a spec references a theme.
- [Charts](/docs/scaiscribe/concepts/charts) — chart_palette colours flow into native + image rendering.
