No-bullshit HTML presentations

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.

$ npm install -g fuck-slides

What is this?

Drag-and-drop templatesWrite HTML
Proprietary animation timelinesCSS keyframes
Export to PDF praying it doesn't breakPuppeteer. It works.
Lock-in to a platformIt's a file. Open it in a browser.
Themes that look like everyone else'sIt's your CSS. You decide.

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.

⌨️
Arrow-key navigation
Player forwards keys. Slides can lock navigation for demo sequences.
📄
PDF & GIF export
Puppeteer-powered. Per-slide overrides for animation state.
🗂️
Slide overview
Live thumbnails, drag-to-reorder, disable slides mid-presentation.
✏️
In-browser editor
Split-pane code editor with hover-to-edit live preview. Autosaves to disk.
🗒️
Speaker notes
Per-slide notes panel. Stored in notes.json, autosaved, synced as you navigate.
🔌
Zero lock-in
Every slide works standalone in a browser. No server required to view.

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.

1
Scaffold
fuckslides create my-talk — creates the project structure with a starter slide and config.
2
Edit
Open slides/index.html in your editor. Write HTML. Add slides. Update fuckslides.config.js with the new filenames.
3
Present
fuckslides serve — opens the player. Use arrow keys to navigate. Press G for the overview.
4
Export
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:

slides/index.html HTML
<!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>
💡 Slides are designed for 1280×720 (16:9). Use 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.

your-slide.htmlHTML
<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.

fuckslides.config.jsJS
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,

};
KeyTypeDescription
namestringPresentation title. Used as PDF filename.
slidesstring[]Ordered slide filenames relative to slidesDir.
labelsstring[]Human-readable names shown in the overview panel.
disabledstring[]Slides to skip in navigation. Still toggleable in overview.
slidesDirstringSlides directory. Defaults to "slides".
portnumberDev server port. Defaults to 3000.
pdfOverridesobjectPer-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:

fuckslides.config.jsJS
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 .reveal elements visible
  • Runs your pdfOverrides.extra script (if defined)
  • Waits pdfOverrides.wait extra 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 generates fuckslides.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 text
  • quote — pull quote with attribution
  • split — two-column layout
  • bullets — heading with bulleted list
  • cover — 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

ActionKeys
Next slide Space PageDown
Previous slide PageUp
Slide overviewG
FilmstripT
In-browser editorE
Speaker notesN
FullscreenF
Close overlayEsc
🎯 Presenter clickers send 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.

🎭 This is designed for live presentations where you want to conditionally skip backup slides or time-dependent content without having to edit the config mid-talk.

In-Browser Editor

Press E to open a split-pane code editor for the current slide — without leaving the browser tab.

Left pane — Source
Syntax-highlighted HTML editor. Edit the raw source directly. Changes reflect in the preview after a short debounce.
Right pane — Live preview
The actual slide, rendered. Hover any text element to highlight it; click to edit inline. Changes sync back to the code pane.

The workflow for a quick fix during prep:

1
Open the editor
Press E. The editor overlays the player with the current slide's source on the left and a live preview on the right.
2
Edit in the preview
Hover over any text in the right pane. It highlights with a blue outline. Click to make it contenteditable. Type your change. The code pane updates live.
3
Or edit in the source
Click the left pane and edit the HTML directly. The preview re-renders as you type (400ms debounce).
4
Autosaves
After 1.5s idle, the change is written to disk and the main slide reloads. The status bar shows ✓ Autosaved.

Other controls in the editor:

ActionHow
Resize panesDrag 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 editingEsc — exits text editing and returns to hover mode
Close editorEsc again (or from the code pane directly)
💡 Hover-to-edit skips structural elements (html, body, svg paths, etc.) and targets the innermost element with visible text. It won't accidentally select a wrapper div.

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:

notes.jsonJSON
{
  "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.

BehaviourDetail
Open / closeN or the notepad icon in the nav bar
Autosave1 second after you stop typing — status line shows ✓ Saved
Slide syncNotes update automatically when you navigate to a different slide
Key conflictsTyping in the notes textarea does not trigger slide navigation
Close from textareaEsc closes the panel without losing unsaved text
📝 The notes panel is designed for use during prep, not live presenting — it covers part of the slide. For an actual presenter display (notes on a second screen), use the standalone slide URL in a second window.

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.

📎 This means you can email a single slide as an HTML attachment and the recipient can open it in any browser — no server, no install, no player required.

fuckSlides · MIT License · Made with unreasonable conviction that slides should just be HTML