Create stunning, animation-rich HTML presentations from scratch or by converting PowerPoint files. Use when the user wants to build a presentation, convert a PPT/PPTX to web, or create slides for a talk/pitch. Helps non-designers discover their aesthetic through visual exploration rather than abstract choices.
Create zero-dependency, animation-rich HTML presentations that run entirely in the browser. This skill helps non-designers discover their preferred aesthetic through visual exploration ("show, don't tell"), then generates production-quality slide decks.
This section is mandatory for ALL presentations. Every slide must be fully visible without scrolling on any screen size.
Each slide = exactly one viewport height (100vh/100dvh)
Content overflows? → Split into multiple slides or reduce content
Never scroll within a slide.
To guarantee viewport fitting, enforce these limits per slide:
| Slide Type | Maximum Content |
|---|---|
| Title slide | 1 heading + 1 subtitle + optional tagline |
| Content slide | 1 heading + 4-6 bullet points OR 1 heading + 2 paragraphs |
| Feature grid | 1 heading + 6 cards maximum (2x3 or 3x2 grid) |
| Code slide | 1 heading + 8-10 lines of code maximum |
| Quote slide | 1 quote (max 3 lines) + attribution |
| Image slide | 1 heading + 1 image (max 60vh height) |
If content exceeds these limits → Split into multiple slides
Every presentation MUST include this base CSS for viewport fitting:
/* ===========================================
VIEWPORT FITTING: MANDATORY BASE STYLES
These styles MUST be included in every presentation.
They ensure slides fit exactly in the viewport.
=========================================== */
/* 1. Lock html/body to viewport */
html, body {
height: 100%;
overflow-x: hidden;
}
html {
scroll-snap-type: y mandatory;
scroll-behavior: smooth;
}
/* 2. Each slide = exact viewport height */
.slide {
width: 100vw;
height: 100vh;
height: 100dvh; /* Dynamic viewport height for mobile browsers */
overflow: hidden; /* CRITICAL: Prevent ANY overflow */
scroll-snap-align: start;
display: flex;
flex-direction: column;
position: relative;
}
/* 3. Content container with flex for centering */
.slide-content {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
max-height: 100%;
overflow: hidden; /* Double-protection against overflow */
padding: var(--slide-padding);
}
/* 4. ALL typography uses clamp() for responsive scaling */
:root {
/* Titles scale from mobile to desktop */
--title-size: clamp(1.5rem, 5vw, 4rem);
--h2-size: clamp(1.25rem, 3.5vw, 2.5rem);
--h3-size: clamp(1rem, 2.5vw, 1.75rem);
/* Body text */
--body-size: clamp(0.75rem, 1.5vw, 1.125rem);
--small-size: clamp(0.65rem, 1vw, 0.875rem);
/* Spacing scales with viewport */
--slide-padding: clamp(1rem, 4vw, 4rem);
--content-gap: clamp(0.5rem, 2vw, 2rem);
--element-gap: clamp(0.25rem, 1vw, 1rem);
}
/* 5. Cards/containers use viewport-relative max sizes */
.card, .container, .content-box {
max-width: min(90vw, 1000px);
max-height: min(80vh, 700px);
}
/* 6. Lists auto-scale with viewport */
.feature-list, .bullet-list {
gap: clamp(0.4rem, 1vh, 1rem);
}
.feature-list li, .bullet-list li {
font-size: var(--body-size);
line-height: 1.4;
}
/* 7. Grids adapt to available space */
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 250px), 1fr));
gap: clamp(0.5rem, 1.5vw, 1rem);
}
/* 8. Images constrained to viewport */
img, .image-container {
max-width: 100%;
max-height: min(50vh, 400px);
object-fit: contain;
}
/* ===========================================
RESPONSIVE BREAKPOINTS
Aggressive scaling for smaller viewports
=========================================== */
/* Short viewports (< 700px height) */
@media (max-height: 700px) {
:root {
--slide-padding: clamp(0.75rem, 3vw, 2rem);
--content-gap: clamp(0.4rem, 1.5vw, 1rem);
--title-size: clamp(1.25rem, 4.5vw, 2.5rem);
--h2-size: clamp(1rem, 3vw, 1.75rem);
}
}
/* Very short viewports (< 600px height) */
@media (max-height: 600px) {
:root {
--slide-padding: clamp(0.5rem, 2.5vw, 1.5rem);
--content-gap: clamp(0.3rem, 1vw, 0.75rem);
--title-size: clamp(1.1rem, 4vw, 2rem);
--body-size: clamp(0.7rem, 1.2vw, 0.95rem);
}
/* Hide non-essential elements */
.nav-dots, .keyboard-hint, .decorative {
display: none;
}
}
/* Extremely short (landscape phones, < 500px height) */
@media (max-height: 500px) {
:root {
--slide-padding: clamp(0.4rem, 2vw, 1rem);
--title-size: clamp(1rem, 3.5vw, 1.5rem);
--h2-size: clamp(0.9rem, 2.5vw, 1.25rem);
--body-size: clamp(0.65rem, 1vw, 0.85rem);
}
}
/* Narrow viewports (< 600px width) */
@media (max-width: 600px) {
:root {
--title-size: clamp(1.25rem, 7vw, 2.5rem);
}
/* Stack grids vertically */
.grid {
grid-template-columns: 1fr;
}
}
/* ===========================================
REDUCED MOTION
Respect user preferences
=========================================== */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
transition-duration: 0.2s !important;
}
html {
scroll-behavior: auto;
}
}
Before generating any presentation, mentally verify:
.slide has height: 100vh; height: 100dvh; overflow: hidden;clamp(min, preferred, max)clamp() or viewport unitsmax-height constraintsmax-height: min(50vh, 400px) or similarauto-fit with minmax() for responsive columnsIf you find yourself with too much content:
DO:
DON'T:
After generating, recommend the user test at these sizes:
First, determine what the user wants:
Mode A: New Presentation
Mode B: PPT Conversion
Mode C: Existing Presentation Enhancement
When enhancing existing presentations, follow these mandatory rules:
1. Before Adding Any Content:
2. When Adding Images (MOST COMMON ISSUE):
max-height: min(50vh, 400px) or similar viewport constraint3. When Adding Text Content:
4. Required Checks After ANY Modification:
✅ Does the slide have `overflow: hidden` on `.slide` class?
✅ Are all new elements using `clamp()` for font sizes?
✅ Do new images have viewport-relative max-height?
✅ Does total content respect density limits?
✅ Will this fit on a 1280×720 screen? On mobile portrait?
5. Proactive Reorganization (NOT Optional): When you detect that modifications will cause overflow:
6. Testing After Modifications: Mentally verify the modified slide at these viewport sizes:
If in doubt → Split the content. Never allow scrolling within a slide.
Before designing, understand the content. Ask via AskUserQuestion:
IMPORTANT: Ask ALL 4 questions in a single AskUserQuestion call so the user can fill everything out at once before submitting.
Question 1: Purpose
Question 2: Slide Count
Question 3: Content
Question 4: Images
assets/ folder in the current projectThe user can select "Other" to type or paste any custom folder path (e.g. ~/Desktop/screenshots). This way the image folder path is collected in the same form — no extra round-trip.
Question 5: Inline Editing
Remember the user's choice — it determines whether edit-related HTML/CSS/JS is included in Phase 3.
If user has content, ask them to share it (text, bullet points, images, etc.).
User-provided assets are important visual anchors — but not every asset is necessarily usable. The first step is always to evaluate. After evaluation, the curated assets become additional context that shapes how the presentation is built. This is a co-design process: text content + curated visuals together inform the slide structure from the start, not a post-hoc "fit images in after the fact."
If user selected "No images" → Skip the entire image pipeline. Proceed directly to Phase 2 (Style Discovery) and Phase 3 (Generate Presentation) using text content only. The presentation will use CSS-generated visuals (gradients, shapes, patterns, typography) for visual interest — this is the original behavior and produces fully polished results without any images.
If user provides an image folder:
ls to list all image files (.png, .jpg, .jpeg, .gif, .svg, .webp)USABLE or NOT USABLE (with reason: blurry, irrelevant, broken, etc.)Co-design: curated assets inform the outline
After evaluation, the usable images become context for planning the slide structure alongside text content. This is not "plan slides then add images" — it's designing the presentation around both text and visuals from the start:
This means curated images are factored in before style selection (Phase 2) and before HTML generation (Phase 3). They are co-equal context in the design process.
Question: Outline Confirmation
This keeps the entire flow in the AskUserQuestion format without dropping to free-text chat.
CRITICAL: This is the "show, don't tell" phase.
Most people can't articulate design preferences in words. Instead of asking "do you want minimalist or bold?", we generate mini-previews and let them react.
Users can select a style in two ways:
Option A: Guided Discovery (Default)
Option B: Direct Selection
Available Presets:
| Preset | Vibe | Best For |
|---|---|---|
| Bold Signal | Confident, high-impact | Pitch decks, keynotes |
| Electric Studio | Clean, professional | Agency presentations |
| Creative Voltage | Energetic, retro-modern | Creative pitches |
| Dark Botanical | Elegant, sophisticated | Premium brands |
| Notebook Tabs | Editorial, organized | Reports, reviews |
| Pastel Geometry | Friendly, approachable | Product overviews |
| Split Pastel | Playful, modern | Creative agencies |
| Vintage Editorial | Witty, personality-driven | Personal brands |
| Neon Cyber | Futuristic, techy | Tech startups |
| Terminal Green | Developer-focused | Dev tools, APIs |
| Swiss Modern | Minimal, precise | Corporate, data |
| Paper & Ink | Literary, thoughtful | Storytelling |
First, ask how the user wants to choose their style:
Question: Style Selection Method
If "Show me options" → Continue to Step 2.1 (Mood Selection)
If "I know what I want" → Show preset picker:
Question: Pick a Preset
(If user picks one, skip to Phase 3. If they want to see more options, show additional presets or proceed to guided discovery.)
Question 1: Feeling
Based on their mood selection, generate 3 distinct style previews as mini HTML files in a temporary directory. Each preview should be a single title slide showing:
Preview Styles to Consider (pick 3 based on mood):
| Mood | Style Options |
|---|---|
| Impressed/Confident | "Bold Signal", "Electric Studio", "Dark Botanical" |
| Excited/Energized | "Creative Voltage", "Neon Cyber", "Split Pastel" |
| Calm/Focused | "Notebook Tabs", "Paper & Ink", "Swiss Modern" |
| Inspired/Moved | "Dark Botanical", "Vintage Editorial", "Pastel Geometry" |
IMPORTANT: Never use these generic patterns:
Instead, use distinctive choices:
Create the previews in: .claude-design/slide-previews/
.claude-design/slide-previews/
├── style-a.html # First style option
├── style-b.html # Second style option
├── style-c.html # Third style option
└── assets/ # Any shared assets
Each preview file should be:
Logo in previews (if available): If the user provided images in Step 1.2 and a logo was identified as USABLE, embed it (base64) into each of the 3 style previews. This creates a "wow moment" — the user sees their own brand identity styled three different ways, making the choice feel personal rather than abstract. Apply any necessary processing (e.g., circular crop) per-style so each preview shows the logo as it would actually appear in the final presentation. If no logo was provided, generate previews without one — this is fine.
Present to user:
I've created 3 style previews for you to compare:
**Style A: [Name]** — [1 sentence description]
**Style B: [Name]** — [1 sentence description]
**Style C: [Name]** — [1 sentence description]
Open each file to see them in action:
- .claude-design/slide-previews/style-a.html
- .claude-design/slide-previews/style-b.html
- .claude-design/slide-previews/style-c.html
Take a look and tell me:
1. Which style resonates most?
2. What do you like about it?
3. Anything you'd change?
Then use AskUserQuestion:
Question: Pick Your Style
If "Mix elements", ask for specifics.
Now generate the full presentation based on:
If the user provided images, the slide outline already incorporates them as visual anchors from Step 1.2. If not, proceed with text-only content — CSS-generated visuals (gradients, shapes, patterns) provide visual interest.
If the user chose "No images" in Step 1.2, skip this entire section and go straight to generating HTML. The presentation will be text-only with CSS-generated visuals — this is a fully supported, first-class path.
If the user provided images, execute these steps before generating HTML.
Key principle: Co-design, not post-hoc. The curated images from Step 1.2 (those marked USABLE) are already part of the slide outline. The pipeline's job here is to process images for the chosen style and place them in the HTML.
For each curated image, determine what processing it needs based on the chosen style (e.g., circular crop for logos, resize for large files) and what CSS framing will bridge any color gaps between the image and the style's palette. Then process accordingly.
Rules:
Dependency: Python Pillow library (the standard image processing library for Python).
# Install if not available (portable across macOS/Linux/Windows)
pip install Pillow
This is analogous to how python-pptx is used in Phase 4 (PPT Conversion) — a standard, well-maintained Python package that any user can install.
Common processing operations:
from PIL import Image, ImageDraw
# ─── Circular Crop (for logos on modern/clean styles) ───
def crop_circle(input_path, output_path):
"""Crop a square image to a circle with transparent background."""
img = Image.open(input_path).convert('RGBA')
w, h = img.size
# Make square if not already
size = min(w, h)
left = (w - size) // 2
top = (h - size) // 2
img = img.crop((left, top, left + size, top + size))
# Create circular mask
mask = Image.new('L', (size, size), 0)
draw = ImageDraw.Draw(mask)
draw.ellipse([0, 0, size, size], fill=255)
img.putalpha(mask)
img.save(output_path, 'PNG')
# ─── Resize (for oversized images that inflate the HTML) ───
def resize_max(input_path, output_path, max_dim=1200):
"""Resize image so largest dimension <= max_dim. Preserves aspect ratio."""
img = Image.open(input_path)
img.thumbnail((max_dim, max_dim), Image.LANCZOS)
img.save(output_path, quality=85)
# ─── Add Padding / Background (for images that need breathing room) ───
def add_padding(input_path, output_path, padding=40, bg_color=(0, 0, 0, 0)):
"""Add transparent padding around an image."""
img = Image.open(input_path).convert('RGBA')
w, h = img.size
new = Image.new('RGBA', (w + 2*padding, h + 2*padding), bg_color)
new.paste(img, (padding, padding), img)
new.save(output_path, 'PNG')
When to apply each operation:
| Situation | Operation |
|---|---|
| Square logo on a style with rounded aesthetics | crop_circle() |
| Image > 1MB (slow to load) | resize_max(max_dim=1200) |
| Screenshot needs breathing room in layout | add_padding() |
| Image has wrong aspect ratio for its slide slot | Manual crop with img.crop((left, top, right, bottom)) |
Save processed images alongside originals with a _processed suffix (e.g., logo_round.png). Never overwrite the user's original files.
Use direct file paths — do NOT convert images to base64 data URIs. Since presentations are viewed locally, reference images with relative paths from the HTML file:
<img src="assets/logo_round.png" alt="Logo" class="slide-image logo">
<img src="assets/screenshot.png" alt="Screenshot" class="slide-image screenshot">
This keeps the HTML file small and images easy to swap. Only use base64 encoding if the user explicitly requests a fully self-contained single-file presentation.
Image CSS classes (adapt border/glow colors to match the chosen style):
/* Base image constraint — CRITICAL for viewport fitting */
.slide-image {
max-width: 100%;
max-height: min(50vh, 400px);
object-fit: contain;
border-radius: 8px;
}
/* Screenshots: add framing to bridge color gaps with the style */
.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);
}
/* Logos: smaller, no frame */
.slide-image.logo {
max-height: min(30vh, 200px);
}
IMPORTANT: Adapt the .screenshot border and shadow colors to match the chosen style's accent color. For example:
border: 1px solid rgba(197, 160, 89, 0.2); box-shadow: 0 0 20px rgba(197, 160, 89, 0.08);border: 2px solid rgba(212, 255, 0, 0.25); box-shadow: 0 0 20px rgba(212, 255, 0, 0.08);Placement patterns:
Note: Processed images (e.g. logo_round.png) are saved alongside originals in the assets folder. Reference them with relative paths in the HTML.
For single presentations:
presentation.html # Self-contained presentation
assets/ # Images, if any
For projects with multiple presentations:
[presentation-name].html
[presentation-name]-assets/
Follow this structure for all presentations:
<!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) -->
<link rel="stylesheet" href="https://api.fontshare.com/v2/css?f[]=...">
<style>
/* ===========================================
CSS CUSTOM PROPERTIES (THEME)
Easy to modify: change these to change the whole look
=========================================== */
:root {
/* Colors */
--bg-primary: #0a0f1c;
--bg-secondary: #111827;
--text-primary: #ffffff;
--text-secondary: #9ca3af;
--accent: #00ffcc;
--accent-glow: rgba(0, 255, 204, 0.3);
/* Typography - MUST use clamp() for responsive scaling */
--font-display: 'Clash Display', sans-serif;
--font-body: 'Satoshi', sans-serif;
--title-size: clamp(2rem, 6vw, 5rem);
--subtitle-size: clamp(0.875rem, 2vw, 1.25rem);
--body-size: clamp(0.75rem, 1.2vw, 1rem);
/* Spacing - MUST use clamp() for responsive scaling */
--slide-padding: clamp(1.5rem, 4vw, 4rem);
--content-gap: clamp(1rem, 2vw, 2rem);
/* 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;
}
html {
scroll-behavior: smooth;
scroll-snap-type: y mandatory;
height: 100%;
}
body {
font-family: var(--font-body);
background: var(--bg-primary);
color: var(--text-primary);
overflow-x: hidden;
height: 100%;
}
/* ===========================================
SLIDE CONTAINER
CRITICAL: Each slide MUST fit exactly in viewport
- Use height: 100vh (NOT min-height)
- Use overflow: hidden to prevent scroll
- Content must scale with clamp() values
=========================================== */
.slide {
width: 100vw;
height: 100vh; /* EXACT viewport height - no scrolling */
height: 100dvh; /* Dynamic viewport height for mobile */
padding: var(--slide-padding);
scroll-snap-align: start;
display: flex;
flex-direction: column;
justify-content: center;
position: relative;
overflow: hidden; /* Prevent any content overflow */
}
/* Content wrapper that prevents overflow */
.slide-content {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
max-height: 100%;
overflow: hidden;
}
/* ===========================================
RESPONSIVE BREAKPOINTS
Adjust content for different screen sizes
=========================================== */
@media (max-height: 600px) {
:root {
--slide-padding: clamp(1rem, 3vw, 2rem);
--content-gap: clamp(0.5rem, 1.5vw, 1rem);
}
}
@media (max-width: 768px) {
:root {
--title-size: clamp(1.5rem, 8vw, 3rem);
}
}
@media (max-height: 500px) and (orientation: landscape) {
/* Extra compact for landscape phones */
:root {
--title-size: clamp(1.25rem, 5vw, 2rem);
--slide-padding: clamp(0.75rem, 2vw, 1.5rem);
}
}
/* ===========================================
ANIMATIONS
Trigger via .visible class (added by JS on scroll)
=========================================== */
.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 */
.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; }
/* ... more styles ... */
</style>
</head>
<body>
<!-- Progress bar (optional) -->
<div class="progress-bar"></div>
<!-- Navigation dots (optional) -->
<nav class="nav-dots">
<!-- Generated by JS -->
</nav>
<!-- Slides -->
<section class="slide title-slide">
<h1 class="reveal">Presentation Title</h1>
<p class="reveal">Subtitle or author</p>
</section>
<section class="slide">
<h2 class="reveal">Slide Title</h2>
<p class="reveal">Content...</p>
</section>
<!-- More slides... -->
<script>
/* ===========================================
SLIDE PRESENTATION CONTROLLER
Handles navigation, animations, and interactions
=========================================== */
class SlidePresentation {
constructor() {
// ... initialization
}
// ... methods
}
// Initialize
new SlidePresentation();
</script>
</body>
</html>
Every presentation should include:
SlidePresentation Class — Main controller
Intersection Observer — For scroll-triggered animations
.visible class when slides enter viewportOptional Enhancements (based on style):
E key)If the user chose "No" for inline editing in Phase 1, skip this entirely — do not generate any edit-related HTML, CSS, or JS.
⚠️ Critical: 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 causes the hover chain to break: user hovers hotzone → button becomes visible → mouse moves toward button → leaves hotzone → button disappears before click reaches it.
Required approach: JS-based hover with delay timeout.
HTML structure:
<div class="edit-hotzone"></div>
<button class="edit-toggle" id="editToggle" title="编辑模式 (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;
}
/* Only JS-added classes control visibility */
.edit-toggle.show,
.edit-toggle.active {
opacity: 1;
pointer-events: auto;
}
JS (all 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();
}
});
Comments: Every section should have clear comments explaining:
/* ===========================================
CUSTOM CURSOR
Creates a stylized cursor that follows mouse with a trail effect.
- Uses lerp (linear interpolation) for smooth movement
- Grows larger when hovering over interactive elements
=========================================== */
class CustomCursor {
constructor() {
// ...
}
}
Accessibility:
<section>, <nav>, <main>)@media (prefers-reduced-motion: reduce) {
.reveal {
transition: opacity 0.3s ease;
transform: none;
}
}
CSS Function Negation:
-clamp(), -min(), -max() are silently ignored by browsers with no console errorcalc(-1 * clamp(...)) instead. See STYLE_PRESETS.md → "CSS Gotchas" for details.Responsive & Viewport Fitting (CRITICAL):
See the "CRITICAL: Viewport Fitting Requirements" section above for complete CSS and guidelines.
Quick reference:
.slide must have height: 100vh; height: 100dvh; overflow: hidden;clamp()When converting PowerPoint files:
Use Python with python-pptx to extract:
from pptx import Presentation
from pptx.util import Inches, Pt
import json
import os
import base64
def extract_pptx(file_path, output_dir):
"""
Extract all content from a PowerPoint file.
Returns a JSON structure with slides, text, and images.
"""
prs = Presentation(file_path)
slides_data = []
# Create assets directory
assets_dir = os.path.join(output_dir, 'assets')
os.makedirs(assets_dir, exist_ok=True)
for slide_num, slide in enumerate(prs.slides):
slide_data = {
'number': slide_num + 1,
'title': '',
'content': [],
'images': [],
'notes': ''
}
for shape in slide.shapes:
# Extract title
if shape.has_text_frame:
if shape == slide.shapes.title:
slide_data['title'] = shape.text
else:
slide_data['content'].append({
'type': 'text',
'content': shape.text
})
# Extract images
if shape.shape_type == 13: # Picture
image = shape.image
image_bytes = image.blob
image_ext = image.ext
image_name = f"slide{slide_num + 1}_img{len(slide_data['images']) + 1}.{image_ext}"
image_path = os.path.join(assets_dir, image_name)
with open(image_path, 'wb') as f:
f.write(image_bytes)
slide_data['images'].append({
'path': f"assets/{image_name}",
'width': shape.width,
'height': shape.height
})
# Extract notes
if slide.has_notes_slide:
notes_frame = slide.notes_slide.notes_text_frame
slide_data['notes'] = notes_frame.text
slides_data.append(slide_data)
return slides_data
Present the extracted content to the user:
I've extracted the following from your PowerPoint:
**Slide 1: [Title]**
- [Content summary]
- Images: [count]
**Slide 2: [Title]**
- [Content summary]
- Images: [count]
...
All images have been saved to the assets folder.
Does this look correct? Should I proceed with style selection?
Proceed to Phase 2 (Style Discovery) with the extracted content in mind.
Convert the extracted content into the chosen style, preserving:
When the presentation is complete:
Clean up temporary files
.claude-design/slide-previews/ if it existsOpen the presentation
open [filename].html to launch in browserProvide summary
Your presentation is ready!
📁 File: [filename].html
🎨 Style: [Style Name]
📊 Slides: [count]
**Navigation:**
- Arrow keys (← →) or Space to navigate
- Scroll/swipe also works
- Click the dots on the right to jump to a slide
**To customize:**
- Colors: Look for `:root` CSS variables at the top
- Fonts: Change the Fontshare/Google Fonts link
- Animations: Modify `.reveal` class timings
Would you like me to make any adjustments?
If the user opted in to inline editing, also include:
**Editing:**
- Hover over top-left corner or press E to enter edit mode
- Click any text to edit directly
- Ctrl+S or click "Save file" to save changes
Use this guide to match animations to intended feelings:
/* Fade + Slide Up (most common) */
.reveal {
opacity: 0;
transform: translateY(30px);
transition: opacity 0.6s var(--ease-out-expo),
transform 0.6s var(--ease-out-expo);
}
.visible .reveal {
opacity: 1;
transform: translateY(0);
}
/* Scale In */
.reveal-scale {
opacity: 0;
transform: scale(0.9);
transition: opacity 0.6s, transform 0.6s var(--ease-out-expo);
}
/* Slide from Left */
.reveal-left {
opacity: 0;
transform: translateX(-50px);
transition: opacity 0.6s, transform 0.6s var(--ease-out-expo);
}
/* Blur In */
.reveal-blur {
opacity: 0;
filter: blur(10px);
transition: opacity 0.8s, filter 0.8s var(--ease-out-expo);
}
/* Gradient Mesh */
.gradient-bg {
background:
radial-gradient(ellipse at 20% 80%, rgba(120, 0, 255, 0.3) 0%, transparent 50%),
radial-gradient(ellipse at 80% 20%, rgba(0, 255, 200, 0.2) 0%, transparent 50%),
var(--bg-primary);
}
/* Noise Texture */
.noise-bg {
background-image: url("data:image/svg+xml,..."); /* Inline SVG noise */
}
/* Grid Pattern */
.grid-bg {
background-image:
linear-gradient(rgba(255,255,255,0.03) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,0.03) 1px, transparent 1px);
background-size: 50px 50px;
}
/* 3D Tilt on Hover */
class TiltEffect {
constructor(element) {
this.element = element;
this.element.style.transformStyle = 'preserve-3d';
this.element.style.perspective = '1000px';
this.bindEvents();
}
bindEvents() {
this.element.addEventListener('mousemove', (e) => {
const rect = this.element.getBoundingClientRect();
const x = (e.clientX - rect.left) / rect.width - 0.5;
const y = (e.clientY - rect.top) / rect.height - 0.5;
this.element.style.transform = `
rotateY(${x * 10}deg)
rotateX(${-y * 10}deg)
`;
});
this.element.addEventListener('mouseleave', () => {
this.element.style.transform = 'rotateY(0) rotateX(0)';
});
}
}
Fonts not loading:
Animations not triggering:
.visible class is being addedScroll snap not working:
scroll-snap-type on html/bodyscroll-snap-align: startMobile issues:
Performance issues:
will-change sparinglytransform and opacity animations./assets folderlogo.png → USABLE → title/closing slidechat_ui.png → USABLE → feature slidedashboard.png → USABLE → feature slidelaunch_card.png → USABLE → feature slideblurry_team.jpg → NOT USABLE (too low resolution)