Loading...
Loading...
Create single-file HTML slide presentations with vanilla JS/CSS. Themed (terminal.css, catppuccin, nord), keyboard-navigable, with inline SVG diagrams and animations. Use when the user asks for an HTML presentation, slide deck, or single-file slides without a framework.
npx skill4agent add ericmjl/skills html-presentations.html.html.html#5slidechangesetTimeout(0)<style>| Theme | File | Aesthetic |
|---|---|---|
| terminal.css | references/theme-terminal.css | Green phosphor on black, monospace, scanlines |
| catppuccin | references/theme-catppuccin.css | Pastel accents on warm dark base (Mocha variant) |
| nord | references/theme-nord.css | Arctic blue-gray, muted palette |
--bg--fg--accent<!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>
<style>
/* ===== Theme (paste from reference file) ===== */
:root { /* ... theme variables ... */ }
/* ===== Base layout ===== */
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
html, body { height: 100%; overflow: hidden; }
body {
background: var(--bg);
color: var(--fg);
font-family: var(--body-font);
}
.deck { position: relative; width: 100vw; height: 100vh; overflow: hidden; }
.slide {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
justify-content: center;
padding: 6vh 8vw;
opacity: 0;
transition: opacity 0.5s ease;
pointer-events: none;
overflow: hidden;
}
.slide.active {
opacity: 1;
pointer-events: auto;
}
/* ===== Typography ===== */
h1 { font-size: 3.5vw; font-family: var(--heading-font); color: var(--heading); margin-bottom: 2vh; }
h2 { font-size: 2.8vw; font-family: var(--heading-font); color: var(--heading); margin-bottom: 2vh; }
h3 { font-size: 2.2vw; font-family: var(--heading-font); color: var(--heading); margin-bottom: 1.5vh; }
p, li { font-size: 1.6vw; line-height: 1.7; margin-bottom: 1vh; }
ul, ol { padding-left: 2vw; }
code {
font-family: var(--mono-font);
background: var(--surface);
padding: 0.15em 0.4em;
border-radius: 4px;
font-size: 0.9em;
}
pre {
background: var(--surface);
padding: 2vh 2vw;
border-radius: 8px;
overflow-x: auto;
}
pre code { background: none; padding: 0; font-size: 1.3vw; }
/* ===== Bottom nav bar (arrows + progress dots) ===== */
.nav-bar {
position: fixed;
bottom: 2.5vh;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 12px;
align-items: center;
z-index: 100;
}
.progress {
display: flex;
gap: 6px;
align-items: center;
}
.progress-dot {
width: 8px;
height: 8px;
border-radius: 4px;
background: var(--muted);
transition: width 0.35s ease, background 0.35s ease;
cursor: pointer;
}
.progress-dot.active {
width: 32px;
background: var(--accent);
}
.nav-arrow {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 36px;
height: 32px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--muted);
cursor: pointer;
transition: color 0.2s, border-color 0.2s;
user-select: none;
flex-shrink: 0;
}
.nav-arrow:hover {
color: var(--accent);
border-color: var(--accent);
}
.nav-arrow .arrow {
font-size: 1vw;
line-height: 1;
font-family: var(--body-font);
}
.nav-arrow .key {
font-size: 0.55vw;
font-family: var(--mono-font);
opacity: 0.7;
}
/* ===== Overview grid (Escape) ===== */
.overview-overlay {
position: fixed;
inset: 0;
background: var(--bg);
z-index: 200;
display: none;
align-items: center;
justify-content: center;
padding: 3vh 3vw;
overflow: auto;
}
.overview-overlay.visible {
display: flex;
flex-direction: column;
}
.overview-overlay .overview-hint {
font-size: 1.2vw;
color: var(--muted);
margin-bottom: 2vh;
}
.overview-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 1rem;
max-width: 90vw;
}
.overview-card {
background: var(--surface);
border: 2px solid var(--border);
border-radius: 8px;
padding: 1rem;
cursor: pointer;
text-align: center;
transition: border-color 0.2s, transform 0.2s;
}
.overview-card:hover {
border-color: var(--accent);
transform: scale(1.02);
}
.overview-card.active {
border-color: var(--accent);
background: var(--surface2);
}
.overview-card .num {
font-size: 1.5vw;
font-weight: 700;
color: var(--accent);
display: block;
}
.overview-card .title {
font-size: 0.9vw;
margin-top: 0.5rem;
color: var(--fg);
line-height: 1.3;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* ===== Supplementary slides ===== */
.supp-label {
display: inline-block; font-size: 0.85vw; text-transform: uppercase;
letter-spacing: 0.15em; color: var(--muted); border: 1px solid var(--border);
border-radius: 4px; padding: 0.2em 0.7em; margin-bottom: 2vh;
}
.supp-link {
color: var(--accent); text-decoration: none;
border-bottom: 1px dashed var(--accent); cursor: pointer;
}
.supp-link:hover { border-bottom-style: solid; }
.progress-sep {
width: 1px; height: 12px; background: var(--border); margin: 0 4px; flex-shrink: 0;
}
.progress-dot.supplementary { opacity: 0.5; }
.progress-dot.supplementary.active { opacity: 1; }
.overview-card.supplementary { opacity: 0.7; }
/* ===== Diagram base classes ===== */
.diagram-node { fill: var(--diagram-fill); stroke: var(--diagram-stroke); stroke-width: 2; }
.diagram-highlight { fill: var(--diagram-highlight); stroke: var(--diagram-highlight); stroke-width: 2; }
.diagram-diamond { fill: var(--surface2); stroke: var(--diagram-stroke); stroke-width: 2; }
.diagram-label { fill: var(--diagram-text); font-family: var(--body-font); font-size: 14px; text-anchor: middle; dominant-baseline: central; }
.diagram-label-inv { fill: var(--bg); font-family: var(--body-font); font-size: 14px; text-anchor: middle; dominant-baseline: central; }
.diagram-label-sm { fill: var(--diagram-text); font-family: var(--body-font); font-size: 11px; text-anchor: middle; dominant-baseline: central; opacity: 0.7; }
.diagram-edge { stroke: var(--diagram-arrow); stroke-width: 2; fill: none; }
.diagram-edge-accent { stroke: var(--diagram-stroke); stroke-width: 2; fill: none; }
.slide svg { max-width: 100%; height: auto; }
/* ===== Animation base classes ===== */
.slide .anim-fade {
opacity: 0;
transform: translateY(20px);
}
.slide.active .anim-fade {
opacity: 1;
transform: translateY(0);
transition: opacity 0.6s ease var(--delay, 0s),
transform 0.6s ease var(--delay, 0s);
}
.slide .anim-draw {
stroke-dasharray: var(--len, 1000);
stroke-dashoffset: var(--len, 1000);
}
.slide.active .anim-draw {
stroke-dashoffset: 0;
transition: stroke-dashoffset 1.2s ease-in-out var(--delay, 0s);
}
@keyframes pulse-glow {
0%, 100% { filter: drop-shadow(0 0 3px var(--diagram-stroke)); }
50% { filter: drop-shadow(0 0 10px var(--diagram-stroke)); }
}
.slide.active .anim-pulse {
animation: pulse-glow 2s ease-in-out infinite;
animation-delay: var(--delay, 0s);
}
/* Additional theme-specific rules (paste from theme file if any) */
</style>
</head>
<body>
<div class="deck">
<section class="slide" id="title">
<h1>Presentation Title</h1>
<p>Author — Date</p>
</section>
<section class="slide" id="slide-2">
<h2>Slide Heading</h2>
<ul>
<li>Point one</li>
<li>Point two</li>
</ul>
</section>
<!-- more <section class="slide"> elements -->
</div>
<div class="nav-bar">
<div class="nav-arrow nav-prev"><span class="arrow">‹</span><span class="key">j</span></div>
<div class="progress"></div>
<div class="nav-arrow nav-next"><span class="arrow">›</span><span class="key">k</span></div>
</div>
<div class="overview-overlay" id="overview" aria-label="Slide overview">
<p class="overview-hint">Click a slide to jump · Esc to close</p>
<div class="overview-grid"></div>
</div>
<script>
(function () {
var slides = document.querySelectorAll('.slide');
var current = 0;
var total = slides.length;
var progress = document.querySelector('.progress');
var prevBtn = document.querySelector('.nav-prev');
var nextBtn = document.querySelector('.nav-next');
var overview = document.getElementById('overview');
var overviewGrid = overview.querySelector('.overview-grid');
var suppInserted = false;
for (var i = 0; i < total; i++) {
if (!suppInserted && slides[i].hasAttribute('data-supplementary')) {
var sep = document.createElement('div');
sep.className = 'progress-sep';
progress.appendChild(sep);
suppInserted = true;
}
var dot = document.createElement('div');
dot.className = 'progress-dot';
if (slides[i].hasAttribute('data-supplementary')) dot.classList.add('supplementary');
dot.dataset.index = i;
dot.addEventListener('click', function () {
show(parseInt(this.dataset.index));
});
progress.appendChild(dot);
}
var dots = progress.querySelectorAll('.progress-dot');
for (var i = 0; i < total; i++) {
var card = document.createElement('button');
card.type = 'button';
card.className = 'overview-card';
if (slides[i].hasAttribute('data-supplementary')) card.classList.add('supplementary');
card.dataset.index = i;
var titleEl = slides[i].querySelector('h1, h2, h3');
var title = titleEl ? titleEl.textContent.trim() : 'Slide ' + (i + 1);
card.innerHTML = '<span class="num">' + (i + 1) + '</span><span class="title">' + title.replace(/</g, '<') + '</span>';
card.addEventListener('click', function () {
show(parseInt(this.dataset.index));
overview.classList.remove('visible');
});
overviewGrid.appendChild(card);
}
var overviewCards = overviewGrid.querySelectorAll('.overview-card');
var slideIdMap = {};
for (var i = 0; i < total; i++) {
if (slides[i].id) slideIdMap[slides[i].id] = i;
}
function getSlideIndexFromHash() {
var hash = window.location.hash;
if (!hash || hash.length < 2) return 0;
var n = parseInt(hash.slice(1), 10);
if (isNaN(n) || n < 0 || n >= total) return 0;
return n;
}
function updateHashForSlide(idx) {
var url = window.location.pathname + window.location.search + '#' + idx;
if (window.history.replaceState) {
window.history.replaceState(null, '', url);
} else {
window.location.hash = idx;
}
}
function show(idx) {
if (idx < 0 || idx >= total) return;
slides[current].classList.remove('active');
dots[current].classList.remove('active');
if (overviewCards[current]) overviewCards[current].classList.remove('active');
current = idx;
slides[current].classList.add('active');
dots[current].classList.add('active');
if (overviewCards[current]) overviewCards[current].classList.add('active');
updateHashForSlide(current);
}
document.addEventListener('click', function (e) {
var link = e.target.closest('[data-goto]');
if (link) {
e.preventDefault();
var target = link.getAttribute('data-goto');
if (slideIdMap[target] !== undefined) show(slideIdMap[target]);
}
});
prevBtn.addEventListener('click', function () { show(current - 1); });
nextBtn.addEventListener('click', function () { show(current + 1); });
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape') {
overview.classList.toggle('visible');
if (overview.classList.contains('visible')) {
for (var i = 0; i < overviewCards.length; i++) {
overviewCards[i].classList.toggle('active', i === current);
}
}
return;
}
if (overview.classList.contains('visible')) return;
switch (e.key) {
case ' ':
e.preventDefault();
show(current + (e.shiftKey ? -1 : 1));
break;
case 'ArrowRight': case 'k':
show(current + 1);
break;
case 'ArrowLeft': case 'j':
show(current - 1);
break;
}
});
window.addEventListener('hashchange', function () {
var idx = getSlideIndexFromHash();
if (idx !== current) show(idx);
});
show(getSlideIndexFromHash());
})();
</script>
</body>
</html><section class="slide" id="title">
<h1>Title Text</h1>
<p>Subtitle or author</p>
</section><section class="slide" id="topic-name">
<h2>Heading</h2>
<p>Body text or a list:</p>
<ul>
<li>Item</li>
</ul>
</section><section class="slide" id="comparison">
<h2>Side by Side</h2>
<div style="display:grid; grid-template-columns:1fr 1fr; gap:4vw; flex:1;">
<div>Left column content</div>
<div>Right column content</div>
</div>
</section><section class="slide" id="architecture">
<h2>System Architecture</h2>
<svg viewBox="0 0 800 400">
<!-- diagram elements using .diagram-node, .diagram-edge, etc. -->
</svg>
</section><section class="slide" id="spotlight-feature" style="justify-content:center;">
<div style="display:grid; grid-template-columns:1fr 1fr; gap:4vw; align-items:center;">
<div>
<p class="dim anim-fade" style="--delay:0.1s; font-size:1vw; text-transform:uppercase; letter-spacing:0.2em; margin-bottom:1vh;">Label</p>
<h2 class="anim-fade" style="--delay:0.2s; font-size:3.2vw;"><code style="background:var(--surface); padding:0.15em 0.4em; border-radius:6px;">Feature Name</code></h2>
<p class="anim-fade" style="--delay:0.4s; font-size:2.4vw; color:var(--accent); margin-top:2vh; line-height:1.3;">Short tagline.<br>One line per idea.</p>
<div class="anim-fade" style="--delay:0.7s; display:flex; gap:0.8vw; flex-wrap:wrap; margin-top:3vh;">
<span style="background:var(--accent); color:white; padding:0.4em 1em; border-radius:999px; font-size:1.1vw; font-weight:600;">Primary pill</span>
<span style="background:var(--surface); color:var(--fg); padding:0.4em 1em; border-radius:999px; font-size:1.1vw;">Secondary pill</span>
<span style="background:var(--surface); color:var(--fg); padding:0.4em 1em; border-radius:999px; font-size:1.1vw;">Another pill</span>
</div>
</div>
<div class="anim-fade" style="--delay:0.5s;">
<svg viewBox="0 0 400 340">
<!-- Large, detailed diagram that fills the right half -->
</svg>
</div>
</div>
</section>viewBox0 0 400 340<br>background:var(--accent); color:white; font-weight:600var(--surface)var(--fg)data-goto<section class="slide" id="supp-topic" data-supplementary>
<span class="supp-label">Supplementary</span>
<h2>Detailed Topic</h2>
<p>Deep-dive content that supports a main slide.</p>
</section>data-supplementary<section><span class="supp-label"><a class="supp-link" data-goto="supp-topic">Detail →</a>supp-supp-detailsupp-harness.supp-label {
display: inline-block; font-size: 0.85vw; text-transform: uppercase;
letter-spacing: 0.15em; color: var(--muted); border: 1px solid var(--border);
border-radius: 4px; padding: 0.2em 0.7em; margin-bottom: 2vh;
}
.supp-link {
color: var(--accent); text-decoration: none;
border-bottom: 1px dashed var(--accent); cursor: pointer;
}
.supp-link:hover { border-bottom-style: solid; }
.progress-sep {
width: 1px; height: 12px; background: var(--border); margin: 0 4px; flex-shrink: 0;
}
.progress-dot.supplementary { opacity: 0.5; }
.progress-dot.supplementary.active { opacity: 1; }slideIdMapiddata-gotoshow(slideIdMap[target]).progress-sep<div>.supplementarydata-supplementary#5show(getSlideIndexFromHash())show(0)show(idx)updateHashForSlide(current)history.replaceStatehashchange<section>id.diagram-*<defs><defs>
<marker id="arrow" markerWidth="10" markerHeight="7"
refX="9" refY="3.5" orient="auto-start-reverse">
<polygon points="0 0, 10 3.5, 0 7" fill="var(--diagram-arrow)" />
</marker>
<marker id="arrow-accent" markerWidth="10" markerHeight="7"
refX="9" refY="3.5" orient="auto-start-reverse">
<polygon points="0 0, 10 3.5, 0 7" fill="var(--diagram-stroke)" />
</marker>
<filter id="shadow" x="-5%" y="-5%" width="110%" height="120%">
<feDropShadow dx="0" dy="2" stdDeviation="3" flood-opacity="0.15" />
</filter>
</defs>marker-end="url(#arrow)".diagram-edgemarker-end="url(#arrow-accent)".diagram-edge-accent| Class | Effect | Trigger |
|---|---|---|
| Fade up into view | Slide becomes active |
| SVG path draws itself (stroke-dashoffset) | Slide becomes active |
| Pulsing glow on an SVG element | Slide becomes active |
--delay<p class="anim-fade" style="--delay:0.2s">Appears first</p>
<p class="anim-fade" style="--delay:0.5s">Appears second</p>--len<path class="diagram-edge anim-draw" style="--len:350"
d="M 50,100 C 150,50 250,150 350,100" />font-size: 2.5vwvhvw--heading--fg--muted--accent<pre><code>--code-bg--code-fguv run script.py# /// scriptrequires-pythondependencies# ///uv run script.pygrid-template-columns: 1fr 1fralign-items:center<br>viewBox