HTML-First Deck
A two-phase way to build a custom, great-looking deck that the user can still edit in PowerPoint.
Why this workflow
PowerPoint is the worst place to iterate on visual design — slow, fiddly, hard to keep consistent. HTML/CSS is the best place to iterate: you can lay out a slide, preview it, and refine spacing, color, and hierarchy in seconds. But the final artifact has to be a real, editable .pptx so the user can tweak wording and branding in their own tool (an image-per-slide export is useless to them).
So: design in HTML, deliver in native PPTX. You get fast iteration AND an editable result.
What is in this folder:
| File | What it is |
|---|---|
references/html_prototype.html | Phase 1 starting point: three slides, brand colors as CSS variables, slideshow navigation |
scripts/pptx_helpers.py | Phase 2 helper library: geometry, palette, header / footer / card / pill / takeaway components |
scripts/render_diagram.py | Renders a code-drawn SVG to a high-resolution PNG |
scripts/render_pptx.ps1 | Exports every slide of a .pptx to PNG for checking (Windows + PowerPoint) |
examples/example_deck.py | A runnable three-slide deck built on the helpers |
examples/example_diagram.py | A runnable diagram generator showing every technique in the diagram section |
Voice — write titles like a consultant, not a copywriter
Read this before writing a single title, sub-header or body line. Getting the layout right and the words wrong produces a deck the user has to rewrite, which is worse than no deck. This applies to anything that goes in front of a client or a colleague — decks, HTML slideshows, dashboards, status reports, one-pagers.
The default LLM register for slides is punchy ad-agency copy. Most business audiences do not want it. Unless the user asks for something else, write in the register of a senior consultant writing for an executive audience: plain, declarative, specific, and finished in one pass.
The rule
A title states what the slide is. It does not perform.
Name the content. If a reader scanning only the titles cannot reconstruct the argument, the titles
have failed. Labelled and numbered beats clever every time — Phase 1: Refund Leakage tells a
reader where they are; The one we open with tells them nothing and makes them read on to find out.
Constructions to avoid
| Don't | Why it fails | Do |
|---|---|---|
The dashboard reports. / It doesn't explain. | The two-beat "setup then punchline". Reads as a slogan, not a finding. | The current dashboard reports the numbers but does not analyze them |
They dropped the last tool over dashboards. / Do not sell them another dashboard. | Same two-beat, plus it lectures the reader. | Why another dashboard will not solve this |
The one we open with: | A colon-tease that withholds the answer to create suspense. | Phase 1: Refund Leakage |
And here's how you fix it. | Chatty, and it answers a question nobody asked. | Recommended fix and build estimate |
Three things. Then we go and prove it. | Staccato fragments used for rhythm. | What we need from this meeting |
What lands on the table. | Idiom in place of a description. | What we deliver |
Every fortnight, a finding with a number on it. | A tagline where a label belongs. | Timeline |
Only somebody who sees all four suppliers will ever find this. | Rhetorical build-up. | Why our team is placed to do this work |
Also avoid: rhetorical questions as headings; single-word drama (The trap, The play, The close) as the main title; sentence fragments used for punch; em-dash reveals; "Here's the thing";
anything that sounds like it wants to be read aloud.
Concretely
- Titles: noun phrases or plain statements.
Analysis method.Data requirements and delivery dates.Questions for the client.Required to run Phase 1. - Number the work. Phases, tiers, steps.
Phase 1throughPhase 8,Tier 1/2/3. Numbering makes a program legible and gives people something to refer to in a meeting. - Sub-headers carry the qualifier, not a joke:
Phase 1 · method,Data request · Tier 1. - Body copy: full sentences with subject and verb. Say the thing once, properly, rather than three times with increasing emphasis.
- Keep the specifics. Plain does not mean vague.
$8.4M year to date at a 12.5% refund rateis exactly right. Numbers, names, dates and sources are what make it credible. - Bold sparingly — for the one phrase in a paragraph that carries the weight, not for rhythm.
- Quotes are the exception. A verbatim client quote can stay verbatim, and a genuinely strong single sentence can sit in a pull-quote — but state it as a finding, not as a tagline.
Percentages must carry their base
Percentages are good — keep using them. But a percentage with no base can't be judged. "The top 10% of agents" means 2 people if there are 20 agents and 100 people if there are 1,000; the reader can't tell whether it's a pattern or two individuals.
- Put the count next to the percentage, every time it's a share of people, things or events.
The top 6 agents (10% of 60)·4,100 customers (0.5% of 820,000)·Supplier A received $12.0M, half of all refund dollars ($24.0M). - When the group is small, lead with the number, not the percentage. Under ~30 items, the count
is the finding:
6 agents issued $18,000beats10% of agents issued 20%. - Two percentages side by side need both bases.
20% of the money on 12% of the contactsis only useful once the reader also knows it's $18,000 of $90,000 and 3,600 of 30,000 contacts. - Name the denominator. Say what the percentage is of:
of August refund dollars,of customers who claimed at least once,of agents with 10 or more days worked. - Say what a percentage means in money or people when you can. "3% were paid to a different
account" becomes
1,500 refunds ($150K), 3% of the year's total. - Share-of-whole findings deserve a share-of-whole chart, such as a 100% stacked bar, so the reader sees the half without doing arithmetic.
Self-check: for every % on the page, can the reader answer "of how many?" without leaving the sentence? If not, add the count.
The self-check before you ship
Read the deck's titles in sequence, on their own. If they read like a table of contents, it is right. If they read like a pitch, rewrite them.
Phase 1 — Prototype in HTML
Build deck.html with one .slide div per slide on a 16:9 canvas (1280×720). Put the brand colors
in :root CSS variables (pull them from the user's template — see "Brand" below). Lay out headers,
section titles, cards, callouts, tables. Iterate here until the design is right — preview by opening
it / screenshotting it, and get the user's sign-off on structure and look.
references/html_prototype.html is a ready starting point (cover, a header + three-card + takeaway
slide, a picture slide, footer) with the CSS-variable palette. Read and adapt it.
This phase is the design sandbox. Don't perfect every word — nail the layout system (the header/ footer chrome, the card style, the type scale, the accent usage) because Phase 2 mirrors it.
Make the prototype navigable like a slideshow
Always add this tiny script before </body> so the HTML preview advances like PowerPoint —
click (or →/↓/Space) goes to the next slide, ←/↑ goes back. It makes Phase 1 review feel like
presenting and is HTML-only (nothing to carry into Phase 2).
Highlighting text must never turn the page. People select text on slides to copy it. A drag
ends in a click event, so a plain click handler jumps to the next slide mid-copy. The script below
ignores the click when the mouse moved more than 5px between press and release, or when text is
selected. A real click still advances.
A click must advance from the slide that was clicked, never from a guess based on scroll position. The guess picks the wrong slide on some window sizes, so a click jumps two slides. Keys (→/←) can still use scroll position, since there is nothing to click.
<script>
/* Preview like a slideshow: click or →/↓/Space = next slide, ←/↑ = previous. Selecting text does not advance. */
(function(){
var slides=[].slice.call(document.querySelectorAll('.slide'));
function next(){ var y=scrollY+10; return slides.find(function(s){return s.offsetTop>y;}) || null; }
function prev(){ var y=scrollY-10, p=null; slides.forEach(function(s){ if(s.offsetTop<y) p=s; }); return p; }
function to(s){ if(s) s.scrollIntoView({behavior:'smooth',block:'start'}); }
var downX=0,downY=0;
addEventListener('mousedown',function(e){ downX=e.clientX; downY=e.clientY; });
addEventListener('click',function(e){
if(e.target.closest('a,button,input,textarea,select'))return;
/* highlighting text, or dragging the mouse, is not a click on the slide */
var sel=window.getSelection&&String(window.getSelection()); if(sel&&sel.trim())return;
if(Math.abs(e.clientX-downX)>5||Math.abs(e.clientY-downY)>5)return;
/* advance from the slide that was CLICKED, not from a guess based on scroll position */
var s=e.target.closest('.slide'); to(s ? slides[slides.indexOf(s)+1] : next());
});
addEventListener('keydown',function(e){
if(['ArrowRight','ArrowDown',' ','PageDown'].indexOf(e.key)>-1){e.preventDefault();to(next());}
else if(['ArrowLeft','ArrowUp','PageUp'].indexOf(e.key)>-1){e.preventDefault();to(prev());}
});
})();
</script>
If the deck is staying in the browser (an HTML slideshow), stop here: Phase 1 is the deliverable.
Phase 2 — Generate the native editable PPTX
Recreate the approved design with python-pptx so every element is editable. Use the bundled helper
library scripts/pptx_helpers.py — copy it into the deck folder. It provides 16:9 geometry, a brand
palette block, and the components you'll reuse on every slide:
new_prs(),blank_slide(prs),body_bg(slide)add_header(slide, title, meta),add_footer(slide, left, right)add_section_title,add_subtitleadd_text(...)— supports a string, a list of paragraphs, OR mixed-styled runs:[[("Bold bit ", {"bold": True}), ("normal bit", {"color": MUTED})]]add_rect,add_card(top/left accent stripe),add_pill,bulleted,add_takeawayadd_image(slide, path, x, y, w)— places a picture at its own proportionssave_deck(prs, path)— saves, and switches to a new filename if the file is locked/open
Structure (see examples/example_deck.py for a runnable three-slide example):
from pptx_helpers import *
def slide_intro(prs): ... # one builder function per slide
def slide_cards(prs): ...
def main():
prs = new_prs()
slide_intro(prs); slide_cards(prs)
print(save_deck(prs, DECK_DIR / "My Deck.pptx"))
Hard-won rules (these prevent the common failures)
- Never overwrite a file the user has open. PowerPoint locks the file; a plain
prs.save()then throwsPermissionError.save_deck()handles this by bumping tov2,v3… Keep the working copy somewhere obvious and tell the user which file is newest. - Build everything as shapes/text, not a picture. That's the whole point — they must be able to
edit it. Use real tables (
slide.shapes.add_table) for dense comparisons. - Set fonts explicitly on every run (python-pptx won't inherit a theme font reliably).
- Kill default shadows on autoshapes (
shape.shadow.inherit = False) and clear their default text — the helpers already do this. - One builder per slide + a
main()that lists them in order. Renumber footers there.
Preview / verify loop (do this — don't ship unseen)
After each build, render slides to PNG and actually look at them; fix overflow/spacing; rebuild.
- Windows (PowerPoint installed):
Writespowershell -NoProfile -File scripts/render_pptx.ps1 -pptx "My Deck.pptx" -outdir previewpreview/s1.png,s2.png… It opens the file read-only, so it works even while the user has the deck open, and it leaves PowerPoint running if it was already running. - No PowerPoint —
libreoffice --headless --convert-to pdf(or to PNGs) if available.
Then read the PNGs, adjust the builders, re-run. Iterate until clean.
Brand (colors + fonts)
Don't default to one palette. Pull the brand from the user's context: read a .pptx template's
theme (ppt/theme/theme1.xml → <a:clrScheme>), the company's site/CSS, or ask for 2–3 colors.
Set them once in the :root variables (HTML) and the BRAND block at the top of pptx_helpers.py
(PPTX) so the two phases match. Set WORDMARK_PATH to a logo PNG to brand the header, or leave None.
The colors that ship in both files are a neutral placeholder; replace them.
Diagrams inside slides — draw them with code, don't generate them
For the hero visual on a slide (a flow, pipeline, funnel, architecture or process diagram), don't hand-place dozens of PowerPoint shapes and don't use an AI image generator. Image models are built for photos and art. For diagrams they fail: blurry text, misspelled labels, shapes that don't line up, colors that drift off-brand, and nothing is editable.
Instead, write an SVG (a vector drawing defined in code: <path>, <circle>, <rect>,
<text>, with gradients and filters) and render it to a high-resolution PNG. A computer draws
every line to spec, so it's pixel-sharp, exactly on-brand, repeatable, and trivially editable
(change a hex code or a word and re-render).
An analogy for the user: asking an image model for a diagram is like asking an artist to freehand a blueprint from memory. Writing code to draw it is CAD — a ruler and compass — exact every time.
Use it for: pipeline / flow / funnel diagrams, architecture diagrams, process maps, concept figures, "system X → Y → Z" visuals, before/after and comparison layouts — and to rescue a diagram that came out of an image generator looking off. The same method works for a figure going into a report or a web page, not only a slide.
Don't use it for: photorealistic imagery, illustration or art, photo editing, or charts drawn from data (use a real charting tool for those).
examples/example_diagram.py is a ready-to-adapt generator that implements every technique below
in about 130 lines. Copy it and change the geometry, labels and palette.
1. Choose the layout and the FINAL proportions first
Decide the width-to-height ratio of the finished image and draw natively at that ratio. Set the
SVG viewBox to it (for example 0 0 1500 460 for a wide band under a slide title).
Never stretch or squash a finished PNG to fit a space — that is what turns circles into ovals. If the picture needs to be shorter or taller, change the canvas height in the code and re-render so the shapes stay correct. If the user gives a screenshot of the space, read its width and height.
2. Write an SVG generator (Python)
Build the SVG as a string in a small Python script (loops make repeated elements, like "hundreds
of lines", easy) and write it to a .svg file. Building blocks:
- Shapes from math, not eyeballing. A regular hexagon is six points on a circle:
pt = (cx + r*cos(a), cy + r*sin(a))forain 0, 60, … 300°. Circles and rounded rectangles are primitives. This is why the shapes come out exact. - Cubic bezier curves (
M x0 y0 C c1x c1y, c2x c2y, x1 y1) for flowing connectors. - Text as
<text>withfont-weight,letter-spacing,text-anchor.
3. Use the deck's palette
Define the palette as named constants at the top of the script — the same hex codes as the deck's
:root variables and the BRAND block — and reuse them everywhere. Passing exact hex codes is what
keeps the picture on-brand, and it makes recoloring a finished diagram a 30-second edit.
A palette usually takes this shape: a background, a dark ink, a muted gray, a hairline, one or two accents, and white for cards.
4. The techniques that make it look finished
- Layered translucent curves for density and flow. Draw 100–150 thin bezier lines, each at low opacity (about 0.10–0.22) with slight random jitter in its control points. Stacked, they read as a glowing funnel or fan. This is the hero effect. Seed the random generator so every run matches.
- Soft glow from an SVG filter:
<feGaussianBlur stdDeviation="6">merged over the shape, plus a faint, large radial-gradient halo behind key nodes. - Gradients for fills (linear on flat shapes, radial on circular nodes) add depth.
- A faint oversized watermark word (for example "100s") at very low opacity, for scale.
- Consistent nodes: hexagon or circle hubs with a 1px outline; clean cards = white rounded rectangle, thin border, a 6px accent stripe on the left, bold name + small muted sub-label.
5. Render to a crisp PNG
python scripts/render_diagram.py diagram.svg diagram.png # 2x resolution
python scripts/render_diagram.py diagram.svg diagram.png 3 # 3x, for large screens or print
The script opens the SVG in headless Chromium at the size of its viewBox and saves a
high-resolution screenshot. (Needs pip install playwright and playwright install chromium.
Libraries such as cairosvg are often missing and render filters poorly; a browser is the
reliable path.)
If you only have a browser-automation tool instead of the script: put the SVG in an HTML page with
its CSS width and height set to twice the viewBox, serve the folder over HTTP (python -m http.server — file:// pages are often blocked), size the window to match and take a screenshot.
6. Background: blend or transparent
The clean default is to fill the SVG background with the color of the slide it will sit on, so
the edges disappear and soft shadows and glows look right. If the picture must sit on more than one
background, leave the background rectangle out of the SVG and add --transparent when rendering.
7. Fonts
Put the deck's font first in the SVG's font-family, with safe fallbacks after it:
font-family="Aptos, 'Segoe UI', Arial, sans-serif". The picture is rendered with the fonts
installed on the machine doing the rendering.
8. Put it on the slide
- PowerPoint:
add_image(slide, "diagram.png", x, y, w)from the helpers. It sets only the width, so the height follows the picture's proportions and nothing distorts. (With python-pptx directly:slide.shapes.add_picture(path, left, top, width=...)and leave the height out.) - HTML prototype:
<img src="diagram.png" style="width:100%;height:auto">, or paste the SVG in.
Optional: keep the labels editable in PowerPoint
If the user wants to change the wording themselves, bake only the hard-to-draw art (the funnel,
hexagon, glow) into the picture and add the labels as native PowerPoint text boxes on top,
positioned with a canvas-to-inches map (x_in = LEFT + canvas_x / canvas_w * width_in). The
tradeoff: labels inside shapes look most polished baked in; standalone captions are fine as
editable text. Ask which they prefer.
Related
If the HTML deck is already final and you want it converted rather than rebuilt by hand, the
companion html-to-pptx skill (https://github.com/ImperaBusinessServices/html-to-pptx) converts an
HTML deck straight into native PowerPoint shapes.
Credit
Built by Impera Business Services. MIT License — see LICENSE.
