🗂️ Deck Templates
How do I define one house style with named layouts my slides can reference, like a PowerPoint master?
A deck template is one file beside the deck that owns how the deck looks. It is the same idea as a slide master in PowerPoint or Google Slides: design tokens and the chrome every slide carries, plus a set of layouts addressed by id that a slide references by name.
---
title: Q3 Business Review
template: ./deck.template.ts
---
---
layout: title-slide
---
# Q3 Business Review
// deck.template.ts
import type { DeckTemplate } from "@getnarro/markdown";
export default {
id: "acme",
tokens: { colors: { brand: "#5b8cff", accent: "#ffb020" } },
master: {
footer: { text: "{title} · {date}" },
slideNumber: { format: "{n} / {total}", from: 2 },
},
layouts: {
"title-slide": { base: "cover", master: false },
"metric-split": { base: "two-column", regions: { right: "text-right text-accent" } },
},
} satisfies DeckTemplate;
That deck now has a footer and a slide number on every slide but the first, an
id (title-slide) that narro check accepts and title-slidee that it
rejects, and two colours usable as text-brand and text-accent anywhere in
the markdown.
A runnable version of exactly this is
apps/template-deck
in the repository.
Why not just class: everywhere
You can do all of this per slide, and for a ten-slide talk you should. A template earns its keep when the same look has to hold across forty slides and survive being edited by someone else:
- The deck stops naming design. No colours, no sizes, no chrome in the markdown — so rebranding is one edit rather than a find-and-replace.
- Layout ids become a closed set. A name that is not in the template does
not exist, and
narro checksays so, with the ids that would have worked. - Slots get checked on custom layouts too. A template layout declares its
slots, so
::sidbar::is an error instead of content that never renders.
Where the file lives
template: in the deck frontmatter, relative to the deck:
---
template: ./brand/acme.template.ts
---
Leave it out and a file beside the deck is used, if there is one:
<deck>.template.* first, then template.* — the same way <deck>.css and
then deck.css are picked up. Naming it explicitly is worth it when the deck is
going to be read by someone who has not seen this page.
Any of these extensions works: .ts, .mts, .js, .mjs, .json, .yaml, .yml.
TypeScript is the one to reach for. satisfies DeckTemplate gives autocomplete
on every key and an error on a mistyped one, and because the import is
import type it is erased before the file runs — so the template loads in a
deck directory with no node_modules at all. In a project that depends on
@getnarro/markdown you can write defineTemplate({ … }) instead.
A template is data: no JSX, no imports of components, nothing that needs a
bundler. That is what lets narro check read it in milliseconds. The one thing
data cannot express has its own escape hatch, below.
The shape
Top-level keys: id, name, description, version, author, tokens, css, master, layouts.
Unlike deck frontmatter, this shape is closed — a key it does not define is
an error, not something quietly kept for a theme to read. Nothing else reads a
template file, so a stranger in it is always a typo.
Tokens
tokens: {
colors: {
background: "#080d18",
foreground: "#e8edf7",
primary: "#5b8cff",
accent: "#ffb020",
brand: "#5b8cff",
},
fonts: { heading: '"Inter", sans-serif', body: '"Inter", sans-serif' },
}
Each colour lands in three places. The six semantic names — primary,
secondary, background, foreground, muted, accent — become the
--slide-* variables the deck’s base styling reads, so setting background
changes the deck’s background without a class anywhere. Every name, semantic or
not, also becomes a Tailwind v4 theme colour: brand gives you text-brand,
bg-brand/10, border-brand, from-brand. Fonts work the same way, as
font-heading and friends.
This is the same variable set a marketplace theme fills in, so a deck can name
both — theme: for the palette, template: for the layouts — and the template
wins where they overlap.
For anything tokens cannot say, css: is appended to the deck’s stylesheet
after them:
css: `.rs-chrome-footer { letter-spacing: 0.16em; text-transform: uppercase; }`,
The master
`class`, `background`, `header`, `footer`, `logo`, `slideNumber`,and every one of them applies to every slide in the deck — including slides that name no layout at all, exactly as a PowerPoint master does.
master: {
class: "bg-linear-to-b from-brand-deep to-background",
footer: { text: "{title} · confidential" },
logo: { image: "https://acme.example/logo.svg" },
slideNumber: { format: "{n} / {total}", from: 2 },
}
The four placeholders — header, footer, logo, slideNumber — take
position is one of
defaults are chosen so the four never collide: header top-left, logo
top-right, footer bottom-left, slide number bottom-right.
Text may contain {title}, {author} and {date} — read from the deck
frontmatter — and {n} and {total}. They are substituted when the deck is
built, so what reaches the browser is a finished string, and
narro check --render sees the same slide the projector will. A brace naming
something else is left alone.
from: 2 on slideNumber is the title-slide exemption; image is an ordinary
URL, so an absolute one, a data URI, or a file in a scaffolded project’s
public/ all work.
Placeholders never take a click — they are pointer-events-none, because a
bottom-left footer sits exactly where click-the-edge navigation lives.
Layouts
Each key under layouts is an id a slide can name. A layout takes
layouts: {
chapter: {
name: "Chapter divider",
description: "Announces a section.",
base: "section",
class: "border-brand border-l-8",
regions: { default: "text-brand" },
defaults: { transition: "fade" },
master: { footer: false },
},
}
base is the built-in the layout is built on (or a layout in the deck’s own
layouts/ directory), defaulting to default. A base never resolves to another
template layout — layouts on a master do not chain, so there is no order to
learn and no cycle to worry about.
class goes on the slide root, after the master’s and before the slide’s
own class:.
regions attaches classes to one slot’s content, keyed by slot name, with
default meaning the slide body. A declared region wraps that slot in a <div>
carrying the classes plus flex flex-col gap-4 — which is what every built-in
already puts around slot content, so the wrapper does not change spacing.
Undeclared regions are not wrapped at all. Tailwind’s arbitrary variants reach
inside: "[&_h3]:text-7xl [&_h3]:text-accent".
defaults are slide frontmatter defaults — background, transition, or
any key the layout’s component reads. A slide that sets the key wins.
master overrides the deck’s. false turns off every placeholder for this
layout, which is the clean title slide most decks want; { footer: false }
turns off one. The master’s class and background survive master: false —
they are the deck’s look rather than chrome. Override those with the layout’s
own class and defaults.background.
slots declares the named slots the layout renders. It declares; it
does not create. A layout built on a built-in already has that built-in’s slots
and cannot add to them — declaring one the base does not render is an error,
because a slide filling it would lose that content silently. Use slots for a
component, or for a base out of your own layouts/ directory whose props
nothing here can see.
A layout that needs code
Some layouts are not expressible as data. component: points at a React file,
relative to the template:
scorecard: {
component: "./template/Scorecard.tsx",
slots: ["cards"],
},
It is still a layout on the master: the slide still gets the footer, the logo
and the slide number, scorecard is still an id narro check knows, and
cards is still a slot it will reject a typo in. The component is an ordinary
Narro layout — see markdown mode.
How a name resolves
A slide’s layout: is looked up in this order, first match winning:
<deckDir>/layouts/<name>.{tsx,jsx,mdx,ts,js}— yours, shadows everything- The deck template’s
layouts - The active theme’s layouts
- The built-ins
So dropping a layouts/chapter.tsx beside the deck overrides the template’s
chapter for that one deck without touching the template.
What narro check tells you
$ narro check deck.md
template: acme (deck.template.ts)
layouts: title-slide, agenda, chapter, metric-split, pullquote, scorecard, close
✓ deck.md looks good
Which ids exist is the question a template raises and nothing else answers —
they live in a file the deck does not otherwise mention. The same run rejects a
layout id that does not exist, a slot the layout does not render, a base that
is not a layout, a region styling a slot that is not there, a component path
that does not resolve, and a template file that does not match the shape. All of
them are names, all of them build cleanly otherwise, and all of them produce a
slide that is subtly not what the file said.
Editing the template in narro dev reloads the deck.
What this is not
A template is a house style for one deck or one directory of decks, written
by whoever owns that deck. It is not a package: there is no registry, no
versioning beyond the version string, and no install step. Sharing one means
committing the file, or pointing several decks at the same path.
For a palette you did not write, use a marketplace theme. For a
whole starting project, use a
template project — npm create narro@latest. The word is
overloaded; these are three different things.