Q3 review
Three slides, one Markdown file.
Numbers
| Metric | Q2 | Q3 |
|---|---|---|
| Revenue | 4.1 M | 4.8 M |
| Churn | 3.2% | 2.6% |
Next quarter
- Ship the importer
- Close both enterprise pilots
A deck is built from the same Markdown file the rest of your app renders as a
document. @react-markdown-kit/slides is one plugin with four entries (the
deck, the deck plus present mode, the deck plus the editor commands, and a
PowerPoint writer). The slides run inside the app you already have, so
there's nothing new to deploy and you don't end up keeping a second copy of
the content.
For a file at the end, /pptx writes a .pptx and the stylesheet prints one
page per slide. Marp and Slidev are still the tools for PDF and PNG from a
command line, and the
comparison with Marp, Slidev and reveal.js goes
through which tool fits which job.
npm install @react-markdown-kit/slidesimport Markdown, { defineMarkdownPreset } from '@react-markdown-kit/renderer'
import { slides } from '@react-markdown-kit/slides'
import '@react-markdown-kit/slides/styles.css' // optional: scaling, class hooks, present mode
const preset = defineMarkdownPreset({ extensions: [slides()] })
<div className="rmk-document">
<Markdown preset={preset}>{deck}</Markdown>
</div>You can try it in the slides demo. The deck's
Markdown is on the left, the deck is on the right and Present is in the header
and the control bar. Add
?embed=1 to drop the page chrome,
which is the URL you'd use for an iframe in a blog post or your own docs.
The rest of this page is reference, covering the dialect first, then present mode, then the editor commands.
A deck is a CommonMark or GFM file whose slides are separated by ---
between blank lines, and that's all it needs. GitHub, a diff or an editor
that doesn't know about the plugin will show the same file as a document with
horizontal rules in it. slides() is the whole API for the package. It reads
the parsed tree a second way, and the renderer draws one
<article data-rmk-deck> made of <section>s. Headings, lists, a GFM table
and code inside a slide are the same elements they'd be outside one, drawn by
the renderer's own handlers.
--- front matter at the top of the file sets deck properties. It's read as
flat key: value lines (there's no YAML parser involved):
| Key | Value | Effect |
|---|---|---|
title | text | data-rmk-deck-title; the fallback is the first slide's title |
aspect | 16:9 (default) or 4:3 | data-rmk-deck-aspect, overriding the aspect option |
| a directive key | what the directive takes | The default for every slide: class, background, layout, transition, footer, paginate and incremental. A slide's own directive wins, and class adds to the deck's |
A --- inside a fenced code block, a block quote or a list item isn't at the
root, so it doesn't split anything. *** and ___ are rules inside a slide,
drawn as <hr>. Front matter is detected in the source instead of by the
parser, so a deck that opens with --- followed by content is plain
CommonMark. You get a leading break that doesn't open a slide, and the lists
and quotes after it keep their shape. DIALECT.md, which ships with the
package, has the full reference for every construct.
A comment on its own lines, anywhere in a slide, sets that slide's properties. Each comment holds one directive.
| Directive | Argument | Emitted as |
|---|---|---|
<!-- class: … --> | tokens of [A-Za-z0-9_-], separated by spaces or commas; may repeat and accumulates | data-rmk-slide-class="a b", never class |
<!-- background: … --> | one URL | <img data-rmk-slide-background src alt=""> as the section's first child, so the renderer's URL policy sanitises it like any image |
<!-- name: … --> | [A-Za-z0-9_-]+ | id="slide-<name>" and data-rmk-slide-name, so #slide-numbers deep-links to it |
<!-- layout: … --> | cover, section, center, two-cols, image-left, image-right, quote, fact | data-rmk-slide-layout |
<!-- image: … --> | one URL | <img data-rmk-slide-image>, the picture an image-left or image-right layout puts beside the body |
<!-- transition: … --> | fade, slide, zoom, none | data-rmk-slide-transition; present mode eases the slide in with CSS (off under reduced motion) |
<!-- footer: … --> | one line of text | <footer data-rmk-slide-footer> at the bottom of the slide |
<!-- paginate: … --> | true or false | the slide number in the footer |
<!-- incremental: … --> | true or false | each item of a root-level list is its own step |
<!-- src: … --> | a path | replaced by that file before compiling, with includeDeckFiles(source, read) |
If a known key gets an argument it doesn't accept, or the key isn't known at all, the comment is left as is and a diagnostic is reported. The comment then renders the way any comment renders in the kit, as visible escaped text, so you'll see the typo on the slide instead of it disappearing quietly.
layout arranges one slide, and a paragraph that's exactly ::right::
(Slidev's spelling) starts a second column. The body then holds two
<div data-rmk-slide-column>s. It works with any layout; two-cols only
names the intent. A pause on the left still counts on the right, so a group
that spans the marker is one step.
image-left and image-right take the image directive's picture and give
it half the slide. cover, center and fact centre the body, section
uses the inverse surface for a divider, and quote sets a block quote large.
Every layout is plain CSS on data-rmk-slide-layout, so your own stylesheet
can add more.
includeDeckFiles(source, read) from the root entry splits a deck across
files. It replaces each <!-- src: path --> line outside a fence with what
read(path) returns, drops that file's front matter and follows nested
includes. A src comment that reaches the renderer unexpanded reports
SLIDES_INCLUDE_UNRESOLVED.
A paragraph that's exactly ??? starts the speaker notes, and every block
after it up to the next slide break is part of the notes. A paragraph that's
exactly -- is a pause. The blocks after the k-th pause form fragment group
k, and present mode reveals them one keypress at a time.
Two more things take steps. With <!-- incremental: true --> every item of
a root-level list shows one at a time. A fence with line ranges after its
language, ```ts {1|3-4|all}, highlights those lines and moves the
highlight one step per keypress; GitHub ignores the braces. Steps are
numbered in reading order across pauses, list items and code, and the
static page shows each code block at its first step.
A ??? written on the line right after a paragraph, with no blank line in
between, is a lazy continuation of that paragraph in CommonMark, so the plugin
reports SLIDES_MARKER_ATTACHED instead of trying to guess. A -- in the
same spot behaves differently. Two or more dashes right under a line of text
make that line a setext heading, so the text turns into an <h2> and the
plugin reports SLIDES_SETEXT_HEADING. Either way, adding a blank line
before the marker fixes it.
@react-markdown-kit/slides/present is the same extension under the same
name, plus one renderer component that turns the article from the static deck
into an interactive one. The first render is the static markup and the
controls mount in an effect, so server rendering and hydration see the same
DOM.
'use client'
import { slides } from '@react-markdown-kit/slides/present'
const preset = defineMarkdownPreset({ extensions: [slides({ hashRouting: true })] })j go to the next step, then the next slide; Left, Up,
Page Up, Shift+Space and k go back; Home and End jump; a number then Enter
goes to that slide; o opens the overview (click a slide to go there); p
toggles the presenter view; f toggles full screen where the browser
supports it; b or . blacks the screen out; d draws on the slide, l
shows a laser dot and x clears the drawing; ? or h lists all of them.
Escape closes whatever is open first, then exits. Keys with a modifier, or
typed into an input, are ignored.hidden lifted at a size you set with + and -, an elapsed
timer that t resets, and the clock. If you open the deck in a second
window with sync: 'my-deck', both windows follow the same
BroadcastChannel, and c opens that second window for you. That's how
you'd put the notes on the laptop and the slides on the projector. For
anything past one browser, pass a SyncTransport (an object with post
and subscribe) as sync instead of a name, over a WebSocket for example.hashRouting: true. #3, #3.2 (slide 3 with two
steps shown), #name and #slide-name open present mode there on load, and
the hash follows the deck while presenting. It's off on this page because
the docs site needs its own hashes.createDeckController() passed as controller lets the
rest of your app drive the deck (next, goto, enter, exit) and read
where it is with useDeck(controller).follow: true a presenting deck that sits on its last
slide jumps to the newest slide when more arrive, which is what you want
while a model writes the deck. A comment or front matter still being written at the
end renders as nothing until it's complete.initialMode: 'present' or 'presenter' opens in that mode;
controls: false removes the bar, and a component there replaces it (it
gets the state, the labels and a run(command) function); labels
replaces every string (SLIDES_LABELS has the defaults), including what a
screen reader hears on each move; onSlideChange(index) reports the
zero-based slide.There's no standalone slideshow component. You get the deck through
<Markdown> and a preset, same as every other feature of the kit, so one
Markdown file can render as a document, a deck or a presentation depending on
which preset it's given.
The optional stylesheet handles scaling with CSS only. Each <section> is a size
container with a fixed aspect ratio, and the slide body sets its font size
in cqw: 1.72cqw is 22px on a 1280px-wide slide, and the renderer's
em-based headings, lists and code follow along. A browser without container
units gets a fixed size. The examples on this page bump that token up in the
stack so the half-width thumbnails stay readable, and present mode uses the
default.
| Token | Default |
|---|---|
--rmk-slide-surface / --rmk-slide-text | #fff / #1e1e1e |
--rmk-slide-inverse-surface / --rmk-slide-inverse-text | #1e1e1e / #fff |
--rmk-slide-font-size | 1.72cqw |
--rmk-slide-padding | 3.75cqw |
--rmk-slide-radius / --rmk-slide-shadow / --rmk-slide-gap | var(--rmk-radius) / none / var(--rmk-space) |
--rmk-deck-backdrop / --rmk-deck-chrome / --rmk-deck-chrome-text | #111 / #222 / #fff, present mode |
--rmk-deck-accent / --rmk-deck-ink | #4f8cff / #e5484d: progress bar and pressed buttons / drawing and laser |
--rmk-deck-blackout / --rmk-deck-scrim | #000 / rgba(0, 0, 0, 0.72) |
--rmk-deck-transition-duration | 0.35s |
The classNames prop of <Markdown> gets three more parts, deck, slide
and slideNotes, if you use utility classes. Everything else is a data
attribute on a plain element, so your own stylesheet doesn't need any class
from the kit.
When printing, every section gets break-after: page on a page of its own
aspect (16 by 9 inches, or 12 by 9 for 4:3, with no margin) and the
controls are hidden. An @page rule of your own wins if you need another
size. With printSteps: true on the present entry, a slide with steps
prints once per step before the finished slide.
@react-markdown-kit/slides/pptx writes a deck as a .pptx with
pptxgenjs, an optional peer
dependency that's loaded on the first call.
import { compileMarkdown } from '@react-markdown-kit/renderer'
import { deckToPptx } from '@react-markdown-kit/slides/pptx'
const blob = await deckToPptx(compileMarkdown(deck, { preset }))Each slide's first heading becomes its title. Paragraphs, lists, code,
quotes, tables and pictures go in the body, split per column for
::right:: and beside the picture for the image layouts, and the footer,
slide number, background and notes carry over. Every fragment and code step
is shown, since a file has no clicks, and a Mermaid diagram comes out as its
source. Pass output: 'nodebuffer' on a server and theme for fonts and
colours. Pictures are downloaded first, each with a time limit, and one
that doesn't arrive is left out instead of failing the export. URLs go
through resolveUrl, which by default allows web and data:image pictures
and web and mail links only. That matters on a server, where a file path
would otherwise be read from disk.
Content problems don't throw. Each one becomes a diagnostic on the compiled
document with the node's source range, and all the codes are exported as
SLIDES_DIAGNOSTIC_CODES.
| Code | Severity | When |
|---|---|---|
SLIDES_SETEXT_HEADING | info | A depth-2 heading made by dashes right under text; a blank line before --- makes it a break |
SLIDES_SLIDE_EMPTY | info | A slide with no content blocks; still rendered, because an author who just typed the break must see it |
SLIDES_FRONT_MATTER_INVALID | warning | --- then key: value lines at the top with no closing --- line, or a line that is neither blank nor key: value before it |
SLIDES_DIRECTIVE_INVALID | warning | A known directive or front matter key with a rejected value |
SLIDES_DIRECTIVE_UNKNOWN | warning | <!-- key: value --> with a key that is not a directive |
SLIDES_NAME_DUPLICATE | warning | Two slides with the same name; the later one gets no id |
SLIDES_MARKER_MISPLACED | warning | A second ??? or ::right::, or a -- or ::right:: after ???; ignored |
SLIDES_MARKER_ATTACHED | warning | A marker glued to the paragraph above |
SLIDES_PROPERTY_BARE | info | A slide opening with bare key: value lines, remark's syntax, which this dialect does not read |
SLIDES_INCLUDE_UNRESOLVED | warning | A <!-- src: … --> that reached the renderer without includeDeckFiles |
SLIDES_CODE_STEPS_INVALID | warning | {…} after a fence's language that isn't line ranges; the code shows without highlights |
<section data-rmk-slide="2" data-rmk-slide-title="Numbers" aria-roledescription="slide" aria-label="Numbers" id="slide-numbers" data-rmk-slide-name="numbers" data-rmk-slide-class="center middle" data-rmk-slide-fragments="2"><img data-rmk-slide-background="" src="https://example.com/bg.jpg" alt=""/><div data-rmk-slide-body=""><h2>Numbers</h2><p>Intro.</p><div data-rmk-fragment="1"><p>One.</p></div><div data-rmk-fragment="2"><p>Two.</p></div></div><aside data-rmk-slide-notes="" hidden=""><p>say this</p></aside></section>slide-<name>. Optional attributes only show up when set.
extension.test.tsx checks
the markup for a three-slide deck character for character, attribute order included,
and the block above is slide 2 of it, copied from that expectation.root, so it works in a server component and in static rendering. The
decks on this page were rendered at site build time, and
present.dom.test.tsx
renders the present entry on the server and hydrates it without a mismatch.aria-roledescription="slide"
and an aria-label from its first heading, falling back to slideLabel and the
slide number (Slide 3).javascript: background loses its src.@react-markdown-kit/slides/editor adds authoring. It's the present entry
plus an editor capability under the same name, so it replaces the
renderer's slides() in a preset.
import { MarkdownEditor } from '@react-markdown-kit/editor'
import { slides } from '@react-markdown-kit/slides/editor'
const preset = defineMarkdownPreset({ extensions: [gfm(), slides()] })
<MarkdownEditor preset={preset} value={value} onChange={setValue} />slides group: New slide,
Speaker notes, Pause and Background. The first three insert
---, ??? and --. Background inserts a directive chip, and you type
its value into an inline field.---, ***, --, ??? or ::right:: on a line of its own and pressing Enter
turns the paragraph into the matching node.<!-- key: value --> directive shows up as a chip. Click its value to
edit it in place. Enter commits and Escape cancels, and if the directive
rejects the value it's marked invalid and doesn't get written..rmk-document,
so the example above gives the preview surface that class with
classNames={{ preview: 'rmk-preview rmk-document' }}.labels prop relabels the toolbar buttons by id
(slide, slideNotes, slidePause, slideBackground) and the node
captions under slides.notes, slides.pause, slides.slideBreak and
slides.rule. slides({ editorLabels }) sets only the node strings per
preset (notes, pause, slideBreak, rule, directive(key),
directiveValue). It can't rename a toolbar button, so button captions
always go through labels.The built-in horizontal-rule button still inserts the editor's own rule node,
which stays unlabelled until the document is loaded again. For a deck editor
you'll probably want to hide it through the toolbar render prop.
With @react-markdown-kit/variables, a {{placeholder}} in slide prose
resolves like it does anywhere else. Markers and directives stay literal, so a
placeholder inside <!-- background: … --> is never treated as data and
runtime values can't set a slide's properties. List slides() before
variables().
| Entry | Loads | Adds |
|---|---|---|
@react-markdown-kit/slides | nothing beyond the parser (packaging.test.ts) | the extension: dialect, diagnostics, the static deck, serialization |
@react-markdown-kit/slides/present | React | a client article component: present mode, keyboard and pointer navigation, fragments, presenter view, overview, drawing, deep links, sync |
@react-markdown-kit/slides/editor | React and Lexical | the present entry plus editor nodes, the Enter shortcut and the four toolbar commands |
@react-markdown-kit/slides/pptx | pptxgenjs, on the first call | deckToPptx, the PowerPoint writer |
The first three return an extension named slides, so a later entry replaces an
earlier one in a preset. A Node service that renders decks to HTML, or
resolves variables in them, can import the root entry and skip installing
React and Lexical.
The deck is always rendered through the plugin. slides(options?) takes
aspect ('16:9' or '4:3'), notes, frontMatter and slideLabel;
/present adds hashRouting, initialMode, controls, sync, labels,
onSlideChange, controller, follow, printSteps and cloneUrl;
/editor adds editorLabels, the node captions only (toolbar buttons are
relabelled through the editor's labels prop). Around it, the root entry
has includeDeckFiles, /present has createDeckController, useDeck,
the built-in Controls, DECK_COMMANDS and broadcastTransport, and
/pptx has deckToPptx. The node type constants, type guards and
diagnostic codes are exported in case you have tooling that walks a compiled
document.
Those three are presentation tools first: Marp and Slidev export PDF, PNG and PPTX from a command line, and reveal.js prints to PDF from the browser. This plugin renders a deck inside a React app from Markdown the app already renders, with present mode and speaker notes, and writes PPTX from the browser or a server. The comparison page puts the four side by side and maps the Marp directives onto this dialect.
The slides demo · The demo in embed mode · Marp and Slidev alternative · @react-markdown-kit/slides on npm · The editor · Styling · Security model
Last updated on