Constraints
- Self-contained: one HTML file with inline CSS and JS. No build step, no framework, no external tooling.
- Vanilla HTML/CSS/JS only — no React, no bundler, no Node scripts, no Python.
- Body limits: this skill stays well under 500 lines and 5000 tokens; the deck you produce has no hard size limit but every slide must fit one viewport.
- Viewport fit is a hard gate: every slide fits one viewport with no internal scrolling.
- Accessibility is required: semantic headings, readable contrast,
prefers-reduced-motion support.
Use when
- Building a talk deck, pitch deck, workshop deck, or internal presentation.
- Improving an existing HTML deck's layout, motion, or typography.
- You need a deck that runs from a local file and exports to PDF without installing anything.
Deck structure
One HTML file. One <section class="slide"> per slide inside a <main>. Theme values live in CSS custom properties so they are trivial to change.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Deck</title>
<style>
:root {
--bg: #0f172a; --fg: #f8fafc; --accent: #38bdf8;
--font: system-ui, sans-serif;
}
* { box-sizing: border-box; margin: 0; }
html, body { height: 100%; background: var(--bg); color: var(--fg); font-family: var(--font); }
/* One slide = one viewport. Active slide shown, others hidden. */
.slide {
position: fixed; inset: 0;
height: 100vh; height: 100dvh;
overflow: hidden;
display: none; flex-direction: column;
justify-content: center; align-items: flex-start;
padding: clamp(2rem, 6vw, 6rem);
gap: clamp(0.75rem, 2vh, 2rem);
}
.slide[aria-current="true"] { display: flex; }
/* Type and spacing scale with the viewport so nothing overflows. */
.slide h1 { font-size: clamp(2rem, 6vw, 5rem); line-height: 1.05; }
.slide h2 { font-size: clamp(1.5rem, 4vw, 3rem); }
.slide p, .slide li { font-size: clamp(1rem, 2.2vw, 1.6rem); line-height: 1.4; max-width: 60ch; }
.slide ul { padding-left: 1.2em; display: grid; gap: clamp(0.4rem, 1.2vh, 1rem); }
.accent { color: var(--accent); }
.progress {
position: fixed; bottom: 0; left: 0; height: 4px;
background: var(--accent); transition: width .3s ease;
}
/* Enter animation, disabled for reduced motion. */
.slide[aria-current="true"] > * { animation: rise .5s ease both; }
@keyframes rise { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) {
.slide[aria-current="true"] > *, .progress { animation: none; transition: none; }
}
</style>
</head>
<body>
<main>
<section class="slide" aria-current="true">
<h1>Deck <span class="accent">Title</span></h1>
<p>Subtitle or tagline.</p>
</section>
<section class="slide">
<h2>Key points</h2>
<ul><li>First point</li><li>Second point</li><li>Third point</li></ul>
</section>
</main>
<div class="progress" id="progress"></div>
<script>
const slides = [...document.querySelectorAll('.slide')];
let current = slides.findIndex(s => s.getAttribute('aria-current') === 'true');
if (current < 0) current = 0;
function show(i) {
current = Math.max(0, Math.min(slides.length - 1, i));
slides.forEach((s, n) => s.setAttribute('aria-current', String(n === current)));
document.getElementById('progress').style.width =
((current + 1) / slides.length * 100) + '%';
location.hash = '#' + (current + 1);
}
document.addEventListener('keydown', e => {
if (['ArrowRight', 'ArrowDown', 'PageDown', ' '].includes(e.key)) { e.preventDefault(); show(current + 1); }
if (['ArrowLeft', 'ArrowUp', 'PageUp'].includes(e.key)) { e.preventDefault(); show(current - 1); }
if (e.key === 'Home') show(0);
if (e.key === 'End') show(slides.length - 1);
});
const fromHash = parseInt(location.hash.slice(1), 10);
show(Number.isFinite(fromHash) ? fromHash - 1 : current);
</script>
</body>
</html>
Viewport fit (hard gate)
- Every
.slide uses height: 100vh; height: 100dvh; overflow: hidden;.
- All type and spacing scale with
clamp() — never a fixed pixel font that overflows small screens.
- When content does not fit, split it into multiple slides. Never shrink text below readable size and never allow a scrollbar inside a slide.
- Avoid invalid CSS such as a negated
-clamp(...).
Validate at: 1920x1080, 1280x720, 768x1024, 375x667, 667x375.
Typography and spacing
- Strong hierarchy: one dominant heading per slide, supporting text clearly smaller.
- Generous whitespace via
clamp()-based padding and gaps.
- A clear visual direction (atmospheric background, accent color) beats a generic template look.
Accessibility
- Semantic structure:
main, section, real h1/h2, ul/li.
- Readable contrast between
--fg and --bg.
- Keyboard-only navigation works (arrows, Page keys, Home/End).
- Honor
prefers-reduced-motion by disabling animation and transitions.
Content density limits
| Slide type |
Limit |
| Title |
1 heading + 1 subtitle + optional tagline |
| Content |
1 heading + 4–6 bullets or 2 short paragraphs |
| Feature grid |
6 cards max |
| Code |
8–10 lines max |
| Quote |
1 quote + attribution |
| Image |
1 image constrained by the viewport |
Export to PDF (no dependency)
The default path uses the browser's built-in Print-to-PDF. Add a print stylesheet so every slide becomes one page.
@media print {
@page { size: 1280px 720px; margin: 0; }
.progress { display: none; }
.slide {
position: static; display: flex !important;
height: 720px; width: 1280px;
break-after: page;
}
}
Then: open the deck in a browser, Print (Cmd/Ctrl+P), choose "Save as PDF", set margins to None and background graphics on.
Open the deck with the platform opener: macOS open deck.html, Linux xdg-open deck.html, Windows start "" deck.html.
Optional, if available: a headless browser (e.g. a Chromium --print-to-pdf invocation) can render the same print stylesheet to PDF unattended. Do not require or install it — the browser Print-to-PDF path always works.
Anti-patterns
- Generic gradient decks with no visual identity.
- Long bullet walls; code blocks that need scrolling.
- Fixed-height content boxes that break on short screens.
- Disabling reduced-motion support.
- Reaching for a framework or build tool when one HTML file suffices.
Done when
- The deck runs from a local file in any modern browser.
- Every slide fits the viewport at all five test sizes with no scrolling.
- Arrow/Page/Home/End keyboard navigation works and the progress bar tracks position.
- Reduced-motion is respected.
- Print-to-PDF produces one page per slide.
- File path, slide count, and theme custom properties are explained at handoff.
1---2name: frontend-slides3description: Use when building standalone HTML/CSS/JS presentation slide decks — self-contained single-file decks with viewport-fit layout, keyboard navigation, and browser Print-to-PDF export.4license: MIT5---67## Constraints8- Self-contained: one HTML file with inline CSS and JS. No build step, no framework, no external tooling.9- Vanilla HTML/CSS/JS only — no React, no bundler, no Node scripts, no Python.10- Body limits: this skill stays well under 500 lines and 5000 tokens; the deck you produce has no hard size limit but every slide must fit one viewport.11- Viewport fit is a hard gate: every slide fits one viewport with no internal scrolling.12- Accessibility is required: semantic headings, readable contrast, `prefers-reduced-motion` support.1314## Use when15- Building a talk deck, pitch deck, workshop deck, or internal presentation.16- Improving an existing HTML deck's layout, motion, or typography.17- You need a deck that runs from a local file and exports to PDF without installing anything.1819## Deck structure20One HTML file. One `<section class="slide">` per slide inside a `<main>`. Theme values live in CSS custom properties so they are trivial to change.2122```html23<!doctype html>24<html lang="en">25<head>26<meta charset="utf-8">27<meta name="viewport" content="width=device-width, initial-scale=1">28<title>Deck</title>29<style>30:root {31 --bg: #0f172a; --fg: #f8fafc; --accent: #38bdf8;32 --font: system-ui, sans-serif;33}34* { box-sizing: border-box; margin: 0; }35html, body { height: 100%; background: var(--bg); color: var(--fg); font-family: var(--font); }3637/* One slide = one viewport. Active slide shown, others hidden. */38.slide {39 position: fixed; inset: 0;40 height: 100vh; height: 100dvh;41 overflow: hidden;42 display: none; flex-direction: column;43 justify-content: center; align-items: flex-start;44 padding: clamp(2rem, 6vw, 6rem);45 gap: clamp(0.75rem, 2vh, 2rem);46}47.slide[aria-current="true"] { display: flex; }4849/* Type and spacing scale with the viewport so nothing overflows. */50.slide h1 { font-size: clamp(2rem, 6vw, 5rem); line-height: 1.05; }51.slide h2 { font-size: clamp(1.5rem, 4vw, 3rem); }52.slide p, .slide li { font-size: clamp(1rem, 2.2vw, 1.6rem); line-height: 1.4; max-width: 60ch; }53.slide ul { padding-left: 1.2em; display: grid; gap: clamp(0.4rem, 1.2vh, 1rem); }5455.accent { color: var(--accent); }5657.progress {58 position: fixed; bottom: 0; left: 0; height: 4px;59 background: var(--accent); transition: width .3s ease;60}6162/* Enter animation, disabled for reduced motion. */63.slide[aria-current="true"] > * { animation: rise .5s ease both; }64@keyframes rise { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } }65@media (prefers-reduced-motion: reduce) {66 .slide[aria-current="true"] > *, .progress { animation: none; transition: none; }67}68</style>69</head>70<body>71<main>72 <section class="slide" aria-current="true">73 <h1>Deck <span class="accent">Title</span></h1>74 <p>Subtitle or tagline.</p>75 </section>76 <section class="slide">77 <h2>Key points</h2>78 <ul><li>First point</li><li>Second point</li><li>Third point</li></ul>79 </section>80</main>81<div class="progress" id="progress"></div>82<script>83const slides = [...document.querySelectorAll('.slide')];84let current = slides.findIndex(s => s.getAttribute('aria-current') === 'true');85if (current < 0) current = 0;8687function show(i) {88 current = Math.max(0, Math.min(slides.length - 1, i));89 slides.forEach((s, n) => s.setAttribute('aria-current', String(n === current)));90 document.getElementById('progress').style.width =91 ((current + 1) / slides.length * 100) + '%';92 location.hash = '#' + (current + 1);93}9495document.addEventListener('keydown', e => {96 if (['ArrowRight', 'ArrowDown', 'PageDown', ' '].includes(e.key)) { e.preventDefault(); show(current + 1); }97 if (['ArrowLeft', 'ArrowUp', 'PageUp'].includes(e.key)) { e.preventDefault(); show(current - 1); }98 if (e.key === 'Home') show(0);99 if (e.key === 'End') show(slides.length - 1);100});101102const fromHash = parseInt(location.hash.slice(1), 10);103show(Number.isFinite(fromHash) ? fromHash - 1 : current);104</script>105</body>106</html>107```108109## Viewport fit (hard gate)110- Every `.slide` uses `height: 100vh; height: 100dvh; overflow: hidden;`.111- All type and spacing scale with `clamp()` — never a fixed pixel font that overflows small screens.112- When content does not fit, split it into multiple slides. Never shrink text below readable size and never allow a scrollbar inside a slide.113- Avoid invalid CSS such as a negated `-clamp(...)`.114115Validate at: 1920x1080, 1280x720, 768x1024, 375x667, 667x375.116117## Typography and spacing118- Strong hierarchy: one dominant heading per slide, supporting text clearly smaller.119- Generous whitespace via `clamp()`-based padding and gaps.120- A clear visual direction (atmospheric background, accent color) beats a generic template look.121122## Accessibility123- Semantic structure: `main`, `section`, real `h1`/`h2`, `ul`/`li`.124- Readable contrast between `--fg` and `--bg`.125- Keyboard-only navigation works (arrows, Page keys, Home/End).126- Honor `prefers-reduced-motion` by disabling animation and transitions.127128## Content density limits129130| Slide type | Limit |131|------------|-------|132| Title | 1 heading + 1 subtitle + optional tagline |133| Content | 1 heading + 4–6 bullets or 2 short paragraphs |134| Feature grid | 6 cards max |135| Code | 8–10 lines max |136| Quote | 1 quote + attribution |137| Image | 1 image constrained by the viewport |138139## Export to PDF (no dependency)140The default path uses the browser's built-in Print-to-PDF. Add a print stylesheet so every slide becomes one page.141142```css143@media print {144 @page { size: 1280px 720px; margin: 0; }145 .progress { display: none; }146 .slide {147 position: static; display: flex !important;148 height: 720px; width: 1280px;149 break-after: page;150 }151}152```153154Then: open the deck in a browser, Print (Cmd/Ctrl+P), choose "Save as PDF", set margins to None and background graphics on.155156Open the deck with the platform opener: macOS `open deck.html`, Linux `xdg-open deck.html`, Windows `start "" deck.html`.157158Optional, if available: a headless browser (e.g. a Chromium `--print-to-pdf` invocation) can render the same print stylesheet to PDF unattended. Do not require or install it — the browser Print-to-PDF path always works.159160## Anti-patterns161- Generic gradient decks with no visual identity.162- Long bullet walls; code blocks that need scrolling.163- Fixed-height content boxes that break on short screens.164- Disabling reduced-motion support.165- Reaching for a framework or build tool when one HTML file suffices.166167## Done when168- The deck runs from a local file in any modern browser.169- Every slide fits the viewport at all five test sizes with no scrolling.170- Arrow/Page/Home/End keyboard navigation works and the progress bar tracks position.171- Reduced-motion is respected.172- Print-to-PDF produces one page per slide.173- File path, slide count, and theme custom properties are explained at handoff.