An unholy mixture of Astro, Svelte, Animotion (and therefore Reveal.js), with a bit of Marp syntax mixed in --- for markdown-authored slide decks in Astro sites.
This is shared in the spirit of openness and bonhomie, but it's really quite idiosyncratic, and I don't expect anyone apart from me is going to find it useful.
npm install github:benswift/astromotion
You also need these in your project:
npm install astro @astrojs/svelte svelte @animotion/core @tailwindcss/vite
In your astro.config.mjs:
import { defineConfig } from "astro/config";
import svelte from "@astrojs/svelte";
import tailwindcss from "@tailwindcss/vite";
import { astromotion, deckPreprocessor } from "astromotion";
export default defineConfig({
integrations: [svelte({ preprocess: [deckPreprocessor()] }), astromotion()],
vite: {
plugins: [tailwindcss()],
},
});
The integration handles:
/decks/[...slug] route$app/environment for Animotion's SvelteKit shimCreate .deck.svelte files in src/decks/<slug>/:
src/decks/
my-talk/
slides.deck.svelte -> /decks/my-talk/
bonus.deck.svelte -> /decks/my-talk/bonus/
assets/
photo.jpg
A file named slides.deck.svelte maps to the folder root URL. Other names
become sub-paths.
Slides are markdown separated by --- (thematic breaks). The preprocessor
converts them into Animotion <Presentation> and <Slide> components at build
time.
---
title: My Talk
description: A talk about things
---
# Title slide
## Subtitle
---
## Second slide
Regular markdown content --- paragraphs, lists, code blocks, images.
---
<!-- _class: impact -->
**Big statement slide**
Borrowed from Marp syntax:
<!-- _class: impact --> --- set slide CSS class (impact, banner,
quote, centered)<!-- notes: Speaker notes here --> --- presenter notes (visible in Reveal.js
speaker view)Also Marp-inspired:
 --- full-bleed background /  --- sizing /  --- split layout --- CSS filtersRelative paths (./, ../) are resolved as Vite imports. Absolute paths
(/images/...) reference public/.

Generates an animated SVG QR code linking to the URL.
<!-- _class: anu-logo -->
<!-- _class: socy-logo -->
Fenced code blocks are rendered using Animotion's <Code> component with syntax
highlighting.
Sections containing <Action>, <Code>, <Transition>, or other Animotion
components skip markdown processing and pass through as raw Svelte. You can mix
markdown and interactive components freely.
<script> and <style> blocks are preserved. Animotion component imports are
auto-added if missing.
The default theme (theme/default.css) provides only the structural CSS needed
for backgrounds, split layouts, QR codes, and logo slides to render correctly.
All visual styling --- colours, typography, slide classes like impact and
banner --- is your responsibility.
Create a CSS file in your project (e.g. src/decks/theme.css) and pass it to
the integration:
astromotion({ theme: "./src/decks/theme.css" });
Your theme should start with these imports (the structural defaults layer underneath):
@import "tailwindcss" source(none);
@source "./";
@import "@animotion/core/theme";
Then add your own styles. At a minimum you'll want to set:
--r-background-color, --r-main-color,
--r-main-font, --r-main-font-size, --r-heading-color, --r-heading-font,
--r-link-color.reveal .slides sectionbanner, impact, quote,
centered, and columns (these are the classes available via
<!-- _class: ... --> directives)The theme CSS should only reference fonts (via font-family), not load
them. Use Astro's built-in font system in your astro.config.mjs to handle
font loading:
export default defineConfig({
fonts: [
{
name: "Your Font",
cssVariable: "--font-your-font",
provider: fontProviders.google(),
},
],
});
These classes are generated by the preprocessor and styled by the default theme. Your custom theme layers on top of them:
| Class | Purpose |
|---|---|
.slide-bg |
Full-bleed background image (absolute positioned) |
.split-layout |
Flex wrapper for split image/content slides |
.split-image |
Image panel in split layout (width set inline) |
.split-content |
Content panel in split layout |
.logo-svg |
SVG container for logo slides |
.qr-code |
Container for generated QR code SVGs |
.columns |
Two-column grid layout within slide content |
astromotion({
theme: "./src/my-theme.css", // custom theme CSS path (default: built-in)
injectRoutes: true, // inject /decks/[...slug] route (default: true)
});
If you set injectRoutes: false, you'll need to create your own route pages.
See pages/[...slug].astro in this package for the reference implementation.
The integration doesn't inject a listing page since it would need your site's
layout. Create your own at src/pages/decks/index.astro:
---
import YourLayout from "../../layouts/YourLayout.astro";
import { parseDeckFrontmatter } from "astromotion";
import fs from "node:fs";
import path from "node:path";
const decksDir = path.resolve("src/decks");
const decks = [];
for (const dir of fs.readdirSync(decksDir, { withFileTypes: true })) {
if (!dir.isDirectory()) continue;
for (const file of fs.readdirSync(path.join(decksDir, dir.name))) {
const match = file.match(/^(.+)\.deck\.svelte$/);
if (!match) continue;
const raw = fs.readFileSync(path.join(decksDir, dir.name, file), "utf-8");
const { data } = parseDeckFrontmatter(raw, dir.name);
const slug = match[1] === "slides" ? dir.name : `${dir.name}/${match[1]}`;
decks.push({ slug, title: data.title ?? slug, description: data.description });
}
}
decks.sort((a, b) => a.slug.localeCompare(b.slug));
---
<YourLayout title="Decks">
<h1>Decks</h1>
<ul>
{decks.map((deck) => (
<li>
<a href={`/decks/${deck.slug}/`}>{deck.title}</a>
{deck.description && <span> --- {deck.description}</span>}
</li>
))}
</ul>
</YourLayout>
Requires decktape:
npx decktape reveal --size 1280x720 http://localhost:4321/decks/my-talk/ output.pdf
Or use the bundled script (builds, starts preview server, exports, and cleans up):
node node_modules/astromotion/scripts/deck-pdf.mjs my-talk output.pdf
MIT --- (c) Ben Swift