🗂️ 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 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:

---
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

a placeholder takes `text`, `image`, `class`, `position`; `slideNumber` takes `format`, `class`, `position`, `from`.

position is one of

`top-left`, `top-center`, `top-right`, `bottom-left`, `bottom-center`, `bottom-right`, 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

`name`, `description`, `base`, `component`, `slots`, `class`, `regions`, `defaults`, `master`.
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:

  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. For a whole starting project, use a template projectnpm create narro@latest. The word is overloaded; these are three different things.

IntroductionWhat is Narro, and should I be writing markdown or React?
InstallationWhat do I install, and what does the project look like afterwards?
First DeckWhat does a working React deck look like, end to end?
Markdown ModeHow do I split a slide, style one word, or reveal a line — without leaving the markdown file?
Writing AI PromptsWhat do I tell an AI so the deck it writes actually builds?
Rules for AI AgentsWhat do I paste into my repo so an agent stops writing decks that build wrong?
React APIWhich component or hook do I import, and what does it take?
CLIWhich command do I run, and what are its flags?
Markdown APIHow do I read, edit, or validate a deck file from my own code instead of by hand?
Component ReferenceWhat props does this component take, and which package do I import it from?
AnimationHow do I reveal a list one line at a time, or move between slides with something other than a cut?
NavigationHow does the audience move through the deck, and how do I present it?
Canvas & PositioningHow do I put something at an exact position instead of in the flow?
Images & MediaHow do I use an image as a background, tint it, or embed a video?
ArchitectureWhich package owns what, and why is the seam where it is?
Transform ModeHow do I zoom and pan across one big canvas instead of cutting between slides?
Import & ExportHow do I get this deck out as PPTX, PDF, or one file I can email?
AI IntegrationHow do I wire an AI assistant up to Narro so it can write and build decks?
ThemingHow do I change the colours, fonts, and overall look of a deck?
Deck TemplatesHow do I define one house style with named layouts my slides can reference, like a PowerPoint master?
TroubleshootingSomething is wrong with my deck. What is it, and how do I fix it?
LimitationsWhat can't Narro do, and what do I do instead?