Slidewright

@slidewright/react

Renders Markdown decks in React. Give it a string, and it shows a presentation:

  • one slide at a time, scaled to fit its container
  • steps, and code with syntax colours
  • navigation with the keyboard and with touch
  • an overview of all the slides, and fullscreen
  • a presenter view with notes and a timer, on the page or in a second window.

The deck follows its markdown prop. When you edit the source, the deck renders again and stays on the same slide and step. So the deck works in live editors, CMS previews and docs sites, and also to present.

The deck syntax reference tells the deck format.

Usage

import { Deck } from "@slidewright/react";
import "@slidewright/react/styles.css";

export function Talk({ markdown }: { markdown: string }) {
  return <Deck markdown={markdown} />;
}

The deck fills the width of its container. Its height comes from the aspect ratio of the deck. To fill a box of fixed size, also give the deck a height, with className or style. The slide then scales to fit, with bars around it.

You need React 19. The package has "use client", and renders on the server. Code gets its colours and maths gets drawn after hydration.

While a slide shows, the deck gets the next three slides ready, and the slide before it. It loads and decodes their images, draws their diagrams, and loads what colours their code and draws their maths. So a slide shows complete when the deck gets to it. The deck keeps the images that it has loaded, so a slide that you go back to shows its images at once.

Props

PropDefaultDescription
markdownThe source of the deck.
position{ slide, step } for a controlled deck. Use it with onPositionChange.
defaultPosition{0, 0}The start position of an uncontrolled deck.
onPositionChangeGets the new position after each move.
hashfalseKeeps the position in the URL hash. See URL hash.
layoutsMore layouts, by name. A layout with a built-in name replaces the built-in layout.
componentsComponents for :::name and ::name directives, by name.
mermaidLoads Mermaid, to draw mermaid code blocks: () => import("mermaid"). See Diagrams.
iconsIcon sets for :set:name: icons, in the Iconify format. See Icons.
compileOptionsSanitising and remark/rehype plugins, for the compiler. Do not make a new object on each render.
colorSchemelight, dark or auto. Replaces the value of the headmatter.
keyboard"focus""focus": keys work when the deck has the focus. "global": keys work on all the page. false: keys don't work.
swipetrueSwipe left and right on touch screens to move.
presentertrueP and a button open the presenter view in a second window. See Presenter window.
controlstrueThe buttons (previous, next, overview, presenter, fullscreen), the slide counter and the progress bar.
classNameA class on the root element of the deck.
styleA style on the root element of the deck.
refA DeckHandle: next(), prev(), goTo(slide, step?), focus(), toggleOverview(), toggleFullscreen(), togglePresenter().

Positions start at 0. The deck changes a position outside the deck to the nearest slide. When you remove slides during an edit, an uncontrolled deck shows the last slide that is still there. When the slides come back, the deck goes back to its position.

Controlled position

Keep the position in your state to sync it with the URL, a presenter window or the cursor of an editor:

const [position, setPosition] = useState({ slide: 0, step: 0 });

<Deck markdown={markdown} position={position} onPositionChange={setPosition} />;

Keyboard

KeysAction
→ PageDown SpaceNext step, then next slide
← PageUp Shift+SpacePrevious step, then previous slide at its end
↓ / ↑Next / previous slide, without the steps
Home / EndFirst / last slide
A number, then EnterGo to that slide
OOpen or close the overview
FGo into or out of fullscreen
POpen or close the presenter window

When you type a number, the counter shows it. Backspace corrects the number, and Escape cancels it. The deck ignores keys that you type in inputs, text areas and editable content on a slide.

On touch screens, swipe left for the next step and right for the previous step. Vertical swipes scroll the page.

After each move, screen readers say the slide number and the title, such as "Slide 2 of 8: Results". When the system asks for reduced motion, steps show immediately and nothing moves. Only the controls fade in.

The overview shows all the slides in a grid, with all their steps. It starts at the current slide. To go to a slide, move with the arrow keys and press Enter, or click the slide. Escape or O closes the overview and stays on the current slide. In fullscreen, the browser uses Escape to leave fullscreen, so use O.

The fullscreen button shows only when the browser lets a page go fullscreen. iPhones let only videos go fullscreen. In an iframe, the iframe needs allow="fullscreen". In fullscreen, the controls show only when the pointer moves to them.

URL hash

