skills/html-presentations/SKILL.md
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 skillsauth add ericmjl/skills html-presentationsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Create self-contained HTML slide presentations. No frameworks, no CDN, no build step — one .html file you can open in any browser.
Use this skill when the user asks for a presentation, slide deck, or slides and wants a single HTML file with vanilla HTML, CSS, and JavaScript. Not for reveal.js or framework-based presentations. Before generating slides, ask about goals, audience, and delivery mode (Step 0); use the answers to shape length, emphasis, and content density.
None. Output is a single .html file. Works in any modern browser (Chrome, Firefox, Safari, Edge).
.html file#5). Refreshing the page keeps you on the same slide; sharing a link with a hash opens that slide directlyStudy these full presentations to see what the skill can produce:
assets/example-agent-skills-overview.html — Polished overview with spotlight layout, two-column grids, rich SVG diagrams, pill badges, progress nav, and overview grid. Use it as a quality benchmark for structure and visuals.
assets/canvas-chat-overview.html — Product overview with interactive, looping slide demos:
slidechange dispatched with setTimeout(0) so demo scripts can run on loadUse the Canvas Chat overview when the user wants automated, looping demos (cursor, click effects, zoom, or step-by-step UI simulation) instead of static diagrams.
Do not generate slides until you have clarified the following with the user. Ask conversationally; one or two questions per message is fine. Use answers to decide length, emphasis, and how much text vs. diagrams to use.
(a) Goals of the presentation
If the user hasn't stated a clear goal, offer choices so they can pick or mix:
(b) Intended audience
Ask who will see it (e.g. executives, engineers, clients, students, general public). This drives jargon level, depth, and what to spell out vs. assume.
(c) Delivery mode
(d) Other useful questions
Record the user's answers and use them when choosing theme, building the outline, and deciding how much to put on each slide.
Read one of the theme reference files and paste its CSS into the presentation's <style> block. Default to catppuccin unless the user requests otherwise.
| 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 |
All themes define the same CSS custom properties (--bg, --fg, --accent, etc.), so the base layout CSS and all diagrams work with any theme.
Use this skeleton. The three sections — theme CSS, base layout CSS, and navigation JS — are always the same structure; only slide content varies.
<!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>
Title slide — centered, large heading, subtitle:
<section class="slide" id="title">
<h1>Title Text</h1>
<p>Subtitle or author</p>
</section>
Content slide — heading + body:
<section class="slide" id="topic-name">
<h2>Heading</h2>
<p>Body text or a list:</p>
<ul>
<li>Item</li>
</ul>
</section>
Two-column slide — use inline CSS grid:
<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>
Diagram slide — inline SVG using diagram classes:
<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>
Spotlight / feature slide — two-column grid with text on the left and a large SVG on the right. Use for showcasing a tool, feature, or concept with visual impact:
<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>
Spotlight design principles:
viewBox should be generous (e.g. 0 0 400 340) so the diagram fills the right half of the slide. Tiny SVGs with excess whitespace look unfinished.<br>) and keyword pills below.background:var(--accent); color:white; font-weight:600 for the primary keyword; keep the rest neutral (var(--surface) / var(--fg)).Supplementary slide — detail, FAQ, or reference slides placed after the Thank You slide. Linked from main slides via 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>
Supplementary slide conventions:
data-supplementary. Add this attribute to the <section>. The JS uses it to insert a visual separator in the progress bar and dim supplementary dots.<span class="supp-label">. Shows "Supplementary" or "Supplementary · FAQ" badge at the top of the slide.<a class="supp-link" data-goto="supp-topic">Detail →</a> in any main slide. The JS intercepts clicks and jumps to the target slide by ID.supp- (e.g. supp-detail, supp-harness).Required CSS (already in the base layout above):
.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; }
Required JS additions (already in the base script above):
slideIdMap mapping each slide's id to its index.data-goto link click: show(slideIdMap[target])..progress-sep <div> before the first supplementary slide..supplementary class to dots and overview cards for data-supplementary slides.#5). On load, call show(getSlideIndexFromHash()) instead of show(0) so that refreshing the page keeps the user on the same slide. In show(idx), call updateHashForSlide(current) (using history.replaceState when available) so the URL always reflects the current slide. Add a hashchange listener to sync the deck when the user edits the URL or uses browser back/forward.Every <section> must have a unique id.
A well-structured presentation follows this order:
The progress bar visually separates main from supplementary with a thin vertical line.
Read references/svg-diagrams.md for complete patterns:
All diagram SVG elements use the .diagram-* CSS classes defined in the base layout, so they automatically match the active theme.
Arrowhead markers — every SVG that uses arrows needs this <defs> block:
<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>
Use marker-end="url(#arrow)" on .diagram-edge lines and marker-end="url(#arrow-accent)" on .diagram-edge-accent lines.
Read references/svg-animations.md for complete patterns and guidance on when each animation type is appropriate.
Three built-in animation classes (defined in the base CSS):
| Class | Effect | Trigger |
|-------|--------|---------|
| .anim-fade | Fade up into view | Slide becomes active |
| .anim-draw | SVG path draws itself (stroke-dashoffset) | Slide becomes active |
| .anim-pulse | Pulsing glow on an SVG element | Slide becomes active |
Stagger timing with inline --delay:
<p class="anim-fade" style="--delay:0.2s">Appears first</p>
<p class="anim-fade" style="--delay:0.5s">Appears second</p>
For SVG path drawing, set --len to the path's total length:
<path class="diagram-edge anim-draw" style="--len:350"
d="M 50,100 C 150,50 250,150 350,100" />
When to use animations (summary — see reference file for full guidance):
font-size: 2.5vw or larger.vh/vw units for padding and margins so spacing scales with the viewport.--heading for titles, --fg for body, --muted for secondary text, --accent for emphasis.<pre><code> with the theme's --code-bg/--code-fg. Keep code short (< 15 lines per slide).uv run script.py so it is self-contained and does not require external software environments. Use PEP 723 inline script metadata at the top of the script (# /// script, requires-python, dependencies, # ///) and state in the slide or speaker notes that the audience runs it with uv run script.py.grid-template-columns: 1fr 1fr) to balance text and diagrams side by side, with align-items:center to vertically center both columns.<br>) and keyword pills beneath. This is punchier and more scannable.viewBox dimensions and fill the space with detail: multiple nodes, labels, phases, wireframe lines, loop-back arrows. A tiny diagram floating in whitespace looks unfinished.development
Create animated videos using Remotion from topics, product URLs, Google reviews, talking-head videos, or CSV data. Supports 5 video types: educational explainers, product launch demos, testimonial/social proof, avatar video overlays, and data visualization dashboards. Each follows a 2-step workflow: research/scrape/analyze then design and animate with spring animations, SVG diagrams, and count-up effects. Requires the Remotion best practices skill (install with `npx skills add remotion-dev/skills`). Use when the user asks to create a Remotion video, explainer video, educational video, product demo video, testimonial video, video with animated overlays, data visualization video, animated dashboard, or short-form vertical video for mobile.
development
Comprehensive YouTube operations using yt-dlp - download videos/audio, extract transcripts and subtitles, get metadata, work with playlists, download thumbnails, and inspect available formats. Use this for any YouTube content processing task.
data-ai
Ingest YouTube videos into the vault. Triggers when user pastes a YouTube URL (youtube.com/watch or youtu.be). Fetches transcript using yt-dlp, extracts metadata, creates transcript note and summary note. User may provide additional context about the video.
tools
Advanced negotiation and communication advisor grounded in Chris Voss's tactical empathy methodology (Never Split the Difference, The Black Swan Group). Use this skill whenever the user needs help with any interpersonal situation involving influence, persuasion, or navigating difficult dynamics. This includes but is not limited to: analyzing conversations, call transcripts, or email threads; preparing for negotiations (salary, vendor, client, partner); drafting tactful responses; handling pushback, objections, or conflict; navigating difficult workplace conversations; preparing for performance reviews or raises; buying a car, house, or any big purchase; dealing with landlords, contractors, or service providers; resolving personal disagreements; practicing negotiation through role-play; or any situation where the user says things like "how should I respond to this", "they're pushing back", "I need to have a tough conversation", "how do I ask for...", "they ghosted me", "I'm not sure how to handle this person", "counter-offer", "pricing", "deal", "objection", or "difficult conversation". Activate broadly — most interpersonal communication benefits from tactical empathy whether or not the user frames it as "negotiation." This skill integrates FBI hostage negotiation techniques (93% success rate) with behavioral economics (Kahneman's Prospect Theory) and neuroscience (amygdala hijacking, loss aversion).