Theming

Map every visible region of a plot to the theme() key that styles it.

A Gribouille plot is a stack of named regions: an outer canvas, an inner panel (or grid of panels), a strip band for facets, a legend column, a title block on top, a caption underneath, and axes around the edges. Each region is one entry in the theme() dictionary, set with an element-text, element-line, element-rect, element-blank, or margin value. This page is a map: it shows where each region sits on a real plot, names the theme() key that controls it, and links worked examples for the common overrides.

Anatomy of a plot

The diagram below is a real plot. Each region carries a deliberate fill so you can see where it sits, and a callout names the theme() key that styles it. Compose your own overrides by replacing any of those keys.

A faceted scatter plot annotated with callouts naming every styled region: plot-background around the outside, plot-title and plot-subtitle on top, plot-caption at the bottom, panel-background and panel-grid inside each facet, strip-background and strip-text on the facet header band, axis-line, axis-ticks, axis-text, and axis-title on each side, and legend-background, legend-title, and legend-text on the right.

A faceted scatter plot annotated with callouts naming every styled region: plot-background around the outside, plot-title and plot-subtitle on top, plot-caption at the bottom, panel-background and panel-grid inside each facet, strip-background and strip-text on the facet header band, axis-line, axis-ticks, axis-text, and axis-title on each side, and legend-background, legend-title, and legend-text on the right.

Figure 1: Anatomy of a Gribouille plot: each region keyed to its theme() entry, with numbered markers tied to a left/right legend table.

Region to theme key reference

The table below names every region the renderer styles and the theme() key that controls it. Each axis family cascades in three steps: per-side (-x-bottom, -x-top, -y-left, -y-right) falls back to per-axis (-x, -y), which falls back to the unprefixed family. Setting axis-text alone styles all four sides at once. The panel grid cascades by weight then axis: per-axis (-x, -y) falls back to the weight (panel-grid-major, panel-grid-minor), which falls back to panel-grid. Minor gridlines default to half the major weight in the same colour, and a blank panel-grid blanks both weights.

Region Key(s) Element type
Outer plot rectangle. plot-background. element-rect.
Breathing room around content (painted rect grows outward to wrap it). inset on plot-background and legend-background. Ignored on panel-background, strip-background, legend-bar. margin.
Outer whitespace reservation. outset on plot-background, panel-background, legend-background, legend-bar. Ignored on strip-background. margin.
Title, subtitle, caption. plot-title, plot-subtitle, plot-caption. element-text.
Drawing area inside a facet. panel-background. element-rect.
Gridlines inside the panel, by weight and axis. panel-grid, panel-grid-major, panel-grid-minor, panel-grid-major-x, panel-grid-major-y, panel-grid-minor-x, panel-grid-minor-y. element-line.
Facet header band. strip-background, strip-text. element-rect, element-text.
Axis line, per side. axis-line, axis-line-x, axis-line-y, axis-line-x-bottom, axis-line-x-top, axis-line-y-left, axis-line-y-right. element-line.
Axis ticks, per side. axis-ticks, axis-ticks-x, axis-ticks-y, axis-ticks-x-bottom, axis-ticks-x-top, axis-ticks-y-left, axis-ticks-y-right. element-line.
Tick mark length, per side. tick-length, tick-length-x, tick-length-y, tick-length-x-bottom, tick-length-x-top, tick-length-y-left, tick-length-y-right. Typst length.
Tick mark labels visible. tick-labels. Boolean.
Axis tick labels, per side. axis-text, axis-text-x, axis-text-y, axis-text-x-bottom, axis-text-x-top, axis-text-y-left, axis-text-y-right. element-text.
Axis title, per side. axis-title, axis-title-x, axis-title-y, axis-title-x-bottom, axis-title-x-top, axis-title-y-left, axis-title-y-right. element-text.
Discrete legend box. legend-background, legend-title, legend-text. element-rect, element-text.
Continuous colour bar (chrome reuses legend-background / legend-title / legend-text). legend-bar, legend-ticks. element-rect, element-line.
Shared cascade parents (fields inherit down to every descendant surface unset on the child). text, line, rect. Element constructors.
Base colour roles (drive geom colour resolution and theme mixing). ink, paper, accent. Colour.
Layer aesthetic defaults. geom. element-geom.

Any key can be hidden with element-blank in place of its normal element. That is how theme-minimal drops the axis line, the panel background, and the tick marks, for example.

Inheritance cascade

Every styled surface walks a parent chain root-to-leaf at render time. The most specific record wins; unset fields inherit from the parent. Two patterns repeat: base records (text, line, rect) feed every descendant surface, and each axis family fans out by axis then by side.

Text sizes accept either an absolute length or a ratio. A ratio such as 80% scales the parent surface size, so setting the base text size rescales every surface that inherits via a ratio (axis titles, tick labels, legend text, and so on). An absolute length such as 12pt pins that surface outright and ignores the cascade.

Three columns of trees. Each base record (text, line, rect) sits on the left of its column, with edges fanning out to every descendant surface stacked vertically on the right: text feeds axis-text, axis-title, legend-text, legend-title, strip-text, plot-title, plot-subtitle, plot-caption; line feeds panel-grid, axis-line, axis-ticks, legend-ticks; rect feeds panel-background, plot-background, strip-background, legend-background, legend-bar.

Three columns of trees. Each base record (text, line, rect) sits on the left of its column, with edges fanning out to every descendant surface stacked vertically on the right: text feeds axis-text, axis-title, legend-text, legend-title, strip-text, plot-title, plot-subtitle, plot-caption; line feeds panel-grid, axis-line, axis-ticks, legend-ticks; rect feeds panel-background, plot-background, strip-background, legend-background, legend-bar.