With hash, the URL follows the deck. #3 is slide 3, and #3.2 is slide 3 with two steps shown.

  • When you load the page again or open a shared link, the deck starts at that position.
  • When the hash changes, the deck moves. You can change the hash by hand, or with a link on a slide such as [demo](#5).
  • A controlled deck gets the position of the hash through onPositionChange.
  • When the Markdown comes later, such as from a fetch, give the deck an empty string until then. The deck opens on the slide of the hash when the slides come.

The deck replaces the history entry when it moves. So the Back button leaves the page, and doesn't go back through the talk. The hash is for all the page, so use hash on one deck only. The anchors of the page, such as #install, stay until the deck moves.

Presenter view

Presenter is the view of the speaker. It shows:

  • the current slide
  • a preview of what the next key press shows
  • the notes for the current step
  • a timer.

Give it the same position as the deck of the audience. Then the two move together:

import { Deck, Presenter } from "@slidewright/react";

const [position, setPosition] = useState({ slide: 0, step: 0 });

<Deck markdown={markdown} position={position} onPositionChange={setPosition} />;
<Presenter
  markdown={markdown}
  position={position}
  onPositionChange={setPosition}
/>;

It takes the props of the deck for the source, the position, the URL hash, layouts, components, compiling, the colour scheme, the keyboard and the styling. It also takes the same keys, but not O, F and P. These keys stay with the deck.

The preview shows the next step of the current slide. After the last step, it shows the next slide. When [step] lines divide the notes, the notes show in parts. See Notes for each step. The part for the current step has a mark, and scrolls into view. The parts before it are dimmed. The timer starts when the presenter mounts. You can pause it and set it back to zero.

The presenter fills the height of its container. On a page of its own, give it the height of the window, and let it take keys from all the page:

<Presenter markdown={markdown} keyboard="global" style={{ height: "100dvh" }} />

Presenter window

To open the presenter view in a second window, press P on a deck, or use its presenter button. Put the window on a second screen. The deck and the window move together. The keys that move, such as →, or a number and then Enter, work in the two windows. O, F and P work on the deck only. To close the window, press P or use the button again. The window also closes with the page.

The window copies these from the page, and follows their changes:

  • the <link rel="stylesheet"> and <style> elements in the head of the page
  • the attributes of the <html> element, such as a dark mode class.

The presenter in the window gets the className of the deck, and the custom properties in its style, such as --deck-accent. So it has the theme of the deck. It doesn't get the other properties of style, because it fills the window.

The window doesn't copy:

  • <style> elements in the body of the page
  • rules that a script adds with insertRule. Some CSS-in-JS libraries add rules in this way in production.

The presenter in the window is not in the parent elements of the deck. So a rule that selects the deck through a parent element, such as .page [data-deck], doesn't apply in the window.

Browsers open windows only after a click or a key press. A popup blocker can also stop the window. presenter={false} removes the key and the button. togglePresenter() on the ref of the deck still opens the window, so you can use a button of your own.

Printing

PrintDeck renders all the slides of a deck at full size, one after the other, to print and to export. Each slide prints on its own page, with the size of the slide. All the steps show, and code shows its last highlight stage:

import { PrintDeck } from "@slidewright/react";

<PrintDeck markdown={markdown} />;

With steps, it prints a page for each step, not for each slide. It also takes these props of the deck: layouts, components, mermaid, icons, compileOptions, colorScheme, className and style.

On the screen, the pages show one below the other, with a gap and a shadow, as a preview. In print, each page is on a sheet of its own. The component sets the size of the printed page to the size of the slide. So put it on a page of its own, without other content to print.

Code colours and maths load after the first render. A code block has aria-busy="true" until its colours are ready. Maths has it until it is drawn. So a script that prints or captures the pages can wait until no element has [aria-busy="true"].

Layouts

A slide sets its layout with layout: in its frontmatter. The built-in layouts are:

LayoutArrangement
defaultContent from the top left.
centerContent in the centre.
coverA title slide: a large heading and a subtitle.
sectionThe start of a section.
statementOne large heading, in the centre.
factA large number or word in the accent colour, and a caption.
quoteA large > quote. The text after it is the source.
fullNo padding.
two-colsContent at the top, then the :::left and :::right columns.
imageimage: fills the slide. The content is at the bottom.
image-left, image-rightContent next to image:.

The image layouts read the URL of the image from image:, and its description from imageAlt:. Without imageAlt:, the image is decoration, and screen readers skip it.

---
layout: quote
---

> Simplicity is prerequisite for reliability.

Edsger W. Dijkstra
---
layout: image
image: /photos/harbour.jpg
imageAlt: Fishing boats in a harbour at dawn
---

# Where we started

Custom layouts

A custom layout is a component. Give it a name in the layouts prop. It gets the content of the slide as children. Its slots list names container directives. The layout gets these directives in slots, not in children, when they are at the top level of the slide:

import type { LayoutProps } from "@slidewright/react";

function Sidebar({ children, slots }: LayoutProps) {
  return (
    <>
      <div data-part="main">{children}</div>
      <aside data-part="aside">{slots.aside}</aside>
    </>
  );
}
Sidebar.slots = ["aside"];

<Deck markdown={markdown} layouts={{ sidebar: Sidebar }} />;
---
layout: sidebar
---

# Release plan

- Beta in May
- Launch in June

:::aside
Owned by the platform team
:::

The layout renders in the <section data-layout="sidebar"> of the slide. So CSS can arrange it by name:

[data-layout="sidebar"] {
  display: grid;
  grid-template-columns: 2fr 1fr;
  gap: 2em;
}
  • slide.frontmatter has the settings of the slide. So a layout can read its own settings, as the image layouts read image:.
  • A layout with a built-in name replaces the built-in layout in the deck, the overview and the presenter view.
  • A layout name without a component renders as default, and keeps the name in data-layout. So a layout that only changes the look needs only CSS:
[data-layout="agenda"] :where(ol) {
  font-size: 1.3em;
}

Components

Give components for directives in the components prop. A component gets the attributes of the directive as string props, and its content as children:

function Callout({
  tone = "info",
  children,
}: {
  tone?: string;
  children?: ReactNode;
}) {
  return <aside className={`callout ${tone}`}>{children}</aside>;
}

<Deck markdown={markdown} components={{ callout: Callout }} />;
:::callout{tone="warning"}
Mind the **gap**.
:::

When sanitising is on, which is the default, a component doesn't get event handlers (on…), srcdoc, or javascript: and vbscript: URLs. The other attributes are text that the author of the deck wrote. Use them as text only.

Each directive renders as a div with data-directive="name". The div keeps the id, the classes and the step of the directive. A component renders in that div. Without a component, the content goes directly in the div, so you can style a directive with CSS only. When a component throws, only its slide breaks.

Diagrams

The deck draws a mermaid code block as a diagram when it can load Mermaid. Mermaid is a large package, so @slidewright/react doesn't depend on it. Install it:

npm install mermaid

Then give the deck a function that loads it:

// Outside the component, or the deck gets a new function on each render.
const loadMermaid = () => import("mermaid");

<Deck markdown={markdown} mermaid={loadMermaid} />;

Mermaid loads in its own chunk, three slides before the first diagram. Without the mermaid prop, the block stays a code block. Presenter and PrintDeck take the same prop.

The deck draws a diagram in the colours and the font of its slide: its --deck-bg, --deck-fg, --deck-accent and the other colour properties. When these change, for example with the colour scheme, the deck draws the diagram again. The config of the diagram, in the frontmatter or a directive of its source, can set other themeVariables.

A diagram has the size that Mermaid gives it. It is not wider than its container. At the top level of a slide and in the columns of two-cols, it gets smaller when the slide has no more room. To set a size, use CSS:

[data-diagram] > svg {
  max-height: 300px;
}

The source shows until the diagram is drawn, and on the server. When the source has a mistake, the source stays, with the message of Mermaid below it. Mermaid runs with securityLevel: "strict". So labels can't have scripts, and diagrams have no click handlers.

Icons

:set:name: in the text of a deck is an icon: :lucide:rocket: is the rocket of the set lucide. The deck draws the icons of the sets in its icons prop. The sets use the Iconify format. So the icons.json of all @iconify-json/* packages works. Install a set:

npm install @iconify-json/lucide

Then give it to the deck:

import lucide from "@iconify-json/lucide/icons.json";

<Deck markdown={markdown} icons={[lucide]} />;

Find the sets and the names of their icons at icon-sets.iconify.design. A set has thousands of icons. To keep a set out of the first script of the page, load it with import(). Give it to the deck when it comes. Until then, the deck shows the source text. It also shows the source text for an icon that no set has. Presenter and PrintDeck take the same prop. With the Vite plugin or the CLI, the page gets only the icons that the deck uses. See Icons in the README of the plugin.

To add your own icons, make a set with the same shape. A body is the content of the <svg> of the icon:

const brand: IconSet = {
  prefix: "brand",
  width: 24,
  height: 24,
  icons: { logo: { body: '<path fill="currentColor" d="M3 3h18v18H3z"/>' } },
};

<Deck markdown=":brand:logo: Acme" icons={[lucide, brand]} />;

The deck puts a body in the page with no change. So use only sets that you trust.

An icon is an <svg data-icon="set:name">. It is as tall as the text around it. When the set draws with currentColor, the icon has the colour of the text. To change the size and the colour, style the element around the icon, or select the icon:

[data-slide] h1 svg[data-icon] {
  color: var(--deck-accent);
}

Screen readers skip icons. To give an icon a name, write its element with a title: <span data-icon="lucide:rocket" title="Launch"></span>.

Transitions

A slide with transition: in its frontmatter comes in with an animation. The theme has fade, slide, slide-up and zoom. The syntax reference tells what each one does. <Deck> plays them. <Presenter> and <PrintDeck> change slides immediately.

During a move, the canvas holds two slides: the slide that goes out, then the slide that comes in. Each slide has these attributes until the animations of the two slides end:

AttributeValue
data-transitionThe name, from the later of the two slides
data-transition-stateentering or leaving
data-transition-directionforward, or backward to an earlier slide

The slide that goes out doesn't change. It stays at its step, and its components keep their state. It is inert, and screen readers don't see it.

The theme properties --deck-transition-duration and --deck-transition-easing set the speed of all transitions.

Your own transitions

A name that the theme doesn't have does nothing until your CSS gives an animation to each of the two slides:

---
transition: turn
---
[data-slide][data-transition="turn"][data-transition-state="entering"] {
  animation-name: turn-in;
}

[data-slide][data-transition="turn"][data-transition-state="leaving"] {
  animation-name: turn-out;
}

@keyframes turn-in {
  from {
    opacity: 0;
    rotate: calc(12deg * var(--deck-transition-direction));
  }
}

@keyframes turn-out {
  to {
    opacity: 0;
    rotate: calc(-12deg * var(--deck-transition-direction));
  }
}
  • The theme gives the two slides the duration, the easing and animation-fill-mode: both. To change the speed of one transition, set animation-duration.
  • --deck-transition-direction is 1 towards a later slide and -1 towards an earlier slide. So one pair of animations can play in the two directions.
  • An animation for only one of the slides is sufficient. The other slide waits. The deck removes the slide that goes out when the two slides have no more animations. An animation that repeats with no end doesn't count.
  • With reduced motion, the theme gives the animations no duration. So the slides change immediately.

Styling

styles.css has the deck controls, the built-in layouts, the default theme and the built-in themes. All of them are in the slidewright cascade layer. A rule in your own CSS that is not in a layer has priority over them.

For maths, styles.css imports the stylesheet and the fonts of KaTeX (katex/dist/katex.min.css) into the same layer. Your bundler finds that import in node_modules. KaTeX loads with the first slide that has maths.

This rule also applies to resets. A global * { margin: 0; padding: 0 } that is not in a layer removes the padding and the spacing of the slides. So put resets in a layer that comes before slidewright. With Tailwind, give the sequence of the layers before you import Tailwind. Then the preflight reset comes first, and the utilities still have priority:

@layer theme, base, slidewright, components, utilities;
@import "tailwindcss";

To give a deck a theme, set custom properties on [data-deck], or on a class that you give to the deck:

.my-deck {
  --deck-bg: #0b1020;
  --deck-fg: #f3f4f8;
  --deck-accent: #ffb000;
  --deck-font-sans: "Inter", sans-serif;
  --deck-font-size: 26px;
}
<Deck markdown={markdown} className="my-deck" />

Don't set them on a parent element of the deck. The default theme sets them on [data-deck], so the deck doesn't get the values of its parent.

PropertyControls
--deck-bg, --deck-fg, --deck-mutedThe background, the text and the second text
--deck-accentLinks, the focus ring, progress and quotes
--deck-border, --deck-surfaceRules and tables; code backgrounds
--deck-code-foregroundCode text (--deck-fg by default)
--deck-code-backgroundCode blocks (--deck-surface by default)
--deck-backdropThe bars around the slide
--deck-font-sans, --deck-font-headingThe body and heading fonts
--deck-font-monoThe code font
--deck-font-size, --deck-line-heightThe base text size on the canvas
--deck-padding, --deck-radiusThe slide padding and the corner radius
--deck-step-durationThe step animation (0 with reduced motion)
--deck-transition-durationThe transition between slides (400ms)
--deck-transition-easingThe timing function of the transition
--deck-dim-opacityThe opacity of the code lines not highlighted
--deck-notes-font-sizeThe notes text in the presenter view
--deck-code-token-*Syntax colours (keyword, string, …)
--deck-code-added, --deck-code-removedAdded and removed lines in diffs

The colours use light-dark(). So a theme can give the colours of the two schemes in one value.

Themes

styles.css has four themes in addition to the default theme. To use one, set theme in the headmatter:

---
theme: paper
---
ThemeLookFor
defaultNeutral greys and indigoAll decks
paperWarm paper and ink, serif headingsTalks that tell a story
frostCool greys and teal, rounded corners, a glow on title slidesTechnical talks
contrastBlack and white, bold headings, a contrast of 7:1 or moreBright rooms, and people with low vision
vividPink and violet, narrow headings, section slides in colourKeynotes

Each theme has colours for light and for dark. In each theme, text has a contrast of 4.5:1 or more on its background, and the main text has 7:1 or more. A theme doesn't change the size of the text or the padding, so a deck that fits with one theme fits with all of them. The fonts are fonts that most computers have, so a deck doesn't download fonts. If a computer doesn't have the first font of the list, the next font shows.

A name that is not a built-in theme gives the default theme. To make a theme with a name of your own, put its properties on [data-deck][data-theme="name"]. Then theme: name selects it:

[data-deck][data-theme="forest"] {
  --deck-bg: light-dark(#f5f6ef, #121610);
  --deck-fg: light-dark(#1c2419, #e3e8dc);
  --deck-accent: light-dark(#2d6a35, #8fcf7e);
  --deck-font-heading: Rockwell, "Roboto Slab", Georgia, serif;
}

[data-deck][data-theme="forest"] [data-layout="section"] {
  background: var(--deck-surface);
}

Your rules win over a built-in theme. So a rule on [data-deck] that sets one property changes that property in each theme. The theme keeps its other properties.

Selectors

The content of a slide is usual HTML, and usual selectors style it. The renderer adds data attributes for all other parts:

SelectorElement
[data-deck]The root. data-theme, data-color-scheme
[data-slide]A slide. data-layout, and the class: value
[data-transition]A slide in a transition
[data-part]Layout parts (content, image, left…)
[data-slot]Content in a layout slot
[data-directive]A directive, by name
[data-step-state]Step content: future, current or past
[data-code]A code block figure. aria-busy while it loads
[data-line-state]A code line: highlighted or dimmed
[data-line-diff]A code line: added or removed
[data-diff-marker]The + or - before a diff line
[data-math]Maths: inline or display
[data-diagram]A diagram figure. aria-busy until it is drawn
svg[data-icon]An icon, by set:name
[data-deck-controls]The buttons and the slide counter
[data-deck-progress]The progress bar
[data-deck-overview]The overview grid
[data-deck-thumbnail]A slide in the overview. data-current
[data-presenter]The presenter root, with data-deck
[data-presenter-note]A part of the notes. With [step], data-note-state
[data-deck-print]The PrintDeck root, with data-deck
[data-deck-page]A printed page. data-page-slide, -step
[data-slide-error]A slide that didn't compile or render

The thumbnails in the overview are also slides. So slide CSS styles them in the same way. To style only the slide that you present, put [data-deck-viewport] in the selector. The current slide of the presenter is also in a [data-deck-viewport].

data-page-slide starts at 0, as positions do. data-page-step is there only with steps.

The slide canvas has a fixed size: canvasWidth in the headmatter, 980px by default. The deck scales it to fit. So sizes in slide CSS are canvas pixels, and look the same on all screen sizes.

Types

TypeWhat it is
DeckPropsThe props of Deck. See Props.
DeckHandleThe ref of Deck: next(), goTo(slide, step?), togglePresenter(), …
DeckPosition{ slide, step }. slide starts at 0, and step 0 is before the first step.
PresenterPropsThe props of Presenter: the props of the deck that it takes.
PrintDeckPropsThe props of PrintDeck. See Printing.
LayoutA layout component, with its optional slots list.
LayoutPropsThe props that a layout gets: slide, children and slots.
DirectiveComponentsThe components prop: components by directive name.
MermaidLoaderThe mermaid prop: a function that loads Mermaid. See Diagrams.
IconSetA set of the icons prop: icons in the Iconify format. See Icons.

On this page