Layers, controls and pages
Three ways to add to a deck without touching its slides: a layer draws on every slide, the
custom-nav-controls layer adds buttons beside the deck’s controls, and a routes file adds
whole pages at addresses of their own. Each is a React component in a file beside deck.mdx, picked
up by kadal-deck dev and kadal-deck build with nothing to register.
Layers on every slide
Section titled “Layers on every slide”Put a component in one of these files, beside deck.mdx or in src/ (.tsx, .jsx, .ts or
.js):
| File | Draws |
|---|---|
global-top.tsx |
once, over the stage’s slides: a logo, a progress line, a watermark |
slide-top.tsx |
with each slide, over its content |
slide-bottom.tsx |
with each slide, under its content |
global-bottom.tsx |
once, under the stage’s slides: a footer that doesn’t move |
A footer with the page number, hidden on the cover:
import type { LayerProps } from "@kadal/deck";
export default function Footer({ page, total, layout }: LayerProps) { if (layout === "cover") return null; return ( <footer className="my-footer"> {page} / {total} </footer> );}.my-footer { position: absolute; right: 48px; bottom: 32px; font-size: 24px; color: var(--deck-muted);}Positions and sizes are in stage pixels: the stage is 1920 × 1080 and scales as a whole.
What a layer knows
Section titled “What a layer knows”A layer gets LayerProps, as props or from useLayer() anywhere inside it:
| Prop | What |
|---|---|
slide |
the slide it is drawn over: the current one for a global layer on the stage, its own for a slide layer |
page, total |
the slide’s number counted from 1 among visible slides, and how many there are |
index, h, v |
the same position counted from 0, and its column and row |
click, clicks |
the click step being shown, and how many the slide has |
layout, frontmatter |
the slide’s layout name and frontmatter, so a footer can hide on covers |
view |
the page the deck is on: play, presenter, print… |
print |
true on a still copy of the slide: a printed page, an exported picture, an overview tile, a thumbnail, the presenter’s next-slide box |
layer |
which layer this is |
On a printed page or a thumbnail a global layer is drawn once per page with that page’s slide, so
read props.slide and props.page, not the navigation.
Where layers show
Section titled “Where layers show”Layers draw in play, in print and in every export. In the presenter view they show unless the headmatter turns them off, and on overview tiles only if it turns them on:
layers: presenter: false # default true overview: true # default falseA layer that throws is dropped with a console message; the slide still draws.
From main.tsx, a theme or an addon
Section titled “From main.tsx, a theme or an addon”<Deck layers> in src/main.tsx sets the same layers in code, and false hides a theme’s or an
addon’s layer of that name:
<Deck manifest={manifest} config={config} layers={{ "global-bottom": Footer, "global-top": false }} />Themes and addons ship layers in their module (layers: { "slide-bottom": Footer }). All of them
draw, stacked from the bottom: the theme’s, then the addons’, then the deck’s own.
Buttons beside the controls: custom-nav-controls
Section titled “Buttons beside the controls: custom-nav-controls”custom-nav-controls.tsx (beside deck.mdx or in src/) adds buttons of your own to the deck’s
controls. In play they sit in a small toolbar at the stage’s bottom-left, faint until hovered or
focused like the arrows; in the presenter view they join the end of its toolbar. They never appear
on a printed page, a tile or a thumbnail.
import { type LayerProps, useActions } from "@kadal/deck";
export default function NavControls({ view }: LayerProps) { const actions = useActions(); return ( <button type="button" title="Overview" onClick={() => actions.toggleOverview()}> ▦{view === "presenter" ? " Overview" : null} </button> );}The component gets the same LayerProps, with view saying whether it is in play or the presenter
view, and acts through useActions() (next, prev, first, last, toggleOverview…) or
useNav(). It follows the controls, not layers:: it shows in play and in the presenter view
whatever that setting says. A theme or an addon can add buttons the same way, under the layer name
custom-nav-controls.
Pages of the deck’s own
Section titled “Pages of the deck’s own”Besides the engine’s pages (/presenter, /notes, /overview…), a deck can add its own: a
handout, a timer for the room, a page of links. A page is drawn inside the deck’s providers, so it
reads the deck with useDeck() like anything else.
Put a routes file beside deck.mdx: routes.tsx (or src/routes.tsx, or setup/routes.ts):
import { defineRoutes, type RoutePageProps, useDeck } from "@kadal/deck";
function Handout(_props: RoutePageProps) { const deck = useDeck(); return ( <main className="handout"> <h1>{deck.config.title}</h1> <ol> {deck.manifest.slides.map((s) => ( <li key={s.index}> <a href={`${deck.base}#/${s.h}/${s.v}`}>{s.title || `Slide ${s.index + 1}`}</a> </li> ))} </ol> </main> );}
export default defineRoutes([ { path: "handout", component: Handout, title: "Handout" }, { path: "q/:id", load: () => import("./src/pages/Question.tsx"), title: "A question" },]);import type { RoutePageProps } from "@kadal/deck";
export default function Question({ params }: RoutePageProps) { return <h1>Question {params.id}</h1>;}The handout opens at /handout and the question page at /q/7, under the deck’s address
(https://decks.kadal.cc/@you/talk/handout once published).
| Route key | What |
|---|---|
path |
under the deck’s base, no leading slash: handout, tools/timer; :name matches one segment and arrives in params; a trailing * matches the rest |
component |
the page |
load |
instead of component, a dynamic import, so the page is a chunk of its own loaded only when opened |
title |
the browser tab’s title while the page is open |
A page gets RoutePageProps: path (the path it was opened at), params and route. The engine’s
own pages always win, so a route named presenter is never used.
defineRoutes also takes a function of the routes so far, defineRoutes((routes) => [...routes, …]), to change or remove what a theme added. <Deck routes={[…]}> in src/main.tsx does the same
from code, a theme’s or addon’s module can export routes, and its setup(api) can call
api.addRoute(route).
On a static host
Section titled “On a static host”kadal-deck build copies index.html into a folder for every plain path the routes file spells out
(dist/handout/index.html), so /handout works on GitHub Pages, S3 or nginx without rewrite rules.
A path with a :param or a * opens through 404.html, which every build writes, on hosts that
serve it for unknown paths. See Build and host anywhere.