A lightweight, zero-dependency presentation framework built with vanilla HTML, CSS, and JavaScript. Includes 11 built-in slide transitions, keyboard/mouse/touch navigation, asset preloading, and offline support.
Presentations are designed for 1920x1080 (16:9) and automatically scale to fit any screen.
- Open
index.htmlin any browser, or - Serve it locally for the best experience:
python3 -m http.server 8000
# then open http://localhost:8000Navigate with arrow keys, spacebar, mouse clicks, or the nav dots on the right edge.
All slides live in index.html inside the <div class="slides-container">. Each slide follows this structure:
<section class="slide" id="slide-1">
<div class="slide-background">
<!-- background goes here -->
</div>
<div class="slide-content">
<!-- your content goes here -->
</div>
</section>To change what a slide says, edit the HTML inside <div class="slide-content">. You can use any HTML you want: headings, paragraphs, images, lists, videos, etc.
Replace the contents of <div class="slide-background"> with one of these options:
Gradient (no images needed):
<div class="gradient-bg gradient-blue-purple"></div>Available gradients: gradient-blue-purple, gradient-dark, gradient-cyan-blue, gradient-purple-magenta. Or write your own CSS gradient.
Image:
<img src="assets/images/yourImage.jpg" style="width:100%; height:100%; object-fit:cover;">Video (loops silently):
<video class="video-bg" autoplay muted loop playsinline>
<source src="assets/video/yourVideo.mp4" type="video/mp4">
</video>Solid color:
<div style="position:absolute; inset:0; background: #1a1a2e;"></div>Place image files in assets/images/ (use camelCase names, no spaces), then reference them:
<img src="assets/images/myPhoto.jpg" alt="Description" style="max-width: 80%;">Three things need to happen:
Insert a new <section> inside <div class="slides-container"> at the position you want. Give it the next sequential ID:
<!-- Slide 6: Your New Slide -->
<section class="slide" id="slide-6">
<div class="slide-background">
<div class="gradient-bg gradient-dark"></div>
</div>
<div class="slide-content">
<h1 class="hero-title">New Slide</h1>
</div>
</section>In the <nav class="slide-nav"> section near the top of the file, add a new button. The data-slide value is 0-indexed (so slide 6 = data-slide="5"):
<button class="nav-dot" data-slide="5" aria-label="Slide 6"></button>If you inserted your slide between existing slides (not at the end), you must renumber all slides that come after it. For example, if you insert a new slide at position 3:
- Old slide-3 becomes slide-4
- Old slide-4 becomes slide-5
- Old slide-5 becomes slide-6
- ...and so on
Update the id="slide-N" on each affected slide, and update the data-slide and aria-label on their corresponding nav dots.
Important: When renumbering, be careful with find-and-replace. Searching for slide-1 will also match slide-10, slide-11, etc. Always include the quote mark in your search: slide-1" to match only the exact ID.
Also update any references in js/slide-config.js (transitions, groups).
- Delete the
<section>fromindex.html - Delete the corresponding
<button>from the nav dots - Renumber all subsequent slides and nav dots (same caution as above)
- Remove any references to that slide in
js/slide-config.js
- Cut and paste the
<section>blocks into the new order - Renumber all slide IDs so they go 1, 2, 3... in order
- Renumber the nav dot
data-slideandaria-labelvalues to match - Update
js/slide-config.jstransitions to reference the new IDs
The slide IDs must always be sequential and match their position. Slide 1 is the first slide shown, slide 2 is second, etc. The URL hash (#3) takes you to id="slide-3".
Edit js/slide-config.js to control how slides animate between each other. If no transition is specified, slides use a simple fade.
transitions: {
'slide-1→slide-2': 'wipe', // wipe from slide 1 to 2
'slide-3↔slide-4': 'alvaro', // alvaro in both directions
},| Name | Description |
|---|---|
wipe |
Sweeps left-to-right, revealing the next slide |
crossfade |
Current slide fades out to reveal the next |
dissolve |
Similar to crossfade with different timing |
hartzet |
Next slide pushes in from the side with a bounce |
alvaro |
Diagonal panels close over the screen, then open on the new slide |
cube |
3D cube rotation (dramatic, best for big moments) |
circleExpand |
A circle grows from the center to reveal the next slide |
pageTurn |
Realistic book page turn (best used with slide groups) |
logoShrink |
Animates a centered logo down to a corner position |
logoExpand |
Animates a corner logo up to a centered position |
boxSlide |
Slides a content box vertically (background stays put) |
'slide-1→slide-2': 'wipe'— wipe only plays going forward (1 to 2). Going backward (2 to 1) uses the default fade.'slide-1↔slide-2': 'wipe'— wipe plays in both directions.
If you have a sequence of slides that should all use the same transition (like pages of a book), use groups instead of listing every pair:
groups: {
book: ['slide-5', 'slide-6', 'slide-7', 'slide-8'],
},
groupTransitions: {
book: 'pageTurn',
},This makes every forward transition within the group use pageTurn automatically.
To make clicking a slide open a URL in a new tab:
slideLinks: {
'slide-3': 'https://example.com',
},Clicking the slide content (not the background) will open the link and auto-advance to the next slide.
| Input | Action |
|---|---|
| Right arrow, Down arrow, Space | Next slide |
| Left arrow, Up arrow | Previous slide |
| Home | First slide |
| End | Last slide |
| Type a number, then Enter | Jump to that slide |
| Click right 2/3 of slide | Next slide |
| Click left 1/3 of slide | Previous slide |
| Nav dots (right edge, hover to reveal) | Jump to any slide |
URL hash (e.g. #5) |
Direct link to slide 5 |
This presentation is a static site — just HTML, CSS, JS, and media files. No server-side code, no build pipeline required.
Double-click index.html to open it in your browser. Everything works from file:/// except:
- The service worker (offline caching) requires HTTP, so it won't register. This is fine — you already have the files locally.
- Some browsers restrict
file:///video autoplay. If background videos don't play, use one of the HTTP options below.
For the full experience (service worker, reliable video playback):
python3 -m http.server 8000Upload the entire folder to any static hosting service:
- GitHub Pages — push to a repo, enable Pages in settings
- Netlify / Vercel — drag and drop the folder, or connect a git repo
- Amazon S3 — upload files, enable static website hosting
- Any web server — Apache, Nginx, IIS — just serve the folder
No special configuration needed. No .htaccess, no rewrites, no server-side processing.
Copy the entire folder to a USB drive. Open index.html on any computer with a modern browser. Good for conference venues with unreliable internet.
Run the build script to update the offline cache:
./build.shThis scans your assets/ folder and regenerates sw.js so the service worker knows about your files. It also bumps the ?v=N version numbers in index.html to bust browser caches.
All files in assets/images/ and assets/video/ must use camelCase with no spaces:
myPhoto.jpg— goodbackgroundVideo.mp4— goodMy Photo.jpg— bad (spaces break URLs)Background Video.mp4— bad
- Aspect ratio: Design for 1920x1080. The framework handles scaling.
- Video format: Use MP4 for cross-browser compatibility.
- Large files: Videos can be large. The preloader shows a progress bar so the audience isn't staring at a blank screen.
- Offline use: Load the presentation once over HTTP to prime the service worker cache. After that, it works without internet.
- Custom CSS: Add your own styles in
css/presentation.cssat the bottom of the file. Use slide IDs for targeted styling:#slide-3 .slide-content { ... }. - Presenter notes: Not built in, but you can add HTML comments inside each slide as notes to yourself — they won't render on screen.
Copyright (c) 2026 NextStep Product Solutions. Released under the MIT License.