The reference for every component a placement can carry. The component's name
is the key inside a place: entry — there is no body: wrapper and no type: field.
Arrive here from the component index in docs/authoring.md,
which owns everything around these: the two-document structure, slide fields, place:
and at:, and the chart block. Error messages are in docs/errors.md.
Registering a component of your own is
Adding a component the spec cannot express.
Every field table below is exhaustive. A component refuses any key it does not
read, naming the ones it does — so a misspelled colums: fails the build rather than
rendering one column and saying nothing.
A figure says what it shows. image, icon, document and chart take alt:,
the words a screen reader reads in the figure's place, written into the file as the
shape's alternative text. A figure that carries no meaning takes decorative: true
instead, which writes Office's decorative flag so a screen reader skips it; giving both is
an error. qa's alt-text check warns about a picture or chart that
has neither. The build marks two kinds of picture decorative itself: a slide's
background: image or theme art, and a card's picture icon when the card has a heading
or body and no alt: of its own.
bullets— a column of bulleted linescallouts— a mark beside a heading and a line of copystats— a row of big-number tilestable— a real, editable PowerPoint tablepanel— a filled block behind other contentellipse— a disc: a badge, a dot, a step numbercard— a rounded plate with a heading, a line of copy and an iconflow— a run of steps, joined in orderconnector— a line joining two placementsequation— one display equationrule— a dividernav— the deck's sections, with the one you are in markedicon— a vector markimage— a photograph, and any text reversed out of itdocument— a real markdown file, renderedfanout— one call and the work it sets offversus— two magnitudes either side of a glyphdiverge— signed bars either side of a centre rulechart— a real, editable PowerPoint chart
Every field table reads the same way: a field marked yes under Required has no default and its absence is an error; everything else may be left out, and the Default column is what you get when you do.
| Field | Required | Default | What it does |
|---|---|---|---|
items |
yes | — | The bullet strings. Non-empty list. |
columns |
no | 1 |
Split the items across this many columns, filling left to right. Capped at the number of items. |
heading |
no | — | A heading above the first column, in the head rung, coloured with the theme's accent-1. |
section: Spec
title: What ships today
subtitle: One vocabulary, every component
animate: one_at_a_time
place:
- at: {cols: full}
bullets:
heading: Components
columns: 2
items:
- Bulleted columns
- Dot-and-text callouts
- Big-number stat tiles
- Rendered markdown documents
- Native OOXML charts
- Chrome-only slidesOne reveal group per bullet when the list is one column, so one_at_a_time stages it a line per click; one group per column when it has several. The heading arrives with the first bullet.
Each item is measured wrapped to its own column, less the indent its bullet takes, with the space after every item counted. A list whose tallest column would run past the placement is refused rather than drawn, naming the bullets and the lines they wrap to — so a long item costs as many lines of depth as it sets on, and a second column narrows every item in it.
| Field | Required | Default | What it does |
|---|---|---|---|
items |
yes | — | Non-empty list of mappings. |
heading |
no | — | A label above the rows, in the head rung — the same band bullets reserves, so the two line up in a split:. |
items[].head |
yes | — | The bold heading for that row. |
items[].body |
no | — | One line of supporting copy under it. |
items[].icon |
no | — | A glyph name, drawn in place of the accent dot. A list may mix marked and unmarked rows; the text indents to clear whichever mark it sits beside. |
section: Spec
title: Why the format changed
animate: together
place:
- at: {cols: full}
callouts:
items:
- {head: One row per datapoint, body: A row carries its own label and its own numbers.}
- {head: Nothing positional, body: "No index to count, no parallel arrays to zip."}
- {head: One word for one thing, body: "A chart has a kind; a placement names its component."}
- {head: Strict by default, body: "An unknown field is an error, never a silent drop."}The mark and its text reveal together, so a click-build never leaves a dot on screen with nothing beside it.
One reveal group per row. Keep to about six rows — past that the rows are thinner than the type needs and the build fails saying so.
| Field | Required | Default | What it does |
|---|---|---|---|
items |
yes | — | Non-empty list of mappings. |
items[].value |
yes | — | The big number. Quote it — "4", not 4. |
items[].label |
no | — | Small caption inside the tile. |
items[].icon |
no | — | A glyph name, drawn above the number. Every tile in the row grows to fit it, so the row stays level. |
columns |
no | one per item | Tiles per row. Hard maximum 4; a larger number is silently reduced. |
caption |
no | — | An italic line under the tiles. |
section: Spec
title: The shape of it
place:
- at: {cols: full}
stats:
columns: 4
caption: The spec says what goes where; the library owns every position.
items:
- {value: "21", label: components}
- {value: "29", label: chart kinds}
- {value: "10", label: type rungs}
- {value: "0", label: colours in the spec}One reveal group per tile; the caption joins the last tile's group.
A native OOXML table: selectable text, editable in PowerPoint, and every cell recorded in the manifest so QA's contrast, minimum-size and overflow checks measure the cells rather than one grey rectangle.
| Field | Required | Default | What it does |
|---|---|---|---|
rows |
yes | — | Non-empty list of rows; each row a non-empty list of cells. Quote numbers — "12", not 12. |
header |
no | — | A list of column labels, set bold on its own band above the rows. Fixes the column count. |
total |
no | — | A final row set bold and ruled above itself at twice the weight — the summary line under a column of figures. |
align |
no | the placement's align |
One of left/center/right per column — so a price column can be right-aligned while its labels are not. |
valign |
no | middle |
top, middle or bottom for every cell in the table. Reach for top when one column carries prose and the rest carry labels. |
widths |
no | an even split | Relative column widths. [3, 2, 2, 1] and [0.375, 0.25, 0.25, 0.125] give the same table. |
head_pair |
no | surface |
The palette pair the header band is painted in; its ink is that pair's foreground. |
body_pair |
no | — | A pair for the body cells. Left out, they are unfilled and the slide's own surface shows through — which is what makes one declaration read on page and on inverse. |
banding |
no | false |
Fill every other body row. The alternate colour is the body's own surface nudged toward its ink, so it works on a light table and a dark one. |
rules |
no | rows |
What the hairlines mark — see the table below. |
color |
no | line |
A palette role for the rules. |
weight |
no | 1.0 |
Rule weight as a multiple of the theme's line weight. A hairline is about 0.3. |
density |
no | 1.0 |
Multiplies the padding inside every cell. 0.6 is dense, 1.4 airy. It is a multiplier and not a measurement because nothing else in a spec is one. |
A cell is a string, or a mapping when a string cannot say enough. Any cell in
rows:, header: or total: may be written as a mapping instead:
| Cell key | Default | What it does |
|---|---|---|
text |
"" |
The words. A cell written as a plain string is exactly this; an omitted or null text is an empty cell. |
across |
1 |
How many columns the cell covers, itself included. The columns it swallows are merged in the file, and their padding becomes measure — a spanned cell wraps later than its first column would. |
down |
1 |
How many rows the cell covers, itself included. The rows beneath it are written with only the columns left to them. |
align |
its column's | Overrides align for this cell alone, which is how a Total label sits left in a right-aligned column. |
valign |
the table's | Overrides valign for this cell alone — a down: label centred against top-set body cells. |
emphasis |
false |
Set in the heading weight. A header or total row is already emphasised. |
pair |
the row's | A palette pair painted behind this cell alone — the recommendation in a comparison, the outlier in a column. |
table:
header: [Workstream, {text: Effort, across: 2, align: center}, Owner]
align: [left, right, right, left]
widths: [3, 1, 1, 1.4]
banding: true
rows:
- [Spec and schema, "12", "8", Compiler]
- [Placement engine, "21", "14", Layout]
total: [{text: Total, emphasis: true}, "33", "22", ""]rules: names what the lines mark, in the colour a rule would take on this slide —
so the theme's line grey gives way to muted on a white page rather than disappearing
into it.
rules |
Draws |
|---|---|
rows |
A hairline under every row but the last. The default, and what a table of figures wants. |
header |
One line, under the header band — plus a total:'s own rule above itself, which every mode but none keeps. The editorial look: the body rows are parted by their spacing. |
grid |
The row lines, plus one down the right of every column but the last. |
none |
Nothing at all — including above a total:, which is still marked by its weight. Pair it with banding: true, or the rows have nothing separating them. |
A cell can reach down as well as across. A row beneath a down: cell is written
with only the columns left to it, because the one above is already spoken for:
table:
rules: grid
valign: top
header: [Stage, Step, Owner]
rows:
- [{text: Discovery, down: 2, valign: middle, emphasis: true}, Interviews, Ana]
- [A brief either side can argue with, Ana]
- [{text: Build, down: 2, valign: middle, emphasis: true}, The first slice, Bo]
- [Everything the first slice deferred, Bo]A reaching cell is merged in the file, so it is one cell in PowerPoint too. Its rule falls where its span ends, and the row boundaries it crosses stay unruled in its own column alone — which is what parting a merged label from the rows beside it looks like.
Reaches stack: two columns may reach down past a third that does not, and the row below is written with the one column left to it.
table:
header: [Phase, Detail, Who]
rows:
- [{text: Discovery, down: 2}, {text: "Interviews, then a brief", down: 2}, Ana]
- [Bea]A row cannot be covered from above in every column. Such a row has no cells of its
own, so it is height and nothing else — and the table you meant is the same table with
neither that row nor the down: that reached into it. It is refused rather than drawn,
which is also what keeps the deck renderable: a row that is entirely a vertical merge is
legal OOXML that LibreOffice imports wrongly, dropping the table's last row.
Where banding's colour comes from. With no body_pair it is the surface pair,
which exists for this — a plate distinct from the page, contrast-checked when the theme
loaded. With one, it is that pair's own colour shifted slightly toward its ink, keeping
the ink the pair was validated with. It is never derived from what the slide happens to
show: nudging a sampled photograph puts a mid-grey under the type.
title: What is on the menu
place:
- at: {cols: full, rows: {from: 0, to: 7}}
table:
header: [Dish, Category, Served, Price]
align: [left, left, center, right]
widths: [3, 2, 2, 1]
density: 0.7
rows:
- [Bruschetta al pomodoro, Starter, All day, $8]
- [Tagliatelle al ragu, Main, From 6pm, $18]
- [Branzino al forno, Main, From 6pm, $26]
- [Tiramisu, Dessert, All day, $9]A table is rectangular: every row has to account for every column exactly once — its own
cells, their across: spans, and any column a down: cell above already covers. The
error names the row by its position in your own rows: list. The column count comes from
header: when there is one, otherwise from the first body row; neither can inherit a
column, so both are a plain count.
Row heights come from the same wrap estimate every component uses, and the estimate
over-reserves rather than risk clipping — a cell that lands on a wrap boundary can get a
row one line taller than its text renders. The slack lands at the bottom of a top-anchored
cell, where it reads as a stray blank line; valign: middle distributes it evenly and the
rhythm evens out.
A row holding an unquoted comma is read by YAML as a nested list, not a cell — the error says so, because it is the commonest way a table fails to parse.
A row is as deep as the wrap estimate says, and the estimate measures the cells in the face they will render in — real per-character advances for the theme's typeface, plus a small safety margin, with a conservative ceiling table standing in for a face the library has no metrics for. A table row grows rather than clips, and the margin is a few percent of width, so a row very rarely reserves a line the render does not use.
A down: cell is measured against the rows it covers together, and any shortfall is
shared between them rather than charged to the first. That even share is not the
shallowest answer when two spans overlap a row — each grows it toward its own need —
so when the even split overruns the placement, a tight packing that satisfies every cell
is used instead. A table that fits is never refused for the sake of an even one; what
you lose in the fallback is the evenness, not the table.
The whole table is one reveal group.
Code, a config fragment, a schema — set in the theme's mono face on a plate of one of
its pairs. The other way to put a listing on a slide is document:, which renders
markdown through a headless browser and returns a picture; this draws real text, so
it is selectable in PowerPoint, recoloured by the theme, and present in the manifest as
lines qa's overflow check must find inside the plate.
Every line is measured in the theme's mono face against the width the plate really sets
it in — inside its padding and its rounded corners — so the build refuses a listing that
would run off its plate rather than drawing it. Courier New and its clones carry accents,
Greek, Cyrillic, arrows and the box-drawing glyphs a file tree is drawn in, each one column
wide. A mono face with no width table of its own is charged the widest monospace advance
measured for Latin, and the widest glyph of its class for anything beyond — Greek,
Cyrillic, arrows, box drawing — since such a face may not carry it, so its estimate errs
wide. A tab is drawn as spaces to the next multiple of four columns, and
trailing spaces are not charged. A space the face may not carry — an em, figure or narrow
no-break space — is charged a full em. Rows are set exactly 1.55 times the type size apart,
wrapped rows included — the advance the plate is sized for — and the leading over single
spacing above the first row is moved from the plate's top margin to its bottom one.
LibreOffice fills the plate with even padding; PowerPoint's spacing of the rows is awaiting
its first check.
| Field | Required | Default | What it does |
|---|---|---|---|
lines |
one of the two | — | A list, one entry per line. Blank entries are blank lines. |
text |
one of the two | — | The same thing as a block scalar, for pasting a listing in whole. |
heading |
no | — | A label above the plate, set in the head rung, like bullets. |
pair |
no | surface |
Any declared palette pair. The plate takes that pair's background, and the text the ink that reads on it. |
accent |
no | — | Line prefixes to emphasise. A line starting with one is drawn bold in the accent that reads on the plate. |
wrap |
no | false |
Whether a long line wraps. Off by default — a wrapped listing lies about its own indentation. Off, a line wider than the plate is refused; on, the plate is as deep as the rows the lines wrap to, broken where LibreOffice breaks them — at spaces and after hyphens, not at a no-break space or inside -1 or -- — indentation included. |
size |
no | the caption rung |
Point size. Refused below the theme's minimum. |
title: The spec, shown as itself
place:
- at: {cols: {from: 0, to: 7}}
code:
heading: deck.yaml
accent: ["theme:", "place:"]
lines:
- "theme: brand"
- "---"
- "place:"
- " - at: {cols: full}"
- " bullets: {items: [...]}"A chip per palette role, labelled with the role's name and the hex it resolved to. The
values are read from the live theme at build time, so a slide showing what
deckwright conform derived from a brand template cannot drift from the theme the deck
was actually built against.
A chip whose colour is close to the slide's own paper — page, surface, an
inverse-ink — is given the theme's rule colour as an edge, because a white chip on a
white page is not a chip.
| Field | Required | Default | What it does |
|---|---|---|---|
roles |
no | every role the theme declares | Which roles to show, in order. |
caption |
no | — | A note beneath the chips. |
columns |
no | 8 |
How many chips per row, from 1 to 8. More than there are roles gives each role an equal share of the row. |
title: Every role, and the hex it resolved to
place:
- at: {cols: full}
swatches:
roles: [ink, accent-1, accent-2, muted, line]
caption: Read from the live theme at build time.The columns every at: resolves against, drawn as bars, with any region a placement
must stay out of laid over them. Both come from the live theme: the grid a deck is
measured on, and the polygons conform derives from a brand template's artwork.
A reserved region is drawn as its real polygon, not its bounding box — a corner wedge is a triangle, and the usable space above its diagonal is exactly what a box would wrongly appear to forbid.
| Field | Required | Default | What it does |
|---|---|---|---|
show |
no | columns |
columns, rows, or both. |
reserve |
no | true |
Whether to overlay the theme's reserved regions. |
caption |
no | the theme's own measurements | A note beneath. The default states the column count, width and gutter the theme actually holds. |
title: A grid, and a polygon it has to respect
place:
- at: {cols: full}
grid: {}A colour field: a half-canvas band, an edge strip, or the plate a title is reversed
out of. It carries no text — a chrome line naming the same pair: supplies that,
and reads on it because the palette checked the two colours against each other when
the theme loaded.
| Field | Required | Default | What it does |
|---|---|---|---|
pair |
no | surface |
Any declared palette pair. The block is painted in that pair's background. |
radius |
no | 0 |
Corner rounding as a fraction of the block's short side. 0.5 is a stadium. |
title: Reversed out of a panel
chrome:
title: {at: {box: {x: 9%, y: 36%, w: 30%, h: 20%}}, pair: accent-3}
place:
- at: {box: {x: 0%, y: 0%, w: 45%, h: 100%}}
bleed: true
panel: {pair: accent-3}A fill too close to the slide's own paper to see — the default surface on a white
page — is given the theme's line colour as an edge, so the block reads on a light
background as well as a dark one.
The commonest non-text mark in the corpus. The diameter is the short side of its placement, so the same declaration is a bullet dot in a one-column slot and a badge in a half-canvas box.
| Field | Required | Default | What it does |
|---|---|---|---|
pair |
no | accent-1 |
Any declared palette pair. The disc is painted in that pair's background, and a label in its foreground. |
label |
no | — | Text centred in the disc. Refused, never clipped, when the disc cannot hold it. |
rung |
no | caption |
The label's type-ramp role. |
size |
no | 1.0 |
Diameter as a fraction of the placement's short side. |
shadow |
no | false |
Drop the theme's declared shadow behind it. |
place:
- at: {cols: {from: 0, to: 1}, rows: {from: 0, to: 2}}
ellipse: {label: '1'}
- at: {cols: left-half, rows: mid-third}
align: center
anchor: middle
ellipse: {label: '2', size: 0.5, rung: stat, shadow: true}Unlike every other component, align/anchor move the disc inside its placement
rather than moving type: the shape is the content here, and a label is centred in it by
construction. A disc smaller than its placement therefore sits at the top-left corner
until you say otherwise.
132 rounded-rect cards across the corpus, and they are all this object. The plate is a declared pair, so its type is contrast-checked against the plate's own fill, not against the slide's — a white card on a dark slide still reads.
| Field | Required | Default | What it does |
|---|---|---|---|
heading |
one of the three | — | Set at the head rung. |
body |
one of the three | — | One or two lines at the body rung. |
icon |
one of the three | — | A glyph name (shield) or an image file (logo.png), placed at the top of the plate two heading line-heights square. A bare name is a glyph; anything with a suffix or a path separator is a file, resolved like any deck image. |
alt |
no | — | What a screen reader says in place of the icon. Beside a heading or body an unlabelled picture icon is marked decorative; a card that is only an icon needs this. |
decorative |
no | false |
true marks the icon for a screen reader to skip. Refused beside alt. |
pair |
no | surface |
Any declared palette pair. |
radius |
no | 0.06 |
Corner rounding as a fraction of the plate's short side. 0 squares it, 0.5 is a stadium. |
shadow |
no | false |
Drop the theme's declared shadow behind it. |
place:
- at: {cols: left-third, rows: {from: 0, to: 5}}
card:
heading: Discover
body: Read the corpus before naming the vocabulary.
shadow: trueThe type is inset from the plate by the theme's gutter on every side, and align/
anchor set it inside that area. The gutter is measured from the plate's painted
edge, not from its square corner, so a large radius insets the sides further: on a
stadium the corner curve has swept a long way in by the height the first line sits
at, and a fixed inset would leave the leading glyph outside the shape. A card with
none of the three content fields is refused, naming panel — an empty plate is that
component.
Sixteen slides across six of the eleven sample templates lay a process out as a run of
steps. flow is that slide in one block: it divides the placement into a cell per step,
draws each as a card, puts a numbered ellipse on the rail beside it, and joins
consecutive marks with a connector. Nothing is hand-placed and no step repeats a
coordinate.
| Field | Required | Default | What it does |
|---|---|---|---|
items |
yes | — | The steps. At least two — one step is the card component. |
items[].head |
yes | — | The step's heading, at the head rung. |
items[].icon |
no | — | A glyph name, drawn above the step's heading. The plate grows to fit it. |
items[].body |
no | — | One line of supporting copy under it. |
direction |
no | horizontal |
horizontal runs the steps across the placement, vertical down it. |
numbered |
no | false |
Put a numbered disc on the rail: above each step across, left of it down. |
current |
no | — | The 1-based step to highlight. Its plate takes the accent. |
pair |
no | surface |
The palette pair every other plate is painted in. |
arrow |
no | end |
none, end or both, on each join. |
place:
- at: {cols: full, rows: top-two-thirds}
flow:
numbered: true
current: 2
items:
- {head: Discover, body: Read the corpus first.}
- {head: Define, body: Name the vocabulary.}
- {head: Build, body: One primitive at a time.}
- {head: Verify, body: Look at the render.}Every plate is drawn as deep as the wordiest step needs at that width, never deeper — a run whose plates each stopped at their own copy would read as a ragged edge rather than a sequence. So the placement bounds the flow, it does not define it: a horizontal flow sits at the top of its rectangle at the depth its copy earns, and only fills the whole rectangle when the copy needs all of it.
The join attaches to the discs when the steps are numbered and to the plates when they are not, so a numbered flow reads as a rail with plates hanging off it. Down the page each plate floats in the middle of its cell, keeping the disc level with the copy.
align passes through to each step's type; anchor settles the whole run inside its placement, like any content-sized component. One reveal group per step, and
the join into a step belongs to that step — so animate: one_at_a_time builds the
process a step at a time, arrow and all.
The line attaches to the edge of each end that faces the other, so moving either placement moves the join and nothing in the spec repeats a coordinate.
| Field | Required | Default | What it does |
|---|---|---|---|
from |
yes | — | A placement id:, or [x, y] as fractions of the canvas. |
to |
yes | — | The same. |
kind |
no | straight |
straight, elbow or curved. straight and curved are native connectors; elbow is drawn as a freeform polyline through explicit bend points, routed the same way regardless of renderer. |
arrow |
no | none |
none, end (the to end) or both. |
color |
no | accent-1 |
A palette role, not a pair. |
weight |
no | 1.0 |
Stroke weight as a multiple of the theme's line weight. |
One reveal group, reported under the line motion role, the same as rule.
place:
- at: {cols: {from: 0, to: 3}, rows: {from: 2, to: 6}}
id: discover
card: {heading: Discover, body: Read the corpus.}
- at: {cols: {from: 3, to: 4}, rows: {from: 2, to: 6}}
connector: {from: discover, to: design, arrow: end}
- at: {cols: {from: 4, to: 7}, rows: {from: 2, to: 6}}
id: design
card: {heading: Design, body: Name the vocabulary.}Two things follow from "it joins placements":
- A connector draws between rectangles, not inside its own, so its own
at:still has to clear the placements it joins. The gap between two cards is the natural home, as above; a line that must cross either end declaresbleed: trueand takes a full-canvasbox:. - The join lands on the placement's edge, not on the shape drawn inside it. A disc at
size: 0.5leaves a visible gap; give it a placement it fills and the join is exact.
An id no placement on the slide declares is an error listing the ids that exist. A
from and to that resolve to the same point is an error too — the line has no
direction to draw in.
Dense copy that is neither a list nor a chart: an argument, a letter, a quotation. The
frame is capped near the classic 66-character measure rather than the placement's full
width — a 16:9 band sets a line nobody can track back to its successor. A content-sized
component: give the placement anchor: middle to settle it, and align: to place the
capped frame inside a wider placement.
| Field | Required | Default | What it does |
|---|---|---|---|
paragraphs |
yes | — | A list of strings, one per paragraph. |
cite |
no | — | An attribution. Its presence sets the paragraphs as a quotation — italic, with — cite beneath in the caption style. |
title: The case in two paragraphs
place:
- at: {cols: full}
anchor: middle
prose:
paragraphs:
- The night network is not a luxury service.
- Cutting it saves four percent and strands the late shift.One equation on a line of its own, from a small subset of LaTeX. It is written as Office
Math, the form PowerPoint keeps an equation in, with a fallback copy of the same shape
holding the equation as one line of plain text in UnicodeMath:
x = (−b ± √(b² − 4ac))/(2a). A reader without Office Math takes the fallback, as
LibreOffice does, and so do render, qa, extract and diff. That line is what the build
measures, so a fit check never trusts maths it cannot see.
| Field | Required | Default | What it does |
|---|---|---|---|
tex |
yes | — | The equation, in the subset below. Single-quote it: in double quotes YAML reads \f, \t, \n, \b and \r as escapes, so \frac arrives as a form feed. |
rung |
no | lead |
The type rung both the maths and its line are set at. |
alt |
no | the line | What a screen reader says. Left out, it reads the UnicodeMath line. |
decorative |
no | false |
true marks the equation for a screen reader to skip. Refused beside alt. |
The subset. ^ and _ with one token or a {group}; \frac{}{}; \sqrt{} and
\sqrt[n]{}; \sum, \prod, \int with optional limits, and \lim_{}; the Greek letters
\alpha to \omega and \Gamma \Delta \Theta \Lambda \Xi \Pi \Sigma \Phi \Psi \Omega;
\cdot \times \pm \mp \leq \geq \neq \approx \infty \to \partial \nabla; \left and
\right with ( ) [ ] \{ \} | or .; \text{}; letters, digits and
+ - = < > , . ! / | ( ) [ ]. Anything else — \begin, \over, &, a macro — is refused
by name. A large operator applies to everything after it up to the next =, <, >, ≤,
≥, ≠, ≈ or →.
It never wraps. An equation wider than its placement on one line is refused. Its box is as tall as the rows its Office Math stacks — a fraction is two, limits under a sum add one — so the space a stacked fraction needs is kept even where the line shows.
The maths is written as PowerPoint stores an equation: the shape inside
mc:AlternateContent, Office Math in the Choice and the line in the Fallback. PowerPoint
opening it without a repair prompt, and setting it as maths, is awaiting its first check.
title: The quadratic formula
place:
- at: {cols: full, rows: {from: 0, to: 4}}
anchor: middle
align: center
equation:
tex: 'x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}'| Field | Required | Default | What it does |
|---|---|---|---|
orient |
no | horizontal |
horizontal or vertical. |
color |
no | line |
A palette role, not a pair. |
weight |
no | 1.0 |
Stroke weight as a multiple of the theme's line weight. A hairline is about 0.3. |
One reveal group, reported under the line motion role — so a rule draws itself
whichever way the theme binds that role (wiperight by default). See
motion.md.
place:
- at: {cols: full, rows: {from: 0, to: 1}}
rule: {color: accent-1, weight: 1.5}
- at: {cols: {from: 5, to: 6}, rows: {from: 1, to: 10}}
align: center
rule: {}A rule spans its placement outright, so the key that would move it along that axis is
refused rather than ignored: a horizontal rule reads anchor (top | middle |
bottom) and refuses align, and a vertical one is the mirror image.
How a stroke gets its colour. A line has no pair to be checked against, so rule
and connector share one rule. A role you name is used as asked, and refused only when
it is invisible against this slide's paper — a brand's own hairline colour at 2.6:1 is a
design choice, not a mistake. The default has no author behind it, so it must clear
WCAG's 3:1 non-text minimum or give way to a role that does: the theme's line grey is
right on a dark slide and a ghost on a white one, where the rule takes muted instead.
| Field | Required | Default | What it does |
|---|---|---|---|
items |
no | the deck's sections: |
The section names, left to right. Non-empty list. Leave it out and the nav follows the deck, so a rename or a reorder in sections: reaches every slide. |
active |
no | the slide's own section:, when it is one of items |
Which of them this slide is in. A name given here must be one of items; a name that is not is an error, because a renamed section would otherwise read as no section. Labels shorter than the section names mark nothing unless active: names one. |
color |
no | the guarded accent | A palette role for the active label. |
A section eyebrow, for a deck long enough that a reader loses the thread but not long
enough to want a divider slide between every act. Set it in a one-row band as nav: {}
and repeat the placement: each slide marks the section it declares.
No reveal group. An eyebrow is chrome, and a slide that builds its body should not
spend its first click arriving at its own furniture — so the band is on screen from the
first beat whatever animate: says.
place:
- at: {cols: full, rows: {from: 0, to: 1}}
align: right
nav:
items: [Problem, Evidence, What shipped, Next]
active: EvidenceHow the active label gets its colour. The same rule rule and connector hold for a
stroke, one layer up: a role you name is used as asked and refused only when it cannot be
seen at all against this slide's paper. A brand accent that reads at 2.6:1 is a design
choice, and the active item is a rung larger and bold as well as coloured, so the mark
never rests on colour alone. The default has no author behind it, so it goes through
the guarded accent — which at caption size wants 4.5:1 and mostly will not get it, and
gives way to the slide's own ink. Name the role when you want the brand colour; leave it
out when you want the reader to be certain.
Naming a role under AA is honoured, and qa's contrast check will still
report the label — a WARN, not an error. That is the check doing its job: the ratio is
genuinely low, you decided it anyway, and the deck still builds.
The corpus's commonest non-text shape by a wide margin: 4,019 custGeom freeform
paths in the size range a glyph occupies, against eight charts and five tables across
128 slides. A card with a mark reads as designed; the same card without one reads as
typeset.
An icon is drawn as real path geometry, not as a pasted picture, so it stays sharp at any size, recolours from the palette, and is editable in PowerPoint like any other shape.
| Field | Required | Default | What it does |
|---|---|---|---|
name |
yes | — | Which glyph: a lowercase slug like chart-bar. Any Material Symbols name from the shipped set (rocket_launch, or the hyphenated rocket-launch), or a curated alias (deploy, team, growth). |
alt |
no | — | What a screen reader says in place of the glyph. See a figure says what it shows. |
decorative |
no | false |
true marks the glyph for a screen reader to skip. Refused beside alt. |
size |
no | 1.0 |
The glyph's side as a fraction of the placement's short side — literally min(width, height) * size, so the drawn square is predictable from the rect the placement was given. 1.0 fills the short side; the long side keeps the leftover as margin. |
ink |
no | — | A colour role, painted verbatim. Omit and the mark takes the first brand accent that reads where it lands, falling back to the surface's ink — and, where no colour reads across the box at all, to a plate of the slide's paper behind the glyph (see icons.md). That plate is padded around the glyph and clipped to the placement, so it never reaches the placement beside it. |
place:
- at: {cols: {from: 0, to: 3}, rows: top-third}
icon: {name: target}
- at: {cols: {from: 3, to: 6}, rows: top-third}
icon: {name: chart-bar, ink: accent-2}The placement is squared off before the glyph is drawn, so an icon in a wide slot
keeps its proportions rather than stretching. Where that square sits in the rect is the
placement's align and anchor — left/top by default, so add align: center to
centre it.
Which name to reach for is docs/glyphs.md, a shortlist grouped by
what the slide is about. 4,001 vendored Material Symbols resolve, plus the curated names
in docs/icons.md. Asking for one nobody drew fails the
build with the closest real names.
Where the glyphs come from, in order: $DECKWRIGHT_ICON_DIR, the theme's own
icons: directory, then the shipped set. A brand overriding target
with its own drawing needs no change to any deck. Each file is an .svg with a
viewBox and one or more <path> elements — <circle>, <rect> and strokes are not
read, so flatten a drawing to paths before dropping it in.
Colour is not decoration. WCAG puts a meaningful graphic at 3:1 (1.4.11) — the
same floor a large heading takes — so an accent below that against what is behind it
is not a stylistic choice, it is a mark nobody can see. Naming an ink skips the
check, on the grounds that an author who names a colour means it.
The corpus's commonest slide by a wide margin. Picture and caption are one component,
not two, because the scrim between them is solved against the pixels the caption
actually covers: write over: and you get a measured scrim by default.
| Field | Required | Default | What it does |
|---|---|---|---|
src |
yes | — | The image file. Resolved beside the deck spec, then beside the theme's template, then out of the template's own ppt/media/. A relative name may not climb out of those directories with ..; an absolute path is taken as written. |
alt |
no | — | What a screen reader says in place of the picture. Without it, or decorative, qa warns. |
decorative |
no | false |
true marks the picture for a screen reader to skip. Refused beside alt. |
fit |
no | cover |
cover fills the placement and crops the source; contain shrinks the picture to fit whole, letterboxing it. |
crop |
no | — | Trim the source to this aspect first, centred — 16:9, 4/3, or a bare 1.78. Applied before fit. |
mask |
no | none |
none, circle, or rounded. |
radius |
no | 0.08 |
Corner radius for mask: rounded, as a fraction 0–0.5 of the picture's short side. |
inset |
no | 0.025 |
Gap between the picture's edge and its over: text, as a fraction of canvas width. |
scrim |
no | on when over: is present |
false to suppress it, true to solve one, or the mapping below. |
over |
no | — | Lines of type reversed out of the picture. Each is a string, or a mapping of text, rung, align. |
place:
- at: {cols: full, rows: top-two-thirds}
anchor: bottom
image:
src: harbour.jpg
crop: "16:9"
scrim: {gradient: bottom}
over:
- text: The sea took them one by one
- {text: Book XII, rung: caption}mask: circle squares the placement first. An ellipse drawn on an oblong box is an
oval, so the box is reduced to the largest square inside it, positioned by the
placement's align/anchor. For the same reason mask: circle refuses fit: contain
— that would re-oblong the box to the source's own aspect. Crop to 1:1 instead.
The scrim mapping.
| Field | Default | What it does |
|---|---|---|
pair |
inverse |
The contrast-checked palette pair the scrim's colour and its ink come from. |
opacity |
auto |
A fraction 0–1, or auto to solve the least opacity that clears WCAG AA over these pixels. |
gradient |
none |
none, top, or bottom — which edge the scrim is opaque at, fading to clear at the other. |
An auto opacity is solved against the sampled photograph at the band the text
occupies, so a caption in a picture's dark third gets far less scrim than the same
caption across all of it. Under a gradient it is solved at the band's weakest point.
A light photograph with white over: text will be scrimmed nearly flat — that is the
honest answer, and the fix is a gradient:, a darker crop, or a pair: whose ink is
dark.
Renders the file itself into a macOS-style window card and places it as a picture. Showing the real document beats retyping it: it cannot drift from the source.
Pictures the markdown names are embedded in the card when they sit in the markdown's own
folder or below it: , and in HTML however it is quoted, an <img>'s src,
each srcset candidate of an <img> or a <picture>'s <source>, an inline SVG
<image>'s href, a <video>'s poster, and each CSS url() in a style attribute or
a <style> block. A name is read as a browser reads it, so my%20pic.png is the file
my pic.png. One that sits anywhere else is refused by name, since the markdown behind a
card is often not yours; a web address is left for the browser to fetch, and so is a CSS
url() naming a font.
| Field | Required | Default | What it does |
|---|---|---|---|
source |
yes | — | Path to a markdown file. Resolved beside the deck spec first, then as given — absolute, or relative to the directory you run the command from. |
alt |
no | — | What a screen reader says in place of the card. The card is a picture, so its words are not otherwise readable. |
decorative |
no | false |
true marks the card for a screen reader to skip. Refused beside alt. |
side |
no | full |
left, right, or full. left/right render it at half the body width. |
max_width |
no | 1000 |
Layout width in CSS pixels before the screenshot. Lower it to make the type read larger on the slide. |
filename |
no | the file's own name | The text in the card's title bar. |
lines |
no | the whole file | 12-40 — one-based and inclusive, the way an editor names them. Cards part of a long file without copying that part into a second file, which is the drift this component exists to prevent. |
section: Spec
title: The document itself, not a retyping of it
place:
- at: {cols: full}
document:
source: ../docs/qa.md
lines: '1-9'
side: right
max_width: 760This one needs a Chromium-family browser on the machine; the others do not.
A card taller than the slide is refused rather than shrunk to unreadable. Reach for lines: to card the part you mean, or raise max_width to set the type smaller — never copy an excerpt into a second file. That copy is what this component exists to avoid, and it drifts.
The "look what that one line actually does" slide: a plate on the left for the call, a
bus, and one branch per consequence. A flow is
the wrong shape for it — a flow is a sequence, each step following the one before, and
this is a fan, every branch leaving the same point at once.
| Field | Required | Default | What it does |
|---|---|---|---|
source |
yes | — | The call the branches leave from, set in the theme's mono face on an accent plate. |
items |
yes | — | The consequences. At least two — one is the connector component. |
items[].text |
yes | — | What that branch sets off. |
items[].icon |
no | — | A glyph on the branch, between the arrow and the text. |
weight |
no | 1.0 |
Bus stroke as a multiple of the default, 0.5 to 4.0. |
title: Invisible side effects
subtitle: One line of code. None of the right-hand column is visible at the call site.
animate: one_at_a_time
place:
- at: {cols: full, rows: {from: 0, to: 9}}
fanout:
source: publish(post)
weight: 2.0
items:
- {icon: mail, text: Subscriber digest}
- {icon: password, text: Cognito login synchronisation}
- {text: A dozen cache invalidations}The plate, the trunk and the spine are the first reveal group, so the shape is established before anything hangs off it; then one group per branch, its arrow, glyph and text arriving together.
A before and an after, or a this and a that. Two stats
tiles say the same numbers without saying they are opposed: the glyph between these two
is what makes the pair read as one comparison, and highlight says which side the slide
is arguing for.
| Field | Required | Default | What it does |
|---|---|---|---|
left |
yes | — | The first side: a mapping of the keys below. |
right |
yes | — | The second side, same keys. |
…value |
yes | — | The magnitude, at the stat rung. |
…label |
yes | — | What it measures. |
…note |
no | — | A third line, smaller. |
…highlight |
no | false |
Paint this side in the accent. One side only — marking both says nothing. |
icon |
no | schedule |
The glyph between them. A clock for time, a scale for a trade. |
title: Default to a unit test
place:
- at: {cols: full, rows: {from: 0, to: 5}}
versus:
icon: schedule
left: {value: "2 days", label: the unit stage — it runs in parallel}
right: {value: "1 day", label: collected in person, highlight: true}The plates are sized in proportion to their values. A value the component can read a
number out of — 11.2%, $1,200, 1.6x — sizes its plate, so a side 60% larger is 60%
wider and the shape carries the comparison rather than leaving it to the reader to do the
arithmetic. choosing.md has the reason: length is a far stronger encoding
than a printed number, and two identical plates state that the magnitudes are equal.
Both sides have to be written in the same unit. 2 days against 4 hours reads 2
against 4 and would draw the longer span smaller, so a pair whose units differ keeps the
even split instead. The unit is matched whole, so 1 day against 4 days splits evenly
too — no string rule separates a plural from a different unit, hrs/hr and ms/m
being the same shape, and sizing two units against each other argues the opposite of the
data. Write both sides in the same unit to get the proportional plates. A magnitude suffix
counts as part of it: $1.2M and $480K do not compare, $1.2M and $0.48M do.
The smaller plate never drops below the width its longest word needs, so a long label
widens the plate rather than breaking mid-word; a placement too narrow to set both sides
is refused at build, naming the two widths. Whether the stack then fits the band is
qa's text-fit check — give the placement more rows, or shorten the note.
Values with no number in them — before and after — keep the even split; there is
nothing to encode.
Each plate is its own surface, so its type is contrast-checked against the plate's fill rather than the slide's. One reveal group per side; the glyph arrives with the first.
The shape a before-and-after wants when some measures improved and others got worse:
right of the rule is movement toward the target, left is away from it. A chart cannot
draw this — a bar chart with negative values writes correct OOXML, but the LibreOffice
path render and qa both go through draws those bars rightward and strips the sign
from the label, so the render loop cannot verify the one thing the slide is about.
| Field | Required | Default | What it does |
|---|---|---|---|
items |
yes | — | Non-empty list of mappings, one row each. |
items[].label |
yes | — | The row's name, set flush to the rule. |
items[].value |
yes | — | The signed number. Positive draws right, negative left, each in an accent that stands off what the bars sit on (see below). |
items[].note |
no | — | A quiet aside — the before-and-after counts behind the percentage. It is placed in the half the row's own bar does not use, so it never collides with the bar however long it runs. |
unit |
no | — | A suffix on every reading — %, pt, x. The sign is always written, so a bare number reads as a count and a % as a change. |
peak |
no | the largest magnitude present | The magnitude the longest bar stands for. Pin it when two blocks share a slide, or each scales to its own longest bar and two equal values draw unequal. |
label_width |
no | 0.3 |
The label column as a fraction of the placement's width, 0.1 to 0.6. |
pair |
no | — | A declared pair this block is drawn on. The placement is painted in that pair's background and every label is inked against it rather than the slide, which is what lets a light half and a dark half sit on one slide. |
title: Refactor roadmap
subtitle: Right is toward the documented architecture; left is away from it
animate: one_at_a_time
place:
- at: {cols: full, rows: {from: 0, to: 5}}
diverge:
peak: 300
unit: "%"
items:
- {label: Sorted by hand, value: 271, note: 7 to 26}
- {label: Machine passes, value: 92, note: 127 to 244}
- at: {cols: full, rows: {from: 5, to: 9}}
diverge:
peak: 300
pair: inverse
items:
- {label: Misroutes, value: -40, note: 10 to 14}Both blocks name the same peak, so a bar in the dark half is comparable with one in
the light half. align is refused — the labels are set against the rule, and centring
them would pull every one off the axis it belongs to.
The bar colours are chosen against what is behind the bars: the page, the block's own
pair, or a backdrop photograph. The candidates are the accents in order, then the
labels' ink. Rightward bars take the first candidate that stands off that ground by 3:1
or by 35 ΔE, the same rule as qa's fill-ground check. Leftward bars take the first
that stands off both the ground and the rightward colour, so the two directions differ
wherever the theme allows; when none does, they share the rightward colour. On most
themes that is the first and second accent. On a theme whose page is the colour of an
accent, the bars skip that accent instead of vanishing into the page.
One reveal group per row: the label, the bar, its value and its note arrive together.
The chart is a native OOXML chart part with an embedded worksheet: editable in PowerPoint via Edit Data, vector at any zoom, and its labels are real text.
section: Spec
title: Two channels, stacked
subtitle: Series names are the keys of each row's values
animate: by_series
place:
- at: {cols: full}
chart:
kind: column-stacked
data:
- {category: Q1, values: {Ads: 20, Organic: 15}}
- {category: Q2, values: {Ads: 28, Organic: 22}}
- {category: Q3, values: {Ads: 35, Organic: 31}}
- {category: Q4, values: {Ads: 42, Organic: 41}}The chart block — kind:, the three row shapes, highlight: and all 29 kinds — is
docs/authoring.md.