Figure 2: Base records feed every descendant. Setting text, line, or rect cascades to every surface in its column; fields unset on the child inherit from the parent.

Tree diagram of the axis family cascade. A single root node labelled <family> splits into <family>-x and <family>-y, each of which then splits into two per-side leaves: <family>-x-bottom and <family>-x-top under the x branch, <family>-y-left and <family>-y-right under the y branch.

Tree diagram of the axis family cascade. A single root node labelled <family> splits into <family>-x and <family>-y, each of which then splits into two per-side leaves: <family>-x-bottom and <family>-x-top under the x branch, <family>-y-left and <family>-y-right under the y branch.

Figure 3: Axis family cascade (one template, four families). Replace <family> with axis-line, axis-ticks, axis-text, or axis-title. Per-side leaves cascade up to per-axis, then to the family root. tick-length follows the same three-step cascade with a Typst length value.

Override one region

The lightest possible change is to pass a single record into theme() for the region you care about. The example below lifts the axis-title and axis-text sizes, paints the panel-background cream, and softens the panel-grid. Every other surface keeps its default.

#let df = range(0, 12).map(i => (x: i, y: i * i * 0.1))

#plot(
  data: df,
  mapping: aes(x: "x", y: "y"),
  layers: (
    geom-line(stroke: 1.2pt, colour: rgb("#d6604d")),
    geom-point(size: 3pt, fill: rgb("#d6604d")),
  ),
  theme: theme(
    axis-title: element-text(size: 12pt),
    axis-text: element-text(size: 10pt),
    panel-background: element-rect(fill: rgb("#f7f0e7")),
    panel-grid: element-line(colour: rgb("#d9cfbf")),
  ),
  labels: labels(
    title: "theme() overrides",
    subtitle: "Larger axis titles, a cream panel fill, and a soft grid",
    x: "Step",
    y: "y = 0.1 × x²",
  ),
  width: 12cm,
  height: 7cm,
)

Scatter plot with cream panel background, soft gridlines, and enlarged axis titles.

Scatter plot with cream panel background, soft gridlines, and enlarged axis titles.

Each line maps directly to one row in the region table: change a single key, the matching region updates, nothing else moves.

Tune the outer canvas

Grow the outer plot rectangle past its content with element-rect’s inset field (inner padding); reserve whitespace around the rectangle with the outset field (outer margin). Each takes a margin with any subset of top, right, bottom, left; sides left at auto add nothing.

#let accent = rgb("#1f77b4")
#let d = range(0, 10).map(i => (x: i, y: i * 0.5))

#let panel(title, theme-arg) = plot(
  data: d,
  mapping: aes(x: "x", y: "y"),
  layers: (
    geom-line(stroke: 1pt, colour: accent),
    geom-point(size: 2pt, fill: accent),
  ),
  labels: labels(title: title, x: "X", y: "Y"),
  theme: theme-arg,
  width: 12cm,
  height: 6cm,
)

#grid(
  columns: 1,
  row-gutter: 0.4cm,
  panel("Default plot-background", theme-minimal()),
  panel(
    "plot-background: element-rect(inset: margin(top: 0.6cm, right: 0.6cm, bottom: 0.9cm, left: 1.6cm))",
    theme-minimal(plot-background: element-rect(
      fill: rgb("#f7f0e7"),
      inset: margin(
        top: 0.6cm,
        right: 0.6cm,
        bottom: 0.9cm,
        left: 1.6cm,
      ),
    )),
  ),
)

Two stacked scatter plots; the lower one has a tinted plot-background grown via element-rect inset on every side.

Two stacked scatter plots; the lower one has a tinted plot-background grown via element-rect inset on every side.

inset grows the painted rectangle outward to wrap its content; only plot-background and legend-background honour it. outset reserves outer whitespace: on panel-background, legend-background, and legend-bar the reservation widens the surrounding cetz chrome slot (shrinking the panel canvas); on plot-background it wraps the rendered block in pad(). strip-background ignores both fields: the facet band has no surrounding slot to grow or reserve into. Facet column and row gutters are a renderer constant, not a theme field. The gap between tick labels and the axis title is controlled by axis-title.margin; gaps around any other text surface use the per-element margin field on its element-text.

Complete vs partial themes

theme() is the full base: every surface listed in the reference table is populated. Use it when you want to start from scratch and define each region yourself.

The library ships eight curated overrides on top of that base. Each one is a thin function that returns a theme with a handful of fields replaced; everything else cascades from theme().

Function Effect
theme-minimal. Drops the axis line, the panel background, and the tick marks. The library default.
theme-bw. Black-and-white frame, white panel, light grid.
theme-classic. Axis lines only, no grid, no panel background.
theme-grey. Grey panel background with a white grid.
theme-light. Light frame and grid on a white panel.
theme-dark. Dark frame and grid on a dark panel.
theme-linedraw. Black frame, dark grid, white panel.
theme-void. Everything removable removed; useful for inset thumbnails.

The theme-sub-* helpers go one level finer: they bundle the keys that move together for a single region. For example, theme-sub-axis accepts line, ticks, text, title, and rewrites the four corresponding theme() keys for you. The full set is theme-sub-axis, theme-sub-axis-x, theme-sub-axis-y, theme-sub-axis-bottom, theme-sub-axis-top, theme-sub-axis-left, theme-sub-axis-right, theme-sub-legend, theme-sub-panel, theme-sub-plot, and theme-sub-strip.

Finally, theme-set installs a theme as the document-wide default so subsequent plot() calls pick it up without an explicit theme: argument.

Where to go next

Back to top