
Revealjs
- 638 installs
- 380 repo stars
- Updated May 2, 2026
- ryanbbrown/revealjs-skill
revealjs is a Claude Code skill that instantly generates polished reveal.js HTML presentations with themes, speaker notes, and custom styling for developers who need browser-ready slide decks without a build step.
About
revealjs is a presentation generator skill that produces professional reveal.js slide decks as standalone HTML plus a companion CSS file loaded from CDN. Developers reach for it when asked to create slides, a presentation, a deck, or a slideshow with themes, multi-column layouts, syntax-highlighted code blocks, animations, and speaker notes. No npm build or bundler is required—open the HTML file directly in a browser. The skill enforces design principles for typography, color, and layout so generated decks look polished for engineering talks, architecture reviews, and sprint demos.
- Generates self-contained HTML + CSS presentations using reveal.js from CDN with no build step
- Supports themes, multi-column layouts, code highlighting, animations, and speaker notes
- Analyzes content then states design approach before writing any code
- Enforces web-safe fonts, strong contrast, clear visual hierarchy and readability
- Creates custom CSS for branding, colors, typography and component styling
Revealjs by the numbers
- 638 all-time installs (skills.sh)
- +8 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #350 of 1,879 Documentation skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/ryanbbrown/revealjs-skill --skill revealjsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 638 |
|---|---|
| repo stars | ★ 380 |
| Last updated | May 2, 2026 |
| Repository | ryanbbrown/revealjs-skill ↗ |
How do you create reveal.js HTML slide decks?
Instantly generate polished, professional reveal.js HTML presentations with themes, speaker notes, and custom styling.
Who is it for?
Developers who need fast, professional HTML slide decks for technical talks without configuring a slide build pipeline.
Skip if: Skip revealjs when you need PowerPoint binaries, Google Slides collaboration, or React-based presentation frameworks like Spectacle.
When should I use this skill?
A user asks to create slides, a presentation, a deck, or a reveal.js slideshow.
What you get
Standalone HTML presentation file, custom CSS stylesheet, and embedded speaker notes ready for browser playback.
- HTML slide deck
- custom CSS stylesheet
Files
Reveal.js Presentations
Create HTML presentations using reveal.js. No build step required - just open the HTML in a browser.
What You Create
A reveal.js presentation consists of:
1. HTML file - Contains slides and loads reveal.js from CDN 2. CSS file - Custom styles for layouts, colors, typography, and components
Design Principles
CRITICAL: Before creating any presentation, analyze the content and choose appropriate design elements:
1. Consider the subject matter: What is this presentation about? What tone, industry, or mood does it suggest? 2. Check for branding: If the user mentions a company/organization, consider their brand colors and identity 3. Match palette to content: Select colors that reflect the subject 4. State your approach: Explain your design choices before writing code
Requirements:
- ✅ State your content-informed design approach BEFORE writing code
- ✅ Use web-safe fonts (Arial, Helvetica, Georgia, Verdana, etc.) or Google Fonts via
@importin CSS - ✅ Create clear visual hierarchy through size, weight, and color
- ✅ Ensure readability: strong contrast, appropriately sized text, clean alignment
- ✅ Be consistent: repeat patterns, spacing, and visual language across slides
- ✅ Always use `pt` (points) for font sizes - slides are fixed-size, so
ptis predictable and familiar (like PowerPoint/Keynote). Never useem,rem, orpxfor font sizes.
Color Palette Selection
Choosing colors creatively:
- Think beyond defaults: What colors genuinely match this specific topic? Avoid autopilot choices.
- Consider multiple angles: Topic, industry, mood, energy level, target audience, brand identity (if mentioned)
- Be adventurous: Try unexpected combinations - a healthcare presentation doesn't have to be green, finance doesn't have to be navy
- Build your palette: Pick 3-5 colors that work together (dominant colors + supporting tones + accent)
- Ensure contrast: Text must be clearly readable on backgrounds
Example color palettes (use these to spark creativity - choose one, adapt it, or create your own):
1. Classic Blue: Deep navy (#1C2833), slate gray (#2E4053), silver (#AAB7B8), off-white (#F4F6F6) 2. Teal & Coral: Teal (#5EA8A7), deep teal (#277884), coral (#FE4447), white (#FFFFFF) 3. Bold Red: Red (#C0392B), bright red (#E74C3C), orange (#F39C12), yellow (#F1C40F), green (#2ECC71) 4. Warm Blush: Mauve (#A49393), blush (#EED6D3), rose (#E8B4B8), cream (#FAF7F2) 5. Burgundy Luxury: Burgundy (#5D1D2E), crimson (#951233), rust (#C15937), gold (#997929) 6. Deep Purple & Emerald: Purple (#B165FB), dark blue (#181B24), emerald (#40695B), white (#FFFFFF) 7. Cream & Forest Green: Cream (#FFE1C7), forest green (#40695B), white (#FCFCFC) 8. Pink & Purple: Pink (#F8275B), coral (#FF574A), rose (#FF737D), purple (#3D2F68) 9. Lime & Plum: Lime (#C5DE82), plum (#7C3A5F), coral (#FD8C6E), blue-gray (#98ACB5) 10. Black & Gold: Gold (#BF9A4A), black (#000000), cream (#F4F6F6) 11. Sage & Terracotta: Sage (#87A96B), terracotta (#E07A5F), cream (#F4F1DE), charcoal (#2C2C2C) 12. Charcoal & Red: Charcoal (#292929), red (#E33737), light gray (#CCCBCB) 13. Vibrant Orange: Orange (#F96D00), light gray (#F2F2F2), charcoal (#222831) 14. Forest Green: Black (#191A19), green (#4E9F3D), dark green (#1E5128), white (#FFFFFF) 15. Retro Rainbow: Purple (#722880), pink (#D72D51), orange (#EB5C18), amber (#F08800), gold (#DEB600) 16. Vintage Earthy: Mustard (#E3B448), sage (#CBD18F), forest green (#3A6B35), cream (#F4F1DE) 17. Coastal Rose: Old rose (#AD7670), beaver (#B49886), eggshell (#F3ECDC), ash gray (#BFD5BE) 18. Orange & Turquoise: Light orange (#FC993E), grayish turquoise (#667C6F), white (#FCFCFC)
Slide Content Principles
Diverse presentation is key. Even when slides have similar content types, vary the visual presentation:
- Use different layouts across slides: columns on one, stacked containers on another, styled cards on a third
- Mix container styles: plain text, custom styled containers, blockquotes
- Use visual hierarchy:
<strong>for key terms, different colors to distinguish categories - Break up lists with other elements (quotes, styled containers, columns)
- Don't repeat the same layout pattern on consecutive slides
Keep it scannable:
- Short bullet points, not paragraphs
- One main idea per slide when possible
- Use icons (Font Awesome) to add visual interest
When a slide has less content, make it bigger - don't leave empty space with tiny text.
Workflow
Step 1: Plan the Structure
Based on the user's content, determine:
- How many slides are needed
- Which slides should be section dividers (centered, larger text)
- Where to use vertical slide stacks for drill-down content
Step 2: Generate the Scaffold
Use the create-presentation.js script (located in the scripts/ directory next to this SKILL.md file) to generate the HTML scaffold.
node <path-to-skill>/scripts/create-presentation.js --structure 1,1,d,3,1,d,1 --title "My Presentation" --output presentation.htmlFinding the script path: The script is at scripts/create-presentation.js relative to where this SKILL.md file is located. Common locations:
- Project skill:
.claude/skills/revealjs/scripts/create-presentation.js - User skill:
~/.claude/skills/revealjs/scripts/create-presentation.js
Options:
--slides N- Create N horizontal slides (simple mode)--structure <list>- Mixed layout with comma-separated values:1= single horizontal slideN(where N > 1) = vertical stack of N slidesd= section divider slide (centered, no content wrapper)--output <file>- Output filename (default: presentation.html)--title <text>- Presentation title--styles <file>- Custom CSS filename (default: styles.css)
Examples:
# 10 horizontal slides
node <path-to-skill>/scripts/create-presentation.js --slides 10 --output presentation.html
# Mixed structure: intro, 2 content slides, divider, 3-slide vertical stack, divider, closing
node <path-to-skill>/scripts/create-presentation.js --structure 1,1,1,d,3,d,1 --title "Q4 Review" --output presentation.htmlStep 3: Customize the CSS
The scaffold script automatically copies base-styles.css to your presentation directory as styles.css. Now customize the CSS variables (especially colors) for your presentation theme.
Using Google Fonts: Add an @import at the top of your CSS file:
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;600;700&family=Lato:wght@300;400;600&display=swap');
:root {
--heading-font: "Playfair Display", Georgia, serif;
--body-font: "Lato", Helvetica, sans-serif;
/* ... */
}The base file includes:
1. CSS Variables for easy customization:
:root {
/* ===========================================
BACKGROUND COLOR - Set this first!
=========================================== */
--background-color: #ffffff; /* Change for dark themes (e.g., #1a1a2e) */
/* Typography - ALWAYS use pt for font sizes */
--heading-font: "Source Sans Pro", Helvetica, sans-serif;
--body-font: "Source Sans Pro", Helvetica, sans-serif;
--base-font-size: 32px; /* Only px value - sets reveal.js base */
--text-size: 16pt; /* Base body text - intentionally small */
--h1-size: 48pt;
--h2-size: 36pt;
--h3-size: 24pt;
/* Colors - customize these for each presentation */
--primary-color: #2196F3;
--secondary-color: #ff9800;
--text-color: #222; /* Use light color (e.g., #FAF7F2) for dark backgrounds */
--muted-color: #666; /* Adjust for dark backgrounds too */
}2. Override reveal.js styles using .reveal prefix:
.reveal {
font-family: var(--body-font);
}
.reveal h1, .reveal h2, .reveal h3 {
font-family: var(--heading-font);
text-transform: none;
color: var(--text-color);
}
.reveal p, .reveal li {
font-size: var(--text-size);
color: var(--text-color);
}3. Slide layout styles - control padding and positioning:
.reveal .slides section {
padding: 40px 60px;
text-align: left;
}4. Text size utilities (use these to scale up text when slides have less content):
/* Base text is 16pt - use these classes to increase size when needed */
.text-lg { font-size: 18pt; } /* Slightly larger */
.text-xl { font-size: 20pt; } /* Medium emphasis */
.text-2xl { font-size: 24pt; } /* Strong emphasis */
.text-3xl { font-size: 28pt; } /* Very large */
.text-4xl { font-size: 32pt; } /* Maximum body text */
.text-muted { color: var(--muted-color); }
.text-center { text-align: center; }Typography guidance:
- Base text (
--text-size: 16pt) is intentionally small to fit more content - When a slide has less content, use
.text-lg,.text-xl, etc. to fill space appropriately - This approach prevents overflow on content-heavy slides while allowing flexibility on lighter slides
Custom CSS classes for repeated patterns:
Use inline styles for layout (grids, flex containers) since those vary per slide. But when a visual pattern appears on multiple slides, create a dedicated CSS class in styles.css instead of repeating inline styles. This keeps the HTML clean and ensures consistency. Common examples: stat boxes (number + label), feature cards (icon + title + description), timeline/process steps, profile/bio cards. If an element repeats 3+ times, it should be a class.
Step 4: Fill in the HTML Content
IMPORTANT: Use the Edit tool to fill in slides incrementally — one or a few slides at a time. Do NOT rewrite the entire HTML file with the Write tool. The scaffold generates unique placeholder text per slide (e.g., Slide 2 Title Here), so each section can be targeted with Edit. This is more token-efficient and less error-prone than generating the full file at once.
Follow these patterns:
Standard slide structure:
<section id="unique-slide-id">
<h2>Slide Title</h2>
<div class="content">
<!-- Content here -->
</div>
</section>Multi-column layouts - always use inline CSS grid (do NOT create utility classes like .grid-2):
<!-- Equal columns -->
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 30px;">
<div>Column 1</div>
<div>Column 2</div>
</div>
<!-- Three columns -->
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 25px;">
<div>Column 1</div>
<div>Column 2</div>
<div>Column 3</div>
</div>
<!-- Unequal columns -->
<div style="display: grid; grid-template-columns: 1fr 2fr; gap: 30px;">
<div>Narrow sidebar</div>
<div>Wide main content</div>
</div>Why inline styles for grids? Each slide's layout needs vary - column ratios, gaps, etc. Inline styles give you full control per-slide without creating dozens of utility classes.
Important HTML patterns:
- Every
<section>should have a uniqueidattribute for stable identification - Use
class="section-divider"for centered section title slides - Wrap main content in
<div class="content">for consistent spacing. This is a flexbox container that fills the remaining vertical space below the title, ensuring content flows properly. - Use
<div class="footnote">for attribution or source text at bottom - All visible text must be inside a text element (
<p>,<li>, or<h1>–<h6>). These elements inherit the base font-size, color, and line-height from the CSS. Never put text directly in<span>or<div>— they won't pick up the base styles and will render at the wrong size. Use<div>only as a layout container (for grids, flexbox, etc.), with<p>elements inside it for the actual text.
Step 5: Check for Content Overflow
Run the overflow checker to ensure no slides have content that extends beyond boundaries:
node scripts/check-overflow.js presentation.htmlThe script checks each slide for:
- Vertical overflow: Content taller than slide height
- Horizontal overflow: Content wider than slide width
If overflow is detected, reduce content or adjust font sizes on affected slides.
Step 6: Visual Review with Screenshots
CRITICAL: You MUST review screenshots of EVERY SINGLE SLIDE. Do not skip slides or review only a sample. Visual issues are common and can only be caught by examining each slide individually.
Capture screenshots of all slides:
cd <presentation-directory>
npx decktape reveal "presentation.html?export" output.pdf \
--screenshots \
--screenshots-directory "screenshots/$(date +%Y%m%d_%H%M%S)"Note: The ?export query parameter disables chart animations for cleaner PDF rendering. Charts will still animate when viewing the HTML directly in a browser.
This creates a timestamped folder (e.g., screenshots/20241210_143052/) so you can track versions and compare before/after fixes.
Then use the Read tool to examine each screenshot image file.
What to Look For
The overflow script catches most layout issues, but these problems require visual inspection:
1. Color inheritance in containers: Text inside styled containers may inherit the wrong color from parent elements. If you have light text on a dark page background, text inside a light-colored container will be unreadable unless you explicitly set dark text color for that container.
2. Custom bullet/list styling: If you override default list styles, bullets may not contrast well on all container backgrounds.
3. Icons not rendering: If Font Awesome fails to load, you'll see empty squares or nothing where icons should be.
4. Overflow edge cases: The script catches most overflow, but complex nested layouts occasionally slip through.
5. Unexpected text wrap: Text that you expected to fit on one line actually overflows to two lines. This is especially common in column layouts, where the header of one column may wrap while the rest don't, making things uneven.
Re-capture specific slides after fixes:
npx decktape reveal "presentation.html?export" output.pdf \
--screenshots \
--screenshots-directory "screenshots/$(date +%Y%m%d_%H%M%S)" \
--slides 2,5,7-9Then re-examine the updated screenshots to verify fixes. The new timestamped folder makes it easy to compare with the previous version.
Step 7: Suggest Browser Editing
After completing the presentation, let the user know they can edit text directly in the browser using the included editor script:
node <path-to-skill>/scripts/edit-html.js <presentation-directory>/presentation.htmlThis opens the presentation in a local server where they can click any text to edit it inline, then save changes back to the file. It's useful for wordsmithing, fixing typos, or tweaking copy without needing to edit raw HTML.
Always mention this at the end of a presentation as a suggestion, e.g.:
To fine-tune the wording, you can edit text directly in the browser:
```
node <resolved-path>/scripts/edit-html.js <presentation-directory>/presentation.html
```
Click any text to edit, press Escape to deselect, then click Save. Press Ctrl+C to stop the server when done.
CSS Components Reference
Blockquotes
.reveal blockquote {
border-left: 4px solid var(--primary-color);
padding-left: 20px;
margin: 20px 0;
font-style: italic;
background: none;
box-shadow: none;
width: 100%;
}
.reveal blockquote cite {
display: block;
margin-top: 10px;
font-style: normal;
color: var(--muted-color);
}Icons (Font Awesome)
Font Awesome is included in the scaffold. Usage:
<i class="fa-solid fa-lightbulb"></i>
<i class="fa-solid fa-check"></i>
<i class="fa-solid fa-gears"></i>Advanced Features
For fragments (progressive reveal), speaker notes, custom backgrounds, auto-animate, and transitions, see references/advanced-features.md.
Reveal.js Configuration
Reveal.initialize({
controls: true, // Show navigation arrows
progress: true, // Show progress bar
slideNumber: true, // Show slide numbers
hash: true, // Update URL hash for each slide
transition: 'slide', // none/fade/slide/convex/concave/zoom
center: false, // Vertical centering of slide content
autoSlide: 0, // Auto-advance (ms), 0 to disable
loop: false, // Loop presentation
});Note on `center`: Default is false (content aligns to top), which works best for content-heavy slides. Set to true for minimal/creative presentations where you want content vertically centered.
Built-in Reveal.js Classes
Use these directly without custom CSS:
r-fit-text- Auto-size text to fill slider-stretch- Stretch element to fill remaining vertical spacer-stack- Layer elements on top of each other
<h1 class="r-fit-text">BIG TEXT</h1>
<img class="r-stretch" src="image.jpg">Adding Charts
IMPORTANT: Before adding ANY chart, you MUST read [references/charts.md](references/charts.md). Charts require specific flexbox/grid patterns to size correctly and avoid overflow. Do not attempt to add charts without reading the full documentation first.
The scaffold includes the Chart.js plugin for adding bar, line, pie, doughnut, and scatter charts to slides.
Required pattern - charts need flexbox containers and maintainAspectRatio: false:
<section style="display: flex; flex-direction: column; height: 100%;">
<h2>Chart Title</h2>
<div style="flex: 1; position: relative; min-height: 0;">
<canvas data-chart="bar">
<!--
{
"data": {
"labels": ["Q1", "Q2", "Q3", "Q4"],
"datasets": [{ "label": "Revenue", "data": [12, 19, 8, 15] }]
},
"options": {
"maintainAspectRatio": false
}
}
-->
</canvas>
</div>
</section>[references/charts.md](references/charts.md) covers (required reading):
- Layout patterns: full slide, half (horizontal/vertical), quarter, unequal splits (1fr 2fr, 1fr 3fr)
- Why the flexbox pattern is required (Chart.js aspect ratio behavior)
- All chart types (bar, line, pie, doughnut, scatter, etc.)
- Styling and color options
- CSV data format (simpler alternative to JSON)
Dependencies
Required for the scripts, should be already installed:
- Node.js (for running scripts)
- Puppeteer (for overflow checking):
npm install puppeteer - Decktape (for screenshots):
npx decktape(runs directly)
Advanced Reveal.js Features
These features are available if the user specifically requests them. They are not used by default.
Fragments (Progressive Reveal)
Show content step-by-step on click:
<p class="fragment">Appears on click</p>
<p class="fragment fade-up">Slides up</p>
<p class="fragment highlight-red">Turns red</p>Fragment animations:
fade-in(default)fade-outfade-up,fade-down,fade-left,fade-righthighlight-red,highlight-green,highlight-bluestrike(strikethrough)
Speaker Notes
Add private notes visible only in speaker view (press S to open):
<section>
<h2>Slide Title</h2>
<p>Visible content</p>
<aside class="notes">
Private notes for the presenter.
- Remember to mention X
- Transition to next topic
</aside>
</section>Backgrounds
Solid Color
<section data-background-color="#283b95">Image
<section data-background-image="image.jpg">
<section data-background-image="image.jpg" data-background-opacity="0.5">
<section data-background-image="image.jpg" data-background-size="contain">Gradient
<section data-background-gradient="linear-gradient(to bottom, #283b95, #17b2c3)">
<section data-background-gradient="radial-gradient(#283b95, #17b2c3)">Auto-Animate
Automatically animate elements between slides. Elements with matching data-id attributes will transition smoothly:
<section data-auto-animate>
<h1>Title</h1>
</section>
<section data-auto-animate>
<h1>Title</h1>
<h2>Subtitle appears with animation</h2>
</section>More complex example with matching elements:
<section data-auto-animate>
<div data-id="box" style="width: 100px; height: 100px; background: blue;"></div>
</section>
<section data-auto-animate>
<div data-id="box" style="width: 300px; height: 150px; background: red;"></div>
</section>Transitions
Set per-slide transitions:
<section data-transition="fade">
<section data-transition="slide">
<section data-transition="convex">
<section data-transition="concave">
<section data-transition="zoom">
<section data-transition="none">Different in/out transitions:
<section data-transition="slide-in fade-out">Slide Visibility
Hide slides from normal flow (accessible via URL only):
<section data-visibility="hidden">Skip slides in navigation but keep visible:
<section data-visibility="uncounted">Code Highlighting
Basic syntax highlighting:
<pre><code class="language-python">
def hello():
print("Hello")
</code></pre>Supported languages: javascript, python, html, css, java, ruby, go, rust, sql, bash, json, yaml, and many more.
Line highlighting (step-through on click):
<pre><code data-line-numbers="1-2|3|4">
let a = 1;
let b = 2;
let c = x => 1 + 2 + x;
c(3);
</code></pre>This highlights lines 1-2 first, then line 3 on click, then line 4.
Static line highlighting (no step-through):
<pre><code data-line-numbers="3,5-7">
...
</code></pre>/* Base reveal.js styles
*
* Copy this file as styles.css for each new presentation.
* Customize the CSS variables (especially colors) for your theme.
*
* IMPORTANT: All font sizes use pt (points) for predictable sizing.
* Base text is intentionally small (16pt) to fit content-heavy slides.
* Use .text-lg, .text-xl, etc. classes when slides have less content.
*/
/* ===========================================
CSS VARIABLES - Customize these for each presentation
=========================================== */
:root {
/* ===========================================
BACKGROUND COLORS
For dark presentations, change to dark colors (e.g., #1a1a2e, #0d1117)
Then update --text-color and --muted-color to light colors below.
=========================================== */
--background-color: #ffffff; /* Main slide background */
--section-divider-bg: #ffffff; /* Section divider background (set different for contrast) */
/* Typography - ALWAYS use pt for font sizes */
--heading-font: "Source Sans Pro", Helvetica, sans-serif;
--body-font: "Source Sans Pro", Helvetica, sans-serif;
--base-font-size: 32px; /* Only px value - sets reveal.js base */
--text-size: 16pt; /* Base body text - intentionally small */
--h1-size: 48pt;
--h2-size: 36pt;
--h3-size: 24pt;
--footnote-size: 11pt;
/* Colors - CUSTOMIZE THESE */
--primary-color: #2196F3; /* Main accent color */
--secondary-color: #ff9800; /* Secondary accent */
--text-color: #222; /* Main text - use light color (e.g., #f0f0f0) for dark backgrounds */
--muted-color: #666; /* Secondary text - use lighter color for dark backgrounds */
--line-color: #333; /* Decorative lines */
/* Layout */
--slide-padding: 60px;
--slide-padding-top: 40px;
--content-gap: 30px;
/* Border radius for custom components */
--box-radius: 8px; /* Set to 0 for sharp corners */
}
/* ===========================================
BASE STYLES - Override reveal.js defaults
=========================================== */
.reveal {
font-family: var(--body-font);
font-size: var(--base-font-size);
}
/* Restore bold after reset.css strips it with font: inherit */
.reveal strong,
.reveal b {
font-weight: bold;
}
/* Apply background colors */
.reveal-viewport {
background-color: var(--background-color);
}
/* Section divider background via data-state */
.reveal-viewport.is-section-divider .slide-background.present {
background-color: var(--section-divider-bg) !important;
}
.reveal h1,
.reveal h2,
.reveal h3,
.reveal h4,
.reveal h5,
.reveal h6 {
font-family: var(--heading-font);
text-transform: none;
color: var(--text-color);
font-weight: 600;
line-height: 1.2;
}
.reveal h1 { font-size: var(--h1-size); }
.reveal h2 { font-size: var(--h2-size); margin-bottom: 0; }
.reveal h3 { font-size: var(--h3-size); }
.reveal p,
.reveal li,
.reveal td,
.reveal th,
.reveal blockquote {
font-size: var(--text-size);
color: var(--text-color);
line-height: 1.5;
}
/* ===========================================
LISTS
=========================================== */
.reveal ul {
list-style: none;
padding-left: 0;
margin: 15px 0;
}
.reveal ul li {
position: relative;
padding-left: 25px;
margin-bottom: 10px;
}
.reveal ul li::before {
content: '';
position: absolute;
left: 0;
top: 0.6em;
width: 8px;
height: 8px;
background: var(--text-color);
border-radius: 50%;
}
.reveal ul ul li::before {
background: var(--muted-color);
width: 6px;
height: 6px;
}
/* Prevent double-sizing for nested elements */
.reveal blockquote p {
font-size: inherit;
}
/* ===========================================
SLIDE LAYOUT
=========================================== */
.reveal .slides section {
height: 100%;
display: flex !important;
flex-direction: column !important;
padding: var(--slide-padding-top) var(--slide-padding) var(--slide-padding) var(--slide-padding) !important;
box-sizing: border-box;
text-align: left;
}
/* Vertical stack wrapper - no padding, just pass through */
.reveal .slides section.stack {
padding: 0 !important;
}
/* Content area - fills remaining space */
.reveal .slides section > .content {
flex: 1;
display: flex;
flex-direction: column;
justify-content: flex-start;
padding-top: var(--content-gap);
text-align: left;
}
/* Footnotes at bottom */
.reveal .slides section > .footnote {
position: absolute;
bottom: 15px;
left: var(--slide-padding);
right: var(--slide-padding);
font-size: var(--footnote-size);
color: var(--muted-color);
}
/* ===========================================
SECTION DIVIDERS - Centered title slides
=========================================== */
.reveal .slides section.section-divider {
display: flex !important;
flex-direction: column !important;
justify-content: center !important;
align-items: center !important;
text-align: center;
}
.reveal .slides section.section-divider h1 {
font-size: 56pt;
text-align: center;
}
.reveal .slides section.section-divider p {
font-size: 20pt;
color: var(--muted-color);
}
/* ===========================================
TEXT SIZE UTILITIES
Base text is 16pt - use these to scale UP when
slides have less content and you want to fill space.
=========================================== */
.text-lg { font-size: 18pt !important; } /* Slightly larger */
.text-xl { font-size: 20pt !important; } /* Medium emphasis */
.text-2xl { font-size: 24pt !important; } /* Strong emphasis */
.text-3xl { font-size: 28pt !important; } /* Very large */
.text-4xl { font-size: 32pt !important; } /* Maximum body text */
/* Other text utilities */
.text-muted { color: var(--muted-color) !important; }
.text-center { text-align: center !important; }
.text-uppercase { text-transform: uppercase; letter-spacing: 0.1em; }
.font-light { font-weight: 300; }
.font-bold { font-weight: 600; }
/* ===========================================
BLOCKQUOTES
=========================================== */
.reveal blockquote {
border-left: 4px solid var(--primary-color);
padding-left: 20px;
margin: 20px 0;
font-style: italic;
background: none;
box-shadow: none;
width: 100%;
max-width: none;
}
.reveal blockquote cite {
display: block;
margin-top: 10px;
font-style: normal;
color: var(--muted-color);
}
/* ===========================================
OPTIONAL: TITLE WITH UNDERLINE
Add this to presentations where you want
a line under the slide title.
=========================================== */
/*
.reveal .slides section > h1,
.reveal .slides section > h2 {
flex-shrink: 0;
margin: 0;
padding-bottom: 15px;
border-bottom: 2px solid var(--line-color);
}
*/
/* ===========================================
OPTIONAL: DECORATIVE ELEMENTS
=========================================== */
/* Top decorative line on all slides */
/*
.reveal .slides section::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: var(--primary-color);
}
*/
/* Bottom decorative line */
/*
.reveal .slides section::after {
content: '';
position: absolute;
bottom: 40px;
left: var(--slide-padding);
right: var(--slide-padding);
height: 2px;
background: var(--line-color);
}
*/
Adding Charts to Reveal.js Presentations
Charts are added using the reveal.js-plugins/chart plugin, which integrates Chart.js into your slides.
Setup
The chart plugin is included by default in the scaffold. It adds these to your HTML:
<!-- In <head> -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<!-- Before Reveal.initialize() -->
<script src="https://cdn.jsdelivr.net/npm/reveal.js-plugins@latest/chart/plugin.js"></script>
<!-- In Reveal.initialize() -->
Reveal.initialize({
plugins: [ RevealChart ],
chart: {
defaults: {
color: 'lightgray',
borderColor: 'lightgray'
}
}
});Chart Types
Supported types: line, bar, pie, doughnut, radar, polarArea, bubble, scatter
For most presentations, you'll use:
- bar - Comparing categories
- line - Trends over time
- pie/doughnut - Parts of a whole
- scatter - Relationships between variables
Chart Sizing (IMPORTANT)
Charts MUST use one of these four layout options to properly fill their space without overflow:
1. Full slide - Chart fills entire slide below title 2. Half slide (horizontal) - Chart on left or right, content on the other side 3. Half slide (vertical) - Chart on top or bottom, content on the other half 4. Quarter slide - Chart in one quadrant, other content in remaining three
Required CSS Pattern
Every chart needs: 1. Flexbox section with display: flex; flex-direction: column; height: 100%; 2. Container div with flex: 1; position: relative; min-height: 0; (and min-width: 0 for grid layouts) 3. `maintainAspectRatio: false` in chart options
Full Slide Layout
<section style="display: flex; flex-direction: column; height: 100%;">
<h2>Chart Title</h2>
<div style="flex: 1; position: relative; min-height: 0;">
<canvas data-chart="bar">
<!--
{
"data": {
"labels": ["Q1", "Q2", "Q3", "Q4"],
"datasets": [{
"label": "Revenue",
"data": [45, 52, 61, 78],
"backgroundColor": "#2196F3"
}]
},
"options": {
"maintainAspectRatio": false
}
}
-->
</canvas>
</div>
</section>Half Slide - Horizontal (Left/Right)
Chart on right, content on left:
<section style="display: flex; flex-direction: column; height: 100%;">
<h2>Chart Title</h2>
<div style="flex: 1; display: grid; grid-template-columns: 1fr 1fr; gap: 30px; min-height: 0; min-width: 0;">
<div style="display: flex; flex-direction: column; justify-content: center; background: #f5f5f5; padding: 20px; border-radius: 8px;">
<p><strong>Key Points</strong></p>
<ul>
<li>First insight</li>
<li>Second insight</li>
<li>Third insight</li>
</ul>
</div>
<div style="position: relative; min-height: 0; min-width: 0;">
<canvas data-chart="pie">
<!--
{
"data": {
"labels": ["A", "B", "C"],
"datasets": [{
"data": [45, 35, 20],
"backgroundColor": ["#2196F3", "#4caf50", "#ff9800"]
}]
},
"options": {
"maintainAspectRatio": false
}
}
-->
</canvas>
</div>
</div>
</section>Half Slide - Vertical (Top/Bottom)
Content on top, chart on bottom (equal halves):
<section style="display: flex; flex-direction: column; height: 100%;">
<h2>Chart Title</h2>
<div style="flex: 1; display: grid; grid-template-rows: 1fr 1fr; gap: 20px; min-height: 0; min-width: 0;">
<div style="display: flex; align-items: center; justify-content: center; background: #f5f5f5; padding: 20px; border-radius: 8px;">
<div>
<p><strong>Analysis Summary</strong></p>
<p>Description of what the chart shows and key takeaways.</p>
</div>
</div>
<div style="position: relative; min-height: 0; min-width: 0;">
<canvas data-chart="line">
<!--
{
"data": {
"labels": ["Jan", "Feb", "Mar", "Apr"],
"datasets": [{
"label": "Trend",
"data": [10, 25, 35, 50],
"borderColor": "#2196F3",
"fill": false
}]
},
"options": {
"maintainAspectRatio": false
}
}
-->
</canvas>
</div>
</div>
</section>Content Header + Chart Below (Unequal Split)
Small content area on top (1/4 or 1/3), chart fills the rest. Use explicit fractions for predictable sizing:
1/4 content, 3/4 chart:
<section style="display: flex; flex-direction: column; height: 100%;">
<h2>Chart Title</h2>
<div style="flex: 1; display: grid; grid-template-rows: 1fr 3fr; gap: 20px; min-height: 0; min-width: 0;">
<div style="display: flex; align-items: center; background: #f5f5f5; padding: 15px 20px; border-radius: 8px;">
<p><strong>Key insight:</strong> Revenue grew 25% quarter-over-quarter, exceeding targets.</p>
</div>
<div style="position: relative; min-height: 0; min-width: 0;">
<canvas data-chart="bar">
<!--
{
"data": {
"labels": ["Q1", "Q2", "Q3", "Q4"],
"datasets": [{
"label": "Revenue",
"data": [45, 52, 61, 78],
"backgroundColor": "#2196F3"
}]
},
"options": {
"maintainAspectRatio": false
}
}
-->
</canvas>
</div>
</div>
</section>1/3 content, 2/3 chart:
<section style="display: flex; flex-direction: column; height: 100%;">
<h2>Chart Title</h2>
<div style="flex: 1; display: grid; grid-template-rows: 1fr 2fr; gap: 20px; min-height: 0; min-width: 0;">
<div style="display: flex; flex-direction: column; justify-content: center; background: #f5f5f5; padding: 15px 20px; border-radius: 8px;">
<p><strong>Summary</strong></p>
<ul style="margin: 10px 0 0 0;">
<li>Strong Q4 performance</li>
<li>All regions exceeded targets</li>
</ul>
</div>
<div style="position: relative; min-height: 0; min-width: 0;">
<canvas data-chart="line">
<!--
{
"data": {
"labels": ["Jan", "Feb", "Mar", "Apr", "May", "Jun"],
"datasets": [{
"label": "Growth",
"data": [100, 120, 135, 150, 180, 210],
"borderColor": "#2196F3",
"fill": false
}]
},
"options": {
"maintainAspectRatio": false
}
}
-->
</canvas>
</div>
</div>
</section>Common row fractions:
1fr 3fr- 25% content / 75% chart (minimal text)1fr 2fr- 33% content / 67% chart (short paragraph or bullet list)1fr 1fr- 50% / 50% equal split
Quarter Slide (Quadrant)
Chart in one quadrant (bottom-right), other content in remaining three:
<section style="display: flex; flex-direction: column; height: 100%;">
<h2>Dashboard View</h2>
<div style="flex: 1; display: grid; grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr; gap: 15px; min-height: 0; min-width: 0;">
<div style="display: flex; align-items: center; justify-content: center; background: #e3f2fd; padding: 15px; border-radius: 8px;">
<div style="text-align: center;">
<p style="font-size: 2em; margin: 0; color: #1565c0;">$2.4M</p>
<p style="margin: 0; color: #666;">Total Revenue</p>
</div>
</div>
<div style="display: flex; align-items: center; justify-content: center; background: #e8f5e9; padding: 15px; border-radius: 8px;">
<div style="text-align: center;">
<p style="font-size: 2em; margin: 0; color: #2e7d32;">+18%</p>
<p style="margin: 0; color: #666;">Growth Rate</p>
</div>
</div>
<div style="display: flex; align-items: center; justify-content: center; background: #fff3e0; padding: 15px; border-radius: 8px;">
<div style="text-align: center;">
<p style="font-size: 2em; margin: 0; color: #ef6c00;">847</p>
<p style="margin: 0; color: #666;">New Customers</p>
</div>
</div>
<div style="position: relative; min-height: 0; min-width: 0;">
<canvas data-chart="doughnut">
<!--
{
"data": {
"labels": ["Product A", "Product B", "Product C"],
"datasets": [{
"data": [40, 35, 25],
"backgroundColor": ["#2196F3", "#4caf50", "#ff9800"]
}]
},
"options": {
"maintainAspectRatio": false,
"plugins": {
"legend": { "position": "bottom" }
}
}
}
-->
</canvas>
</div>
</div>
</section>Why This Pattern Works
Chart.js by default maintains a 2:1 aspect ratio, which causes overflow in constrained slide layouts. The flexbox/grid pattern solves this by:
1. `height: 100%` on section - Makes the slide fill available space 2. `flex: 1` on container - Expands to fill remaining space after title 3. `min-height: 0; min-width: 0` - Allows flex/grid children to shrink below content size (critical for preventing overflow) 4. `position: relative` - Required by Chart.js for responsive sizing 5. `maintainAspectRatio: false` - Tells Chart.js to fill container instead of maintaining ratio
Styling Charts
Colors
Set colors in the JSON configuration:
"datasets": [{
"data": [12, 19, 8, 15],
"backgroundColor": ["#2196F3", "#ff9800", "#4caf50", "#e91e63"]
}]Common color arrays for charts
// Blues
["#1565c0", "#1976d2", "#1e88e5", "#2196f3", "#42a5f5"]
// Warm palette
["#c62828", "#ef6c00", "#f9a825", "#2e7d32", "#1565c0"]
// Grayscale
["#212121", "#424242", "#616161", "#757575", "#9e9e9e"]
// Categorical (distinct)
["#2196F3", "#ff9800", "#4caf50", "#e91e63", "#9c27b0"]Common Options
Hide Legend
"options": {
"maintainAspectRatio": false,
"plugins": {
"legend": { "display": false }
}
}Custom Axis Labels
"options": {
"maintainAspectRatio": false,
"scales": {
"y": {
"title": { "display": true, "text": "Revenue ($)" }
},
"x": {
"title": { "display": true, "text": "Quarter" }
}
}
}Start Y-Axis at Zero
"options": {
"maintainAspectRatio": false,
"scales": {
"y": { "beginAtZero": true }
}
}Legend Position
"options": {
"maintainAspectRatio": false,
"plugins": {
"legend": { "position": "bottom" }
}
}CSV Data Format
You can define data using CSV format (simpler than JSON):
<canvas data-chart="line">
<!--
Month, Sales, Expenses
Jan, 40, 30
Feb, 50, 35
Mar, 60, 40
Apr, 55, 38
May, 70, 45
-->
</canvas>The first row becomes labels, subsequent columns become datasets.
External CSV Files
For larger datasets, use external CSV files:
<canvas data-chart="line" data-chart-src="data/sales.csv">
<!--
{
"options": {
"maintainAspectRatio": false,
"plugins": { "legend": { "position": "bottom" } }
}
}
-->
</canvas>Validation
Run the overflow check script to verify charts don't overflow:
node scripts/check-overflow.js presentation.htmlTips
1. Always use the flexbox pattern - Never set fixed height on canvas directly 2. Always include `maintainAspectRatio: false` in chart options 3. Keep charts simple - Presentations aren't dashboards 4. Use consistent colors - Match your presentation's color palette 5. Limit data points - 4-8 points is ideal for readability 6. Test overflow - Run check-overflow.js after adding charts
#!/usr/bin/env node
/**
* Validates chart configurations in a reveal.js presentation.
* Checks for valid JSON syntax and required properties.
*
* Usage: node check-charts.js <path-to-html>
*/
const fs = require('fs');
const path = require('path');
const cheerio = require('cheerio');
const VALID_CHART_TYPES = ['line', 'bar', 'pie', 'doughnut', 'radar', 'polarArea', 'bubble', 'scatter'];
function extractCharts(html) {
const $ = cheerio.load(html);
const charts = [];
$('canvas[data-chart]').each((index, element) => {
const $el = $(element);
const chartType = $el.attr('data-chart');
const externalSrc = $el.attr('data-chart-src') || null;
// Get the raw HTML content inside the canvas to extract the comment
const innerHtml = $el.html() || '';
// Extract JSON/CSV from HTML comment
const commentMatch = innerHtml.match(/<!--([\s\S]*?)-->/);
const configText = commentMatch ? commentMatch[1].trim() : null;
// Calculate line number by finding position in original HTML
// Get the outer HTML and find it in the source
const outerHtml = $.html(element);
const position = html.indexOf(outerHtml.substring(0, 50));
const lineNumber = position >= 0
? (html.substring(0, position).match(/\n/g) || []).length + 1
: -1;
charts.push({
type: chartType,
configText,
externalSrc,
lineNumber,
preview: outerHtml.substring(0, 80) + '...'
});
});
return charts;
}
function validateChart(chart, index) {
const errors = [];
const warnings = [];
const lineInfo = chart.lineNumber > 0 ? `line ${chart.lineNumber}` : 'unknown line';
const prefix = `Chart ${index + 1} (${lineInfo}, type: ${chart.type})`;
// Check chart type
if (!VALID_CHART_TYPES.includes(chart.type)) {
errors.push(`${prefix}: Invalid chart type '${chart.type}'. Valid types: ${VALID_CHART_TYPES.join(', ')}`);
}
// If there's no config and no external source, that's an error
if (!chart.configText && !chart.externalSrc) {
errors.push(`${prefix}: No chart configuration found. Add JSON in HTML comment or use data-chart-src attribute.`);
return { errors, warnings };
}
// If using external source, just note it
if (chart.externalSrc) {
warnings.push(`${prefix}: Uses external data source '${chart.externalSrc}' - ensure file exists`);
}
// If there's config text, try to parse it
if (chart.configText) {
// Check if it looks like CSV (no opening brace)
const trimmed = chart.configText.trim();
if (!trimmed.startsWith('{')) {
// Looks like CSV data - basic validation
const lines = trimmed.split('\n').filter(l => l.trim());
if (lines.length < 2) {
errors.push(`${prefix}: CSV data should have at least a header row and one data row`);
} else {
const headerCols = lines[0].split(',').length;
for (let i = 1; i < lines.length; i++) {
const dataCols = lines[i].split(',').length;
if (dataCols !== headerCols) {
warnings.push(`${prefix}: CSV row ${i + 1} has ${dataCols} columns, header has ${headerCols}`);
}
}
}
} else {
// Try to parse as JSON
try {
const config = JSON.parse(trimmed);
// Check for required properties
if (!config.data && !chart.externalSrc) {
errors.push(`${prefix}: Missing required 'data' property in chart configuration`);
}
if (config.data) {
// Check for labels (required for most chart types)
if (!config.data.labels && !['scatter', 'bubble'].includes(chart.type)) {
warnings.push(`${prefix}: Missing 'data.labels' - chart may not display correctly`);
}
// Check for datasets
if (!config.data.datasets || !Array.isArray(config.data.datasets)) {
errors.push(`${prefix}: Missing or invalid 'data.datasets' array`);
} else if (config.data.datasets.length === 0) {
errors.push(`${prefix}: 'data.datasets' array is empty`);
} else {
// Check each dataset
config.data.datasets.forEach((dataset, i) => {
if (!dataset.data && !chart.externalSrc) {
errors.push(`${prefix}: Dataset ${i + 1} is missing 'data' property`);
}
});
}
}
// Check for maintainAspectRatio: false (recommended)
if (!config.options || config.options.maintainAspectRatio !== false) {
warnings.push(`${prefix}: Missing 'options.maintainAspectRatio: false' - chart may overflow`);
}
} catch (e) {
errors.push(`${prefix}: Invalid JSON syntax - ${e.message}`);
// Try to give helpful hints
if (e.message.includes('Unexpected token')) {
errors.push(` Hint: Check for missing commas, quotes, or brackets`);
}
}
}
}
return { errors, warnings };
}
function main() {
const htmlPath = process.argv[2];
if (!htmlPath) {
console.error('Usage: node check-charts.js <path-to-html>');
process.exit(1);
}
if (!fs.existsSync(htmlPath)) {
console.error(`Error: File not found: ${htmlPath}`);
process.exit(1);
}
const html = fs.readFileSync(htmlPath, 'utf-8');
const charts = extractCharts(html);
console.log(`Checking charts in: ${htmlPath}\n`);
if (charts.length === 0) {
console.log('No charts found in presentation.');
return;
}
console.log(`Found ${charts.length} chart(s)\n`);
let totalErrors = 0;
let totalWarnings = 0;
charts.forEach((chart, index) => {
const { errors, warnings } = validateChart(chart, index);
if (errors.length === 0 && warnings.length === 0) {
console.log(`✓ Chart ${index + 1} (line ${chart.lineNumber}, type: ${chart.type}): OK`);
} else {
if (errors.length > 0) {
errors.forEach(e => console.log(`✗ ${e}`));
totalErrors += errors.length;
}
if (warnings.length > 0) {
warnings.forEach(w => console.log(`⚠ ${w}`));
totalWarnings += warnings.length;
}
}
});
console.log(`\n${'─'.repeat(50)}`);
console.log(`Total: ${charts.length} charts, ${totalErrors} errors, ${totalWarnings} warnings`);
if (totalErrors > 0) {
process.exit(1);
}
}
main();
/** Checks reveal.js slides for content overflow using Puppeteer */
const puppeteer = require('puppeteer');
const path = require('path');
async function checkSlideOverflow(htmlPath) {
const absolutePath = path.resolve(htmlPath);
const fileUrl = `file://${absolutePath}`;
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
// Set viewport to standard presentation size
await page.setViewport({ width: 1920, height: 1080 });
await page.goto(fileUrl, { waitUntil: 'networkidle0' });
// Wait for reveal.js to initialize
await page.waitForFunction(() => typeof Reveal !== 'undefined' && Reveal.isReady());
// Get all slides using DOM query (includes vertical slides)
const results = await page.evaluate(() => {
const allSlides = document.querySelectorAll('.slides > section, .slides > section > section');
const slides = [];
allSlides.forEach((slide, i) => {
// Skip stack parents (sections that contain nested sections)
const isStackParent = slide.parentElement.classList.contains('slides') && slide.querySelector('section');
if (isStackParent) return;
const id = slide.id || `slide-${i}`;
const scrollHeight = slide.scrollHeight;
const scrollWidth = slide.scrollWidth;
const clientHeight = slide.clientHeight;
const clientWidth = slide.clientWidth;
const hasVerticalOverflow = scrollHeight > clientHeight;
const hasHorizontalOverflow = scrollWidth > clientWidth;
slides.push({
index: i,
id,
hasOverflow: hasVerticalOverflow || hasHorizontalOverflow,
hasVerticalOverflow,
hasHorizontalOverflow,
dimensions: {
scrollHeight,
clientHeight,
scrollWidth,
clientWidth,
verticalDiff: scrollHeight - clientHeight,
horizontalDiff: scrollWidth - clientWidth
}
});
});
return slides;
});
await browser.close();
return results;
}
async function main() {
const htmlPath = process.argv[2];
if (!htmlPath) {
console.error('Usage: node check-overflow.js <path-to-html>');
process.exit(1);
}
console.log(`Checking slides for overflow: ${htmlPath}\n`);
const results = await checkSlideOverflow(htmlPath);
let hasAnyOverflow = false;
for (const slide of results) {
if (slide.hasOverflow) {
hasAnyOverflow = true;
console.log(`OVERFLOW: Slide ${slide.index} (${slide.id})`);
if (slide.hasVerticalOverflow) {
console.log(` - Vertical overflow: ${slide.dimensions.verticalDiff}px (content: ${slide.dimensions.scrollHeight}px, container: ${slide.dimensions.clientHeight}px)`);
}
if (slide.hasHorizontalOverflow) {
console.log(` - Horizontal overflow: ${slide.dimensions.horizontalDiff}px (content: ${slide.dimensions.scrollWidth}px, container: ${slide.dimensions.clientWidth}px)`);
}
}
}
if (!hasAnyOverflow) {
console.log('No overflow detected on any slides.');
}
console.log(`\nTotal slides checked: ${results.length}`);
}
main().catch(err => {
console.error('Error:', err);
process.exit(1);
});
#!/usr/bin/env node
/**
* Creates a reveal.js presentation scaffold with the specified options.
* Usage: node create-presentation.js [options]
*/
const fs = require('fs');
const path = require('path');
// Path to the base styles file (relative to this script)
const BASE_STYLES_PATH = path.join(__dirname, '..', 'references', 'base-styles.css');
function parseArgs(args) {
const options = {
slides: null,
structure: null,
output: 'presentation.html',
title: 'Presentation',
stylesFile: 'styles.css',
};
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg === '--slides' || arg === '-s') {
options.slides = parseInt(args[++i], 10);
} else if (arg === '--structure') {
options.structure = args[++i].split(',').map(n => n === 'd' ? 'd' : parseInt(n, 10));
} else if (arg === '--output' || arg === '-o') {
options.output = args[++i];
} else if (arg === '--title') {
options.title = args[++i];
} else if (arg === '--styles') {
options.stylesFile = args[++i];
} else if (arg === '--help' || arg === '-h') {
printHelp();
process.exit(0);
}
}
return options;
}
function printHelp() {
console.log(`
create-presentation.js - Generate a reveal.js presentation scaffold
Usage: node create-presentation.js [options]
Options:
--slides, -s <num> Number of horizontal slides (simple mode)
--structure <list> Mixed layout: comma-separated values (e.g., "1,1,d,3,1,d,1")
- Number 1 = single horizontal slide
- Number >1 = vertical stack of that many slides
- 'd' = section divider slide
Cannot be used with --slides
--output, -o <file> Output HTML filename (default: presentation.html)
--title <text> Presentation title (default: Presentation)
--styles <file> Custom CSS filename (default: styles.css)
--help, -h Show this help message
Examples:
node create-presentation.js --slides 10 -o my-deck.html
node create-presentation.js --structure 1,1,d,3,1,d,1 -o my-deck.html
node create-presentation.js --structure 1,1,1,d,3,d,1,1 --title "Q4 Review"
`);
}
/** Generates slides from a structure array (e.g., [1, 1, 'd', 5, 1, 2]) */
function generateSlides(structure) {
let slides = '';
let hIndex = 1; // Horizontal index (1-based for display)
let dividerCount = 1;
for (let colIndex = 0; colIndex < structure.length; colIndex++) {
const item = structure[colIndex];
if (item === 'd') {
// Section divider
slides += `
<section id="divider-${dividerCount}" class="section-divider" data-state="is-section-divider">
<h1>Section ${dividerCount} Title</h1>
</section>
`;
dividerCount++;
hIndex++;
} else if (item === 1) {
// Single horizontal slide
if (hIndex === 1) {
slides += `
<section id="title" class="section-divider" data-state="is-section-divider">
<h1>Presentation Title</h1>
</section>
`;
} else {
slides += `
<section id="slide-${hIndex}">
<h2>Slide ${hIndex} Title Here</h2>
</section>
`;
}
hIndex++;
} else {
// Vertical stack
slides += `
<section>
`;
for (let vIndex = 1; vIndex <= item; vIndex++) {
slides += ` <section id="slide-${hIndex}-${vIndex}">
<h2>Slide ${hIndex}.${vIndex} Title Here</h2>
</section>
`;
}
slides += ` </section>
`;
hIndex++;
}
}
return slides;
}
function generateHTML(options) {
const slidesContent = generateSlides(options.structure);
return `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${options.title}</title>
<!-- Reveal.js core -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/reveal.js@5.1.0/dist/reset.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/reveal.js@5.1.0/dist/reveal.css">
<!-- Font Awesome for icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<!-- Custom styles -->
<link rel="stylesheet" href="${options.stylesFile}">
<!-- Chart.js for data visualization -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</head>
<body>
<div class="reveal">
<div class="slides">
${slidesContent}
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/reveal.js@5.1.0/dist/reveal.js"></script>
<script src="https://cdn.jsdelivr.net/npm/reveal.js-plugins@latest/chart/plugin.js"></script>
<script>
Reveal.initialize({
width: 1280,
height: 720,
margin: 0,
controls: true,
progress: true,
slideNumber: false,
hash: true,
transition: 'slide',
center: false,
plugins: [ RevealChart ],
chart: {
defaults: Object.assign({
color: 'rgba(0, 0, 0, 0.8)',
borderColor: 'rgba(0, 0, 0, 0.8)',
devicePixelRatio: 2
}, window.location.search.includes('export') ? { animation: false } : {})
}
});
</script>
</body>
</html>
`;
}
function main() {
const args = process.argv.slice(2);
const options = parseArgs(args);
// Validate mutually exclusive options
if (options.slides !== null && options.structure !== null) {
console.error('Error: Cannot use both --slides and --structure. Choose one.');
process.exit(1);
}
// Default to 5 horizontal slides if neither specified
if (options.slides === null && options.structure === null) {
options.structure = [1, 1, 1, 1, 1];
} else if (options.slides !== null) {
// Convert --slides N to structure of N ones
if (options.slides < 1 || isNaN(options.slides)) {
console.error('Error: Slide count must be at least 1.');
process.exit(1);
}
options.structure = Array(options.slides).fill(1);
} else {
// Validate structure
if (options.structure.some(n => n !== 'd' && (n < 1 || isNaN(n)))) {
console.error('Error: Structure values must be positive integers or "d" for dividers.');
process.exit(1);
}
}
const totalSlides = options.structure.reduce((a, b) => a + (b === 'd' ? 1 : b), 0);
// Determine output directory from the output path
const outputDir = path.dirname(options.output);
// Ensure output directory exists
if (outputDir && outputDir !== '.') {
fs.mkdirSync(outputDir, { recursive: true });
}
// Generate and write HTML
const html = generateHTML(options);
fs.writeFileSync(options.output, html);
console.log(`Created ${options.output}`);
// Copy example-styles.css to output directory as styles.css (if it doesn't exist)
const stylesOutputPath = outputDir && outputDir !== '.'
? path.join(outputDir, options.stylesFile)
: options.stylesFile;
if (!fs.existsSync(stylesOutputPath)) {
if (fs.existsSync(BASE_STYLES_PATH)) {
fs.copyFileSync(BASE_STYLES_PATH, stylesOutputPath);
console.log(`Copied base-styles.css to ${stylesOutputPath}`);
} else {
console.warn(`Warning: Could not find ${BASE_STYLES_PATH}`);
console.warn(`Please manually copy the base styles to ${stylesOutputPath}`);
}
} else {
console.log(`${stylesOutputPath} already exists, skipping`);
}
console.log(`\nPresentation created with ${totalSlides} slides (structure: ${options.structure.join(',')}).`);
console.log(`Customize colors in ${stylesOutputPath}, then open ${options.output} in a browser to view.`);
}
main();
#!/usr/bin/env node
/**
* Local HTML Editor
*
* Usage: node edit-html.js <path-to-html-file>
*
* Opens your HTML file in the browser with editable text regions.
* Click any text to edit it, then click "Save" to write changes back to the file.
*
* By default, all <p>, <h1>-<h6>, <li>, <td>, <th>, <span>, and <div> elements
* become editable. You can customize this by modifying EDITABLE_SELECTORS.
*/
const http = require('http');
const fs = require('fs');
const path = require('path');
const { exec } = require('child_process');
// Configuration
const PORT = 3456;
const EDITABLE_SELECTORS = 'p, h1, h2, h3, h4, h5, h6, li, td, th, span:not(.edit-toolbar *), div:not(.edit-toolbar):not([contenteditable="false"])';
// Get the HTML file from command line args
const htmlFile = process.argv[2];
if (!htmlFile) {
console.error('Usage: node edit-html.js <path-to-html-file>');
console.error('Example: node edit-html.js index.html');
process.exit(1);
}
const htmlFilePath = path.resolve(htmlFile);
if (!fs.existsSync(htmlFilePath)) {
console.error(`File not found: ${htmlFilePath}`);
process.exit(1);
}
// The editor script that gets injected into the HTML
const editorScript = `
<style>
.edit-toolbar {
position: fixed;
top: 10px;
right: 10px;
z-index: 999999;
background: #333;
padding: 10px 15px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
display: flex;
gap: 10px;
align-items: center;
}
.edit-toolbar button {
background: #4CAF50;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
}
.edit-toolbar button:hover {
background: #45a049;
}
.edit-toolbar button.secondary {
background: #666;
}
.edit-toolbar button.secondary:hover {
background: #555;
}
.edit-toolbar .status {
color: #fff;
font-size: 13px;
margin-left: 10px;
}
.edit-toast {
position: fixed;
bottom: 30px;
left: 50%;
transform: translateX(-50%);
background: #333;
color: #fff;
padding: 12px 24px;
border-radius: 8px;
font-size: 14px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
z-index: 9999999;
opacity: 0;
transition: opacity 0.3s;
pointer-events: none;
}
.edit-toast.error {
background: #d32f2f;
}
.edit-toast.visible {
opacity: 1;
}
[contenteditable="true"] {
outline: 2px dashed transparent;
transition: outline-color 0.2s;
}
[contenteditable="true"]:hover {
outline-color: #4CAF5066;
}
[contenteditable="true"]:focus {
outline-color: #4CAF50;
outline-style: solid;
}
</style>
<div class="edit-toast" id="editToast"></div>
<div class="edit-toolbar">
<button onclick="saveChanges()">💾 Save</button>
<button class="secondary" onclick="location.reload()">↻ Reload</button>
<span class="status" id="editStatus">Click any text to edit</span>
</div>
<script>
function showToast(msg, isError) {
const toast = document.getElementById('editToast');
toast.textContent = msg;
toast.className = 'edit-toast visible' + (isError ? ' error' : '');
setTimeout(() => { toast.className = 'edit-toast'; }, 2000);
}
// Show toast on reload after save
const params = new URLSearchParams(location.search);
if (params.has('saved')) { showToast('Saved successfully'); history.replaceState(null, '', '/'); }
if (params.has('error')) { showToast('Error: ' + params.get('error'), true); history.replaceState(null, '', '/'); }
// Make elements editable
document.querySelectorAll('${EDITABLE_SELECTORS}').forEach(el => {
// Skip if inside toolbar or already has contenteditable set to false
if (el.closest('.edit-toolbar') || el.getAttribute('contenteditable') === 'false') return;
// Skip elements that only contain other editable elements (to avoid nested editing)
const hasOnlyElementChildren = el.children.length > 0 &&
Array.from(el.childNodes).every(n => n.nodeType !== 3 || !n.textContent.trim());
if (hasOnlyElementChildren && el.tagName !== 'LI') return;
el.setAttribute('contenteditable', 'true');
});
// Escape to exit edit mode (blur the active element)
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && document.activeElement.getAttribute('contenteditable') === 'true') {
document.activeElement.blur();
}
});
// Track changes
let hasChanges = false;
document.addEventListener('input', (e) => {
if (e.target.getAttribute('contenteditable') === 'true') {
hasChanges = true;
document.getElementById('editStatus').textContent = 'Unsaved changes';
}
});
// Save function
async function saveChanges() {
// Clone the document and strip editor UI from the clone (leave live DOM intact)
const clone = document.documentElement.cloneNode(true);
clone.querySelectorAll('.edit-toolbar, .edit-toast').forEach(el => el.remove());
clone.querySelectorAll('[contenteditable]').forEach(el => el.removeAttribute('contenteditable'));
clone.querySelectorAll('style, script').forEach(el => {
if (el.textContent.includes('edit-toolbar') || el.textContent.includes('saveChanges')) el.remove();
});
const html = '<!DOCTYPE html>\\n' + clone.outerHTML;
try {
const response = await fetch('/save', {
method: 'POST',
headers: { 'Content-Type': 'text/html' },
body: html
});
if (response.ok) {
hasChanges = false;
location.href = '/?saved=1' + location.hash;
} else {
throw new Error('Save failed');
}
} catch (err) {
location.href = '/?error=' + encodeURIComponent(err.message) + location.hash;
}
}
// Warn before leaving with unsaved changes
window.addEventListener('beforeunload', (e) => {
if (hasChanges) {
e.preventDefault();
e.returnValue = '';
}
});
</script>
`;
// Create the server
const server = http.createServer((req, res) => {
// Handle save request
if (req.method === 'POST' && req.url === '/save') {
let body = '';
req.on('data', chunk => body += chunk);
req.on('end', () => {
try {
fs.writeFileSync(htmlFilePath, body, 'utf8');
console.log(`✓ Saved changes to ${htmlFilePath}`);
res.writeHead(200);
res.end('OK');
} catch (err) {
console.error('Error saving:', err);
res.writeHead(500);
res.end('Error saving file');
}
});
return;
}
// Serve the HTML file with editor injected
const reqPath = req.url.split('?')[0];
if (req.method === 'GET' && (reqPath === '/' || reqPath === '/index.html')) {
try {
let html = fs.readFileSync(htmlFilePath, 'utf8');
// Inject the editor before </body> or at the end
if (html.includes('</body>')) {
html = html.replace('</body>', editorScript + '</body>');
} else {
html += editorScript;
}
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(html);
} catch (err) {
res.writeHead(500);
res.end('Error reading file');
}
return;
}
// Serve other static files (css, js, images) from the same directory
const baseDir = path.dirname(htmlFilePath);
const filePath = path.join(baseDir, req.url);
if (!filePath.startsWith(baseDir)) { res.writeHead(403); res.end('Forbidden'); return; }
if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
const ext = path.extname(filePath).toLowerCase();
const mimeTypes = {
'.css': 'text/css',
'.js': 'application/javascript',
'.json': 'application/json',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.ico': 'image/x-icon',
'.woff': 'font/woff',
'.woff2': 'font/woff2',
};
res.writeHead(200, { 'Content-Type': mimeTypes[ext] || 'application/octet-stream' });
res.end(fs.readFileSync(filePath));
return;
}
res.writeHead(404);
res.end('Not found');
});
server.listen(PORT, () => {
const url = `http://localhost:${PORT}`;
console.log(`\n🖊️ HTML Editor running at ${url}`);
console.log(` Editing: ${htmlFilePath}\n`);
console.log(' Click any text to edit, press Escape to deselect, then click Save.\n');
console.log(' Press Ctrl+C to stop the server.\n');
// Try to open browser
const openCmd = process.platform === 'darwin' ? 'open' :
process.platform === 'win32' ? 'start' : 'xdg-open';
exec(`${openCmd} ${url}`);
});Related skills
FAQ
Does revealjs require a build step?
revealjs does not require a build step. The skill generates an HTML file that loads reveal.js from a CDN plus a separate CSS file, and you open the HTML directly in a browser.
What files does revealjs create?
revealjs creates two files: an HTML file containing slides, themes, layouts, and speaker notes, and a CSS file for custom colors, typography, and component styling.