Skip to content

Theme schema

Precise reference for both theme file formats: the DOCX ThemeConfig (validated against the generated theme.schema.json) and the PPTX ThemeConfig (usually supplied inline on props.theme). For the conceptual guide — tokens, built-ins, cascades — see Themes & styling.

Shared visual layers

These optional roots use the same schema in DOCX and PPTX. DOCX also accepts them in props.themeOverrides, deep-merging objects and replacing arrays. PPTX accepts them in an inline props.theme or a registered theme. jto://themes/values preserves all these values; jto://themes retains its list of names.

Themes specify appearance. They never require content, an archetype, action titles, sources, trackers, footers or density. Blueprints and quality profiles own those expectations. A coordinate-authored document can use every theme without adding chrome.

palette

Optional scalar keys: rule, textMuted, onPrimary, surfaceInverse, positive, negative. Values are six-digit hex (optional #) or references to legacy colors or these palette keys. Palette keys take precedence over same-named legacy colors; unknown targets and cycles fail generation.

chart is an ordered array of 1–12 such colors. Native charts and Highcharts use it when the author provides no chart colors. Without it, the existing six-token chart palette applies. Quality checks recognize these additional colors.

PPTX component color enums are unchanged: reference new roles inside themes, or use their resolved hex in component props. DOCX component colors can reference new palette names directly.

typography

roles accepts eyebrow, display, stat, quote, label, footer, tableHeader, tableCell, chartLabel, tracker, source. Only declared roles are materialized; role names alone do not restyle existing components.

Role fieldMeaning
faceheading, body, mono, light; defaults to body. PPTX missing mono falls back to body, missing light to heading.
weightInteger 100–900. Overrides bold; numeric weights use the existing synthetic-family mechanism.
sizeExplicit 5–200 points; wins over the canvas scale.
lineHeightLine-height multiple, 0.5–3.
trackingHundredths of an em, negative for condensed tracking.
casenone, upper, smallCaps.
colorHex or theme token.
spaceBefore, spaceAfterNonnegative points.

Use themeStyle: "display" on a DOCX paragraph or style: "display" on PPTX text/shape. Native styles.<role> fields override projected role fields, and explicit component formatting wins. DOCX font.case and native theme styles accept the same case values; PPTX native text styles also accept case and paraSpaceBefore.

DOCX writes native caps/small-caps formatting and preserves text. PPTX writes uppercase output text; small caps are emulated by uppercasing lowercase spans at 80% of the run size. Authored JSON remains unchanged. This is not OpenType small-cap glyph selection.

scale.<canvas> takes required base (5–200 pt), optional ratio (1–2, default 1.25) and baselinePt (>0–24, default 4). Derived size is base × ratio^step, snapped to the nearest multiple of baselinePt and clamped to 5–200 pt. A step-0 role keeps base exactly, unsnapped. Steps: display 4, stat 3, quote 1, tableHeader/tableCell 0, eyebrow/label/chartLabel/tracker −1, footer/source −2. Without a canvas scale, roles use the existing body/default size.

Canvas keys are a4, letter, wide169, standard43. DOCX uses theme.page.size: LETTER selects letter; A4, A3, LEGAL and custom dimensions use a4. PPTX selects the closest of 16:9 and 4:3 from document dimensions (default 10 × 7.5 inches); ties use standard43. There is no authored canvas identifier.

spacing

canvas.<canvas> accepts nonnegative safeAreaIn and gutterIn, and integer columns/rows (1–100). DOCX maps safe area to four page margins and gutter to the page gutter, converting inches to twips; section/page overrides still apply. PPTX uses them as grid defaults; the document grid and a group’s gridConfig win. Absolute PPTX coordinates remain absolute.

basePt (>0) and blockGap.tight/normal/loose (≥0) are spacing values in points. DOCX normal-style spacing falls back to blockGap.normal, then basePt, when native normal-style spacing is absent. Its normal size similarly falls back to the selected scale base. Tight/loose gaps and DOCX grid rows/columns are reserved for semantic block consumers; they do not insert blocks or columns. PPTX block gaps are values for those later consumers, not automatic coordinate rearrangement.

chrome and motif

Visual recipes the JSON blocks of both formats paint from. No content is inserted by a theme: a recipe says how a block looks once an author places it.

chrome optionally contains runningHead, tracker, actionTitle, keyTakeaways, sourceLine, confidentialFooter, logoSlot, cover. Each recipe accepts type (a role name), color, fill, rule: { weightPt, color }, padPt and alignment (left/center/right). Weights/padding are nonnegative points. A recipe reads nothing on its own: a JSON block binds the values it wants with $theme pointers, so a theme swap restyles every block that binds them. A rule.weightPt of 0 draws no rule.

motif is a single object with required kind (none/rule/corner/band), optional color, weightPt and placement (top, bottom, left, right, topLeft, topRight, bottomLeft, bottomRight). kind: "none" resolves to no motif at all, so a composition can ask { "$if": { "$theme": "/motif" } } and draw nothing. Neither schema accepts content requirements.

How a recipe layers over a type role

A composition names the role it paints in and lets the recipe override it, with a $theme pointer chain — the first pointer that resolves wins:

json
{
  "themeStyle": { "$theme": "/chrome/sourceLine/type", "default": "source" },
  "font": {
    "color": {
      "$theme": ["/chrome/sourceLine/color", "/styles/source/color"],
      "default": "textMuted"
    }
  }
}

So the recipe wins over the role, the role wins over the literal the composition declares, and a theme that states neither still renders. themeStyle brings the role's face, case, tracking, weight and paragraph spacing with it — which is why a block paragraph states no font.family. A themeStyle naming a style the theme does not define is ignored rather than written into the file.

Retargeting type moves face, case, tracking and weight; size stays with the composition's own role, because a recipe carries no size and a pointer cannot dereference type.

What each recipe paints

Rows are the shipped compositions in client-report-blocks.docx.json, technical-report-blocks.docx.json and consulting-deck-blocks.pptx.json. is a field the format's compositions do not draw, with the reason.

RecipeDOCXPPTX
runningHeadtype/color head the chain for the header text, ahead of tracker; rule is the hairline under it; alignment is the header paragraph's— no page chrome in a deck
trackertype/color for the header text when the running head states nonetype/color/alignment for the slide tracker on every deck block
actionTitlecolor for the section-opener headingtype/color for the title of action-chart, kpi-row, two-column and statement
keyTakeawaystype/color for the label, rule for the rules that bound it, padPt for the gap they keep from the contenttype/color for the takeaway label, rule for its accent rule in action-chart and statement
sourceLinetype/color for the source line and a data table's notes, rule for the hairline above ittype/color for the source line on every deck block
confidentialFootertype/color/alignment for the footer line, rule for the hairline above ittype/color for the slide footer; alignment — a slide footer is placed by frame, not by paragraph alignment
logoSlotalignment for the cover logo— a slide logo is placed by frame
covertype/color for the cover title, rule for the rule above ittype/color for the cover title, rule for the cover mark
motifkind/color/weightPt as the mark at the top edge of the coverthe same mark on the cover slide

fill is the one field no shipped composition draws: none of the report or deck blocks paints a filled surface behind text. rule is drawn only where a composition has a rule — tracker, actionTitle and logoSlot have none, and no bundled theme sets one there. placement is not read: each composition decides where its own mark goes.

DOCX theme

Both formats also accept the five optional shared visual layers. Existing themes need none of them.

A DOCX theme is a JSON object (conventionally a *.docx.theme.json file). Unknown top-level properties are rejected (additionalProperties: false).

FieldTypeRequiredDescription
$schemastringnoSchema URI for editor tooling. The validator injects ./json-schemas/theme.schema.json when missing.
namestringyesTheme identifier — the value documents reference in props.theme.
displayNamestringyesHuman-readable name.
descriptionstringyesShort description of the theme.
versionstringyesTheme version string (built-ins use "2.0.0").
colorsobjectyesThe 13-key color scheme (below).
fontsobjectyesExactly four font slots (below).
pageobjectyesPage size and margins (below).
stylesobjectnoNamed paragraph-style presets, compiled into real Word styles (below).
componentDefaultsobjectnoPer-component prop defaults (below).
noProofWordsstring[]noHouse-style allowlist: whole-word, case-insensitive terms never flagged by Word's spellchecker. The document's own noProofWords merges on top at render.

colors

13 required keys:

primary, secondary, accent, text, background, border, textPrimary, textSecondary, textMuted, borderPrimary, borderSecondary, backgroundPrimary, backgroundSecondary

3 optional keysaccent4, accent5, accent6 — named to match the PPTX palette so both formats share one chart-series vocabulary. They exist for the chart palette, though a theme that defines one makes it referenceable from any component color prop like any other token. minimal and consulting define them; devportal and vermilion do not — see Charts for what a theme that omits them produces.

No other key is accepted (additionalProperties: false).

Each value matches the pattern ^(#[0-9A-Fa-f]{6}|[a-zA-Z][a-zA-Z0-9]*)$ — either a #RRGGBB hex value or the name of another theme color. Name references are resolved recursively at render time (so "border": "backgroundSecondary" is valid); unknown names throw during generation. Because the second alternative is a name, a digit-leading bare hex (00FF00) is not expressible: it matches neither alternative and fails validation. A letter-leading one (AABBCC) passes the pattern, and resolveColor falls back to reading it as hex once no token by that name is found — no theme color name is six hex characters, so the two never collide. Always write the # regardless: it is unambiguous and works for both leading digits and letters.

fonts

Exactly four required slots — heading, body, mono, light — each a font definition:

FieldTypeRequiredDescription
familystringyesFont family name (see Fonts for safe families and registration).
sizenumber (8–72)noSize in points.
colorstringnoHex or theme color token.
boldbooleannoEquivalent to fontWeight: 700.
fontWeightinteger (100–900)noNumeric weight; wins over bold when both are set.
italicbooleanno
underlinebooleanno
lineSpacingobjectno{ "type": "single" | "atLeast" | "exactly" | "double" | "multiple", "value"?: number }
spacingobjectno{ "before"?: number, "after"?: number } (points, ≥ 0).
characterSpacingobjectno{ "type": "condensed" | "expanded", "value": number }

page

FieldTypeRequiredDescription
sizestring or objectyesOne of "A4", "A3", "LETTER", "LEGAL", or a custom { "width": number, "height": number } in twips.
marginsobjectyesAll seven keys required, numbers ≥ 0, in twips: top, bottom, left, right, header, footer, gutter.

Twips

Word measures in twips: 1 twip = 1/20 pt, so 1440 twips = 1 inch and 720 = 0.5 inch.

styles

Known keys: normal, heading1heading6, title, subtitle (regular style properties) and TOC1TOC6 (TOC style properties). Arbitrary custom style names are also allowed and can be referenced from paragraphs via themeStyle.

Regular style properties = all the text-formatting fields from the font definition table above, plus:

FieldTypeDescription
font"heading" | "body" | "mono" | "light"Reference into theme.fonts.
alignment"left" | "center" | "right" | "justify"Paragraph alignment.
prioritynumberOrdering in Word's style gallery.
baseStylestringName of a style to inherit from (chains are resolved).
followingStylestringStyle applied to the next paragraph when the user presses Enter in Word.
widowControlbooleanPrevent widow/orphan lines.
keepNextbooleanKeep with the following paragraph.
keepLinesTogetherbooleanKeep all lines on one page.
outlineLevelnumberOutline level for navigation/TOC collection.
bordersobjectPer-side (top/bottom/left/right) plus between, each { "style", "size", "color", "space"? }. size is in eighths of a point; style is one of the 27 Word border styles (e.g. single, double, dashed, dotted, thick, ...). between is the rule Word draws between consecutive paragraphs sharing this style, in place of their adjoining bottom and top edges.
indentobjectParagraph indentation.

TOC styles (TOC1TOC6) deliberately exclude baseStyle and add one field:

FieldTypeDescription
tabStopsarrayEach entry: { "type", "position", "leader" }. position is in twips or the string "max" (= 9026, the right page edge); leader is "dot" | "hyphen" | "middleDot" | "none" | "underscore". Default: [{ "type": "right", "position": "max", "leader": "none" }] — the classic right-aligned page number.

componentDefaults

Optional per-component prop defaults. Allowed keys: heading, paragraph, image, statistic, table, section, columns, list. Values are partial props objects for the matching component — see DOCX components. Document-level props.componentDefaults deep-merges on top; props on the component itself win over both.

Complete minimal example

A valid theme with all required fields plus a small styles and componentDefaults block (colors and margins from the built-in minimal theme):

json
{
  "$schema": "./json-schemas/theme.schema.json",
  "name": "brand",
  "displayName": "Brand Theme",
  "description": "House style for generated reports",
  "version": "2.0.0",
  "colors": {
    "primary": "#000000",
    "secondary": "#666666",
    "accent": "#2c3e50",
    "background": "#ffffff",
    "text": "#2c2c2c",
    "border": "#f0f0f0",
    "textPrimary": "#000000",
    "textSecondary": "#4a4a4a",
    "textMuted": "#999999",
    "borderPrimary": "#e0e0e0",
    "borderSecondary": "#f5f5f5",
    "backgroundPrimary": "#ffffff",
    "backgroundSecondary": "#fafafa"
  },
  "fonts": {
    "heading": { "family": "Arial", "size": 24 },
    "body": { "family": "Arial", "size": 11 },
    "mono": { "family": "Menlo", "size": 10 },
    "light": { "family": "Arial", "size": 24 }
  },
  "page": {
    "size": "A4",
    "margins": {
      "top": 1440,
      "bottom": 1440,
      "left": 1080,
      "right": 1080,
      "header": 720,
      "footer": 720,
      "gutter": 0
    }
  },
  "styles": {
    "normal": {
      "font": "body",
      "lineSpacing": { "type": "multiple", "value": 1.5 },
      "alignment": "justify",
      "spacing": { "after": 9 }
    },
    "heading1": {
      "font": "light",
      "size": 24,
      "color": "primary",
      "spacing": { "before": 18, "after": 12 },
      "keepNext": true
    }
  },
  "componentDefaults": {
    "table": { "hideBorders": true, "cellDefaults": { "padding": 4 } },
    "list": { "format": "bullet" }
  }
}

Runtime defaults

When a theme is loaded programmatically, missing optional pieces are filled from built-in defaults (normal at 11 pt, heading1 24 pt bold down to heading6 11 pt bold, LETTER page with 720-twip margins, and a default color/font set). A theme file must still pass the schema — the defaults matter mostly for partial ThemeConfig objects created in code.

PPTX theme

Also accepts optional displayName, description, version and the shared visual layers. fonts.mono and fonts.light are optional family strings for type roles.

A PPTX theme is a plain object, most often supplied inline on the presentation's props.theme (see Themes & styling). Unknown top-level properties are rejected (additionalProperties: false). jto pptx schemas emits a PPTX theme.schema.json generated from this ThemeConfig schema — the format parent determines which theme schema is generated (see JSON schemas).

FieldTypeRequiredDescription
namestringyesTheme name.
colorsobjectyesThe 10-slot scheme (below).
fontsobjectyes{ "heading": string, "body": string } — plain family-name strings, no size/weight here.
defaultsobjectyes{ "fontSize": number, "fontColor": string } — base font size in points and base color (hex).
stylesobjectnoPartial map of the seven style names (below).
componentDefaultsobjectnoPer-component prop defaults (below).

colors

SlotRequired
primary, secondary, accent, background, textyes
text2, background2, accent4, accent5, accent6no

Values are strict hex matching ^#?[0-9A-Fa-f]{6}$ (leading # optional). Unlike DOCX, palette values cannot reference other color names — token indirection happens only where components consume colors. An unset optional slot resolves to primary at render time, with a THEME_COLOR_FALLBACK warning, whenever something names the token explicitly; the implicit chart palette skips it instead (below).

accent4accent6 carry the same names in the DOCX scheme so the chart palette reads one token list in both formats, and both formats skip an unfilled slot identically — the palette shrinks, no warning. Only the PPTX built-ins ship values for them. See Charts.

styles

A partial map — override any subset of: title, subtitle, heading1, heading2, heading3, body, caption. Each value is a text style:

FieldTypeDescription
fontSizenumberPoints.
fontFacestringFont family name.
fontColorstringHex, a semantic token (primary, text2, ...), or a PowerPoint alias (accent1, tx1, ...).
boldboolean
fontWeightinteger (100–900)Wins over bold when both are set.
italicboolean
align"left" | "center" | "right" | "justify"
lineSpacingnumber
charSpacingnumberPoints; positive = wider.
paraSpaceAfternumberSpace after paragraph, points.

componentDefaults

Allowed keys: text, image, shape, table, highcharts, chart (extra keys are tolerated, as in the DOCX equivalent). See PPTX components and PPTX charts for the props each accepts.

Example

json
{
  "name": "brand",
  "colors": {
    "primary": "#0C2340",
    "secondary": "#1B3A5C",
    "accent": "#D4A843",
    "background": "#FFFFFF",
    "text": "#1A1A1A",
    "text2": "#5A6B7C",
    "background2": "#F3F5F7",
    "accent4": "#8A9BAA",
    "accent5": "#C4A35A",
    "accent6": "#2E4A66"
  },
  "fonts": { "heading": "Georgia", "body": "Calibri" },
  "defaults": { "fontSize": 18, "fontColor": "#1A1A1A" },
  "styles": {
    "title": {
      "fontSize": 40,
      "bold": true,
      "fontColor": "primary",
      "align": "left"
    },
    "caption": { "fontSize": 10, "italic": true, "fontColor": "text2" }
  },
  "componentDefaults": {
    "table": { "fontSize": 12 }
  }
}

Slide grid

The grid used to position slide content (default 12 columns × 6 rows) is configured separately from the theme — see Slides & grid.

Loading rules

DOCX theme files loaded from disk (CLI --theme-path, or loadThemeFromFile in the library) pass through a hardened loader:

  • .json extension only — anything else is rejected. (--theme-path alternatively accepts a JS/TS module that exports the theme as default or theme; that path bypasses the JSON loader.)
  • 10 MB maximum file size.
  • Path guards — rejects .. traversal segments, null bytes, and paths longer than 1000 characters.
  • Empty files are rejected.
  • Content is validated against the theme schema (TypeBox), then runtime defaults are applied.

PPTX theme files given to the CLI are read as plain JSON — without the hardened loader or schema validation.

Loaded custom themes are registered under their sanitized theme.name, which is what the document's props.theme must reference. (The --theme flag currently takes effect only in plugin-loaded runs — see the CLI reference.)

Validate a theme file without generating anything:

bash
jto docx validate ./brand.docx.theme.json

The validator auto-detects theme files (an object with colors/fonts/styles and no name: "docx" root is treated as a theme). See Validation.

Released under the MIT License.