# Deck Templates

> The slide master for markdown mode — one file of tokens, chrome, and layouts addressed by id

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.

```markdown
---
title: Q3 Business Review
template: ./deck.template.ts
---

---
layout: title-slide
---

# Q3 Business Review
```

```ts
// 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`](https://github.com/getnarro/narro/tree/main/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 check` says 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:

```yaml
---
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: <!-- generated:template-extensions -->`.ts`, `.mts`, `.js`, `.mjs`, `.json`, `.yaml`, `.yml`<!-- /generated:template-extensions -->.
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: <!-- generated:template-keys -->`id`, `name`, `description`, `version`, `author`, `tokens`, `css`, `master`, `layouts`<!-- /generated:template-keys -->.
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

```ts
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:

```ts
css: `.rs-chrome-footer { letter-spacing: 0.16em; text-transform: uppercase; }`,
```

### The master

<!-- generated:template-master-keys -->`class`, `background`, `header`, `footer`, `logo`, `slideNumber`<!-- /generated:template-master-keys -->,
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.

```ts
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
<!-- generated:template-chrome-keys -->a placeholder takes `text`, `image`, `class`, `position`; `slideNumber` takes `format`, `class`, `position`, `from`<!-- /generated:template-chrome-keys -->.
`position` is one of
<!-- generated:chrome-positions -->`top-left`, `top-center`, `top-right`, `bottom-left`, `bottom-center`, `bottom-right`<!-- /generated:chrome-positions -->, and the
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
<!-- generated:template-layout-keys -->`name`, `description`, `base`, `component`, `slots`, `class`, `regions`, `defaults`, `master`<!-- /generated:template-layout-keys -->.

```ts
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:

```ts
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](/docs/markdown-mode#layouts).

## How a name resolves

A slide's `layout:` is looked up in this order, first match winning:

1. `<deckDir>/layouts/<name>.{tsx,jsx,mdx,ts,js}` — yours, shadows everything
2. The deck template's `layouts`
3. The active theme's layouts
4. 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](/docs/theming). For a
whole starting project, use a
[template project](/docs/installation) — `npm create narro@latest`. The word is
overloaded; these are three different things.