
Your slides are HTML.
Finally.
Every presentation tool gives you a box to check. fuckSlides gives you a blank HTML file.
Your CSS. Your animations. Your design system. Your code.
What is this?
fuckSlides is a presentation shell. It handles the player, keyboard navigation, overview, PDF export, and GIF export. Your slides are plain .html files — no special syntax, no runtime DSL, no framework opinions.
notes.json, autosaved, synced as you navigate.Install
Install globally to get the fuckslides command:
$ npm install -g fuck-slides
Or run without installing:
$ npx fuck-slides create my-talk
Quick Start
From zero to presenting in four commands:
$ fuckslides create my-talk $ cd my-talk $ npm install $ fuckslides serve
A browser opens at http://localhost:3000 with your presentation in the player. Start editing slides/index.html and refresh.
fuckslides create my-talk — creates the project structure with a starter slide and config.slides/index.html in your editor. Write HTML. Add slides. Update fuckslides.config.js with the new filenames.fuckslides serve — opens the player. Use arrow keys to navigate. Press G for the overview.fuckslides pdf — produces a single PDF. All slides, all states, properly captured.Project Structure
my-talk/ ├─ slides/ │ ├─ index.html ← your first slide │ ├─ problem.html │ ├─ solution.html │ └─ thank-you.html ├─ fuckslides.config.js ← slide manifest + options └─ package.json
That's the whole project. No src/, no dist/, no build step. Slides live in slides/ and the config tells fuckSlides the order.
Writing Slides
Each slide is a standalone HTML file. The only requirement is including the fuckSlides runtime at the bottom of <body>:
<script src="/js/fuckslides.js"></script>
Here's a minimal slide that actually looks good:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <style> html, body { width: 100%; height: 100vh; overflow: hidden; display: flex; align-items: center; justify-content: center; background: #0A1520; color: #fff; font-family: 'Inter', sans-serif; } h1 { font-size: clamp(3rem, 7vw, 8rem); font-weight: 900; } </style> </head> <body> <h1>Hello, world.</h1> <script src="/js/fuckslides.js"></script> </body> </html>
clamp(), vw, and vh units to keep layouts fluid — they'll scale correctly in the player regardless of screen size.
Built-ins
Including /js/fuckslides.js gives you two conveniences for free.
Scroll reveals
Add .reveal to any element. It fades in when it enters the viewport — and in PDF export, all reveals are forced visible automatically.
<p class="reveal">This appears on scroll.</p>
Add the transition CSS to your slide:
.reveal { opacity: 0; transform: translateY(16px); transition: opacity 0.5s ease, transform 0.5s ease; } .reveal.visible { opacity: 1; transform: none; }
Animated counters
Add data-target to any element. The number counts up when it enters the viewport.
<span data-target="30" data-suffix="×"></span> <span data-target="99.9" data-suffix="%" data-prefix="~"></span>
Integers animate as whole numbers. Floats animate to one decimal place.
fuckslides.config.js
One config file. Lives at the root of your presentation directory.
module.exports = { // Presentation name — used as PDF filename and player title name: 'my-talk', // Slides directory (default: 'slides') slidesDir: 'slides', // Ordered list of slide filenames slides: [ 'cover.html', 'problem.html', 'solution.html', 'demo.html', 'thank-you.html', ], // Labels shown in the overview panel — must match slides length labels: [ 'Cover', 'Problem', 'Solution', 'Demo', 'Thank You', ], // Slides to skip during navigation (still visible in overview) disabled: [ 'backup-slide.html', ], // Dev server port (default: 3000) port: 3000, };
| Key | Type | Description |
|---|---|---|
name | string | Presentation title. Used as PDF filename. |
slides | string[] | Ordered slide filenames relative to slidesDir. |
labels | string[] | Human-readable names shown in the overview panel. |
disabled | string[] | Slides to skip in navigation. Still toggleable in overview. |
slidesDir | string | Slides directory. Defaults to "slides". |
port | number | Dev server port. Defaults to 3000. |
pdfOverrides | object | Per-slide PDF capture overrides. See below. |
PDF Overrides
Some slides have JS-driven animations that need special handling before Puppeteer captures them. Define per-slide overrides in your config:
module.exports = { // ... pdfOverrides: { // Wait for a JS animation to reach its end state 'animated-counter.html': { wait: 5000, // extra ms on top of the default 2000ms }, // Inject JS to reach the desired visual state before capture 'step-reveal.html': { extra: ` document.querySelectorAll('.step').forEach(el => { el.classList.add('visible'); }); `, wait: 300, }, }, };
The extra script runs after fuckSlides has already called document.getAnimations().forEach(a => a.finish()) and forced all .reveal elements visible. Use it for anything your slide-specific JS controls.
Commands
fuckslides serve
Starts the dev server and opens the player in a browser.
$ fuckslides serve
fuckSlides · "my-talk"
http://localhost:3000
Slides are served from your slides/ directory. The slide manifest (window.FUCKSLIDES_SLIDES) is injected into every HTML file automatically — your slides don't need to know about each other.
fuckslides pdf
Exports all slides to a single PDF using Puppeteer.
$ fuckslides pdf → my-talk.pdf
Before capturing each slide, fuckSlides automatically:
- Finishes all running CSS animations
- Forces all
.revealelements visible - Runs your
pdfOverrides.extrascript (if defined) - Waits
pdfOverrides.waitextra ms (if defined)
fuckslides gif <slide>
Exports a single slide as an animated GIF at 2× retina quality.
$ fuckslides gif demo.html → demo.gif
Captures at 2560×1440, 20fps, 13 seconds via Puppeteer, then encodes with gifski for maximum quality. Requires gifski:
$ brew install gifski # macOS
fuckslides create <name>
Scaffolds a new presentation directory with a starter slide and config.
$ fuckslides create my-talk
Creates my-talk/ with slides/index.html, fuckslides.config.js, and package.json.
fuckslides import <file …>
Converts an existing PDF or a set of images into a complete fuckSlides presentation using Claude's vision API. The fastest way to go from an existing deck to a fully animated HTML presentation.
$ export ANTHROPIC_API_KEY=sk-ant-... $ fuckslides import deck.pdf 📄 12 pages found in deck.pdf [1/12] Converting slide… ✅ [2/12] Converting slide… ✅ ... [12/12] Converting slide… ✅ ✅ 12 slides imported into slides/ fuckslides serve
Accepts a PDF, individual images (PNG, JPG, WebP), or a mix:
$ fuckslides import deck.pdf $ fuckslides import slide1.png slide2.png slide3.png $ fuckslides import export/*.jpg
What it does:
- For PDFs — extracts each page as a screenshot using Puppeteer + pdf-lib
- Sends every slide image to Claude with a detailed fuckSlides design prompt
- Gets back a complete, animated HTML file per slide
- Writes to
slides/and generatesfuckslides.config.js
The generated slides reproduce the content and layout of the originals and apply the full fuckSlides design system — dark background, dot-grid, Inter typography, staggered fade-up animations.
Full example — PDF to live presentation
# 1. Set your API key $ export ANTHROPIC_API_KEY=sk-ant-... # 2. Create a project folder and import $ mkdir my-talk && cd my-talk $ fuckslides import ~/Downloads/q3-review.pdf # 3. Open in the player $ fuckslides serve
Full example — screenshots to live presentation
# Export your Google Slides or Keynote as PNG images first, # then point fuckslides import at them $ mkdir my-talk && cd my-talk $ fuckslides import ~/Desktop/slides/*.png $ fuckslides serve
Requires: ANTHROPIC_API_KEY environment variable. The SDK (@anthropic-ai/sdk) is included as a dependency — no extra install needed.
fuckslides add-slide <name>
Adds a new slide to your presentation from a built-in template. The slide is written to your slides/ directory and appended to fuckslides.config.js automatically.
$ fuckslides add-slide my-slide --template stat → slides/my-slide.html
Available templates:
title— large headline with eyebrow and subtitle (default)stat— big number with label and supporting textquote— pull quote with attributionsplit— two-column layoutbullets— heading with bulleted listcover— full-bleed cover slide with logo area
fuckslides export [output.html]
Bundles your entire presentation into a single self-contained HTML file. All assets — fonts, images, GIFs, JavaScript — are inlined as base64 data URIs. The output file opens in any browser with no server required.
$ fuckslides export → my-talk.html (4.2 MB) Open in any browser — no server needed.
You can specify a custom output path:
$ fuckslides export dist/deck.html
This is the command used by CI pipelines (e.g. GitHub Actions) to produce a deployable artifact from a fuckSlides presentation.
fuckslides pptx
Exports all slides to a PowerPoint file (.pptx) by screenshotting each slide at 1280×720 with Puppeteer and embedding the images into a presentation. Useful for sharing with stakeholders who need an editable deck.
$ fuckslides pptx → my-talk.pptx
Like fuckslides pdf, CSS animations are finished and .reveal elements are forced visible before each screenshot is captured.
fuckslides publish
Builds a self-contained export and deploys it to GitHub Pages via a gh-pages branch on your origin remote. After the first publish, the presentation is live at your GitHub Pages URL.
$ fuckslides publish Building export… Pushing to gh-pages… ✅ Published → https://you.github.io/my-talk
Requires: a git repository with an origin remote pointing to GitHub. GitHub Pages must be enabled on the repository (Settings → Pages → source: Deploy from a branch → gh-pages).
fuckslides hub [path|github-url]
Serves multiple presentations from a single hub interface. Scans one level deep for directories containing a fuckslides.config.js or index.html and lists them all in a browsable index.
$ fuckslides hub # serve current directory $ fuckslides hub ./decks # serve a local path $ fuckslides hub https://github.com/you/repo # clone & serve a GitHub repo
When given a GitHub URL, hub clones the repo to a local cache (~/.fuckslides-hub/) and pulls on subsequent runs to stay up to date. This makes it easy to serve a shared team library of presentations without checking anything out manually.
Player
Keyboard Shortcuts
| Action | Keys |
|---|---|
| Next slide | → ↓ Space PageDown |
| Previous slide | ← ↑ PageUp |
| Slide overview | G |
| Filmstrip | T |
| In-browser editor | E |
| Speaker notes | N |
| Fullscreen | F |
| Close overlay | Esc |
PageDown and PageUp — both are wired up and work out of the box.
Slide Overview
Press G to open a full-screen grid of all slides as live thumbnails. From the overview you can:
- Jump to any slide by clicking its thumbnail
- Reorder slides by dragging — order is saved instantly to
fuckslides.config.js - Disable individual slides with the ⊘ toggle (see below)
Disable Slides
In the overview, every slide has a ⊘ button. Toggle it to disable a slide: navigation skips it, the thumbnail greys out, and the change is written to fuckslides.config.js via the disabled array.
Disabled slides are never removed — they're just skipped during → navigation. You can re-enable them at any time from the overview.
In-Browser Editor
Press E to open a split-pane code editor for the current slide — without leaving the browser tab.
The workflow for a quick fix during prep:
contenteditable. Type your change. The code pane updates live.Other controls in the editor:
| Action | How |
|---|---|
| Resize panes | Drag the centre divider |
| Zoom code pane | + / − buttons in the code toolbar, or click the percentage to reset |
| Zoom preview pane | + / − buttons in the preview toolbar — scrolls when larger than the pane |
| Stop inline editing | Esc — exits text editing and returns to hover mode |
| Close editor | Esc again (or from the code pane directly) |
Speaker Notes
Press N to open a slide-up notes panel at the bottom of the screen. Each slide has its own notes — type freely, they autosave after 1 second of idle.
Notes are stored in notes.json at your project root, one key per slide filename:
{ "cover.html": "Wait for the room to settle before clicking. Start with the question, not the deck.", "context.html": "Two minutes max on this slide. They already know the background — don't over-explain.", "demo.html": "Live demo — terminal ready in a second Space. If it breaks, say 'let me show the recording' and move on. Don't apologise.", "results.html": "Pause after the headline number. Let it land before going to the breakdown.", "cta.html": "Slow down here. Make the ask explicit. Don't move on until the next step is clear." }
The file is plain JSON — easy to diff, commit alongside your slides, and share with co-presenters. Deleting a slide's notes from the panel removes its key from the file.
| Behaviour | Detail |
|---|---|
| Open / close | N or the notepad icon in the nav bar |
| Autosave | 1 second after you stop typing — status line shows ✓ Saved |
| Slide sync | Notes update automatically when you navigate to a different slide |
| Key conflicts | Typing in the notes textarea does not trigger slide navigation |
| Close from textarea | Esc closes the panel without losing unsaved text |
Standalone Mode
Every slide works without the player. Open any .html file directly in a browser — fuckSlides detects it's not running inside an iframe and injects a minimal nav pill in the bottom-right corner.
The pill shows current / total, back/forward arrows, and a fullscreen button. It reads the slide manifest from window.FUCKSLIDES_SLIDES (which the server injects), falling back to single-slide navigation when opened as a file.
fuckSlides · MIT License · Made with unreasonable conviction that slides should just be HTML