11 KiB
HTML Presentation Template
Reference architecture for generating slide presentations. Every presentation follows a fixed 16:9 stage model: slides are authored at 1920×1080 and the whole stage scales to fit the browser window.
Base HTML Structure
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Presentation Title</title>
<!-- Fonts: use Fontshare or Google Fonts — never system fonts -->
<link rel="stylesheet" href="https://api.fontshare.com/v2/css?f[]=...">
<style>
/* ===========================================
CSS CUSTOM PROPERTIES (THEME)
Change these to change the whole look
=========================================== */
:root {
/* Colors — from chosen style preset */
--bg-primary: #0a0f1c;
--bg-secondary: #111827;
--text-primary: #ffffff;
--text-secondary: #9ca3af;
--accent: #00ffcc;
--accent-glow: rgba(0, 255, 204, 0.3);
/* Typography — authored at 1920×1080 stage size */
--font-display: 'Clash Display', sans-serif;
--font-body: 'Satoshi', sans-serif;
--title-size: 112px;
--subtitle-size: 34px;
--body-size: 28px;
/* Spacing — authored at 1920×1080 stage size */
--slide-padding: 72px;
--content-gap: 32px;
/* Animation */
--ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
--duration-normal: 0.6s;
}
/* ===========================================
BASE STYLES
=========================================== */
* { margin: 0; padding: 0; box-sizing: border-box; }
/* --- PASTE viewport-base.css CONTENTS HERE --- */
/* ===========================================
ANIMATIONS
Trigger via .visible class on the active slide
=========================================== */
.reveal {
opacity: 0;
transform: translateY(30px);
transition: opacity var(--duration-normal) var(--ease-out-expo),
transform var(--duration-normal) var(--ease-out-expo);
}
.slide.visible .reveal {
opacity: 1;
transform: translateY(0);
}
/* Stagger children for sequential reveal */
.reveal:nth-child(1) { transition-delay: 0.1s; }
.reveal:nth-child(2) { transition-delay: 0.2s; }
.reveal:nth-child(3) { transition-delay: 0.3s; }
.reveal:nth-child(4) { transition-delay: 0.4s; }
/* ... preset-specific styles ... */
</style>
</head>
<body>
<div class="deck-viewport">
<main class="deck-stage" id="deckStage">
<section class="slide title-slide active">
<h1 class="reveal">Presentation Title</h1>
<p class="reveal">Subtitle or author</p>
</section>
<section class="slide">
<div class="slide-content">
<h2 class="reveal">Slide Title</h2>
<p class="reveal">Content...</p>
</div>
</section>
<!-- More slides... -->
</main>
</div>
<script>
/* ===========================================
SLIDE PRESENTATION CONTROLLER
=========================================== */
class SlidePresentation {
constructor() {
this.slides = document.querySelectorAll('.slide');
this.currentSlide = 0;
this.stage = document.getElementById('deckStage');
this.setupStageScale();
this.setupKeyboardNav();
this.setupTouchNav();
this.showSlide(0);
}
setupStageScale() {
const scale = () => {
const factor = Math.min(window.innerWidth / 1920, window.innerHeight / 1080);
const x = (window.innerWidth - 1920 * factor) / 2;
const y = (window.innerHeight - 1080 * factor) / 2;
this.stage.style.transform = `translate(${x}px, ${y}px) scale(${factor})`;
};
scale();
window.addEventListener('resize', scale);
}
setupKeyboardNav() {
// Arrow keys, Space, Page Up/Down
}
setupTouchNav() {
// Touch/swipe support for mobile
}
showSlide(index) {
this.currentSlide = Math.max(0, Math.min(index, this.slides.length - 1));
this.slides.forEach((slide, i) => {
slide.classList.toggle('active', i === this.currentSlide);
slide.classList.toggle('visible', i === this.currentSlide);
});
}
}
new SlidePresentation();
</script>
</body>
</html>
Required JavaScript Features
Every presentation must include:
-
SlidePresentation Class — Main controller with:
- Keyboard navigation (arrows, space, page up/down)
- Touch/swipe support
- Mouse wheel navigation
- Optional progress indicator or page count, kept outside the slide stage
-
Stage Scaling — For fixed 16:9 presentation behavior:
- Keep all slides at 1920×1080 inside
.deck-stage - Scale the whole stage with one transform
- Letterbox/pillarbox as needed; never reflow slide content per device
- Keep all slides at 1920×1080 inside
-
Optional Enhancements (match to chosen style):
- Custom cursor with trail
- Particle system background (canvas)
- Parallax effects
- 3D tilt on hover
- Magnetic buttons
- Counter animations
-
Inline Editing (included by default after draft generation):
- Edit toggle button (hidden by default, revealed via hover hotzone or
Ekey) - Auto-save to localStorage
- Export/save file functionality
- See "Inline Editing Implementation" section below
- Edit toggle button (hidden by default, revealed via hover hotzone or
Inline Editing Implementation
Inline editing is a lightweight post-draft affordance. Do not ask the user whether they want it during the pre-generation Q&A. Include it by default unless the user explicitly asks for a locked/export-only presentation or no editing controls.
Do NOT use CSS ~ sibling selector for hover-based show/hide. The CSS-only approach (edit-hotzone:hover ~ .edit-toggle) fails because pointer-events: none on the toggle button breaks the hover chain: user hovers hotzone -> button becomes visible -> mouse moves toward button -> leaves hotzone -> button disappears before click.
Required approach: JS-based hover with 400ms delay timeout.
HTML:
<div class="edit-hotzone"></div>
<button class="edit-toggle" id="editToggle" title="Edit mode (E)">✏️</button>
CSS (visibility controlled by JS classes only):
/* Do NOT use CSS ~ sibling selector for this!
pointer-events: none breaks the hover chain.
Must use JS with delay timeout. */
.edit-hotzone {
position: fixed; top: 0; left: 0;
width: 80px; height: 80px;
z-index: 10000;
cursor: pointer;
}
.edit-toggle {
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease;
z-index: 10001;
}
.edit-toggle.show,
.edit-toggle.active {
opacity: 1;
pointer-events: auto;
}
JS (three interaction methods):
// 1. Click handler on the toggle button
document.getElementById('editToggle').addEventListener('click', () => {
editor.toggleEditMode();
});
// 2. Hotzone hover with 400ms grace period
const hotzone = document.querySelector('.edit-hotzone');
const editToggle = document.getElementById('editToggle');
let hideTimeout = null;
hotzone.addEventListener('mouseenter', () => {
clearTimeout(hideTimeout);
editToggle.classList.add('show');
});
hotzone.addEventListener('mouseleave', () => {
hideTimeout = setTimeout(() => {
if (!editor.isActive) editToggle.classList.remove('show');
}, 400);
});
editToggle.addEventListener('mouseenter', () => {
clearTimeout(hideTimeout);
});
editToggle.addEventListener('mouseleave', () => {
hideTimeout = setTimeout(() => {
if (!editor.isActive) editToggle.classList.remove('show');
}, 400);
});
// 3. Hotzone direct click
hotzone.addEventListener('click', () => {
editor.toggleEditMode();
});
// 4. Keyboard shortcut (E key, skip when editing text)
document.addEventListener('keydown', (e) => {
if ((e.key === 'e' || e.key === 'E') && !e.target.getAttribute('contenteditable')) {
editor.toggleEditMode();
}
});
Image Pipeline (Skip If No Images)
If user chose "No images" in Phase 1, skip this entirely. If images were provided, process them before generating HTML.
Dependency: pip install Pillow
Image Processing
from PIL import Image, ImageDraw
# Circular crop (for logos on modern/clean styles)
def crop_circle(input_path, output_path):
img = Image.open(input_path).convert('RGBA')
w, h = img.size
size = min(w, h)
left, top = (w - size) // 2, (h - size) // 2
img = img.crop((left, top, left + size, top + size))
mask = Image.new('L', (size, size), 0)
ImageDraw.Draw(mask).ellipse([0, 0, size, size], fill=255)
img.putalpha(mask)
img.save(output_path, 'PNG')
# Resize (for oversized images that inflate HTML)
def resize_max(input_path, output_path, max_dim=1200):
img = Image.open(input_path)
img.thumbnail((max_dim, max_dim), Image.LANCZOS)
img.save(output_path, quality=85)
| Situation | Operation |
|---|---|
| Square logo on rounded aesthetic | crop_circle() |
| Image > 1MB | resize_max(max_dim=1200) |
| Wrong aspect ratio | Manual crop with img.crop() |
Save processed images with _processed suffix. Never overwrite originals.
Image Placement
Use direct file paths (not base64) — presentations are viewed locally:
<img src="assets/logo_round.png" alt="Logo" class="slide-image logo">
<img src="assets/screenshot.png" alt="Screenshot" class="slide-image screenshot">
.slide-image {
max-width: 100%;
max-height: min(50vh, 400px);
object-fit: contain;
border-radius: 8px;
}
.slide-image.screenshot {
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}
.slide-image.logo {
max-height: min(30vh, 200px);
}
Adapt border/shadow colors to match the chosen style's accent. Never repeat the same image on multiple slides (except logos on title + closing).
Placement patterns: Logo centered on title slide. Screenshots in two-column layouts with text. Full-bleed images as slide backgrounds with text overlay (use sparingly).
Code Quality
Comments: Every section needs clear comments explaining what it does and how to modify it.
Accessibility:
- Semantic HTML (
<section>,<nav>,<main>) - Keyboard navigation works fully
- ARIA labels where needed
prefers-reduced-motionsupport (included in viewport-base.css)
File Structure
Single presentations:
presentation.html # Self-contained, all CSS/JS inline
assets/ # Images only, if any
Multiple presentations in one project:
[name].html
[name]-assets/