Theme generator
Generate a new Marp slide theme CSS file from 1–3 input brand colors, following the var-based palette contract used by the marp-slides skill. Picks an appropriate color scheme (complementary, split-complementary, triadic, analogous, mono, or scheme-from-inputs), generates every palette slot (bg/fg/heading/em/links/code/tables/cards/compare-A-B/TL;DR/process-flow badge/cover title/section divider/hljs tokens), validates WCAG-2 contrast on critical pairs and iterates L until they pass, then writes a drop-in theme CSS with the layouts.css block appended. Use when the user says "make a theme from this brand color", "generate a marp theme for our brand", "I want a new theme based on #X", "convert these brand colors to a slide theme", or asks to extend the theme catalog.From its SKILL.md
npx -y skills add RanaCM/marpsmith --skill theme-generatorAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
- runs commandsInstructs the agent to run 3 commands, including `node <skills-dir>/theme-generator/scripts/generate-theme.mjs` and 2 more.
SKILL.md
16.6 KB, ~4.1k tokens by cl100k_base, as published. Nobody here has run it
Theme generator for Marp slide decks
Given 1–3 brand colors, produce a complete Marp theme CSS file that drops into ~/.marp/ (or any project's .marp/) and conforms to the var-based palette contract used by the starter themes in the marp-slides skill. Output is a single self-contained CSS file with palette + appended layouts block.
Related skills
Part of the marpsmith bundle — composes with:
- marp-slides — the themes you generate plug into its decks (interchangeable-theme contract).
- deck-a11y-audit — audit the generated theme for WCAG contrast before using it.
Script paths below use
<skills-dir>— the skills directory configured by the active agent harness; the marpsmith skills live there together.
Scope
This skill owns:
- Mapping 1–3 brand hex inputs to every palette slot the marp-slides contract expects.
- Deciding a color scheme (complementary / split-complementary / triadic / analogous / mono / scheme-from-inputs) based on input count + accent-style hint.
- Validating contrast on critical foreground-vs-background pairs (WCAG 2.1 AA), iterating L until pairs pass.
- Suggesting an optional layout variant (glow / rounded / uppercase-divider / circle-badges / square-badges) when the palette mood implies one.
- Writing a complete theme CSS file that includes the layouts.css block, ready for
themeSet:registration.
This skill defers:
- Building slide decks →
marp-slidesskill. - Critiquing colour-blindness / APCA / vision impairment beyond WCAG AA →
deck-a11y-auditskill. - Re-running auto-fit / split passes after theme swap →
marp-slidesskill (theme swap shouldn't trigger them since the layout block is identical).
Workflow
-
Collect inputs from the user:
- Required: at least one brand hex (e.g.
#E63946). Accept 1, 2, or 3 brand colors. - Required: a
--slugfor the theme (kebab-case; becomes both the filename and the@themedirective). - Optional:
--bg dark|light— bg-tone hint. If omitted, heuristic uses brand L: very dark (< 0.35 L) → dark theme; very pale (> 0.78 L, low S) → light theme; otherwise dark (default — most brand palettes pop better on dark). - Optional:
--accent vivid|muted|pastel— saturation/L tuning. Defaultvivid. - Optional:
--scheme auto|complementary|split|triadic|analogous|mono— override the auto-pick. - Optional:
--variant auto|none|glow|rounded|uppercase-divider|circle-badges|square-badges. - Optional:
--out <path>— defaults to<skills-dir>/marp-slides/themes/<slug>.css. For a project-local theme, use<project>/.marp/<slug>.css.
- Required: at least one brand hex (e.g.
-
Run the generator script:
node <skills-dir>/theme-generator/scripts/generate-theme.mjs \ --brand "#E63946" \ --slug crimson-night \ --bg dark \ --accent vivid \ --variant glowIt writes the CSS file, prints a contrast report to stderr, and exits non-zero only on argument errors.
-
Inspect the contrast report. Every critical pair should pass
OK(≥ 4.5:1, WCAG AA normal text). If anything reportsLARGE-ONLYorFAIL, see "Troubleshooting" below. -
Register the theme in the user's
.marprc.yml:themeSet: - .marp/<slug>.cssOr, if writing to
<skills-dir>/marp-slides/themes/, the user's existingthemeSetentry pattern probably already covers it — confirm with the user before adding/removing entries. -
Smoke-test by rendering an existing deck. Pick the user's most-recent deck or a canonical one (
lemborexant-drug-interaction.slides-dark.mdor any of the workspace's*.slides-*.mdfiles), copy it to a tmp path, swap thetheme:line in frontmatter to the new slug, and render withmarp:marp --config-file ./.marprc.yml --html --allow-local-files -o ./<deck>.html ./<deck>.md marp --config-file ./.marprc.yml --images png ./<deck>.mdThen
Readslides 1 (title-cover) and a comparison/matrix slide to visually confirm:- Heading colour reads against the bg.
- Compare A / B columns have distinct accents that aren't muddy.
- TL;DR callouts (if any) have legible text against their own bg.
- Process-flow badges contrast against the bg.
-
Report to the user: scheme used, bg-tone, variant applied, contrast ratios, any iterations needed, and the file path. Mention which existing themes are similar in mood (so the user knows where it fits).
Decision rubric: which color scheme?
When --scheme is auto (the default):
| Inputs | Scheme chosen | Rationale |
|---|---|---|
| 3 brand colors | triadic-from-input | Honour what the user gave; treat as triadic anchors. |
| 2 brand colors | dual-input | Use both inputs; synthesize a 3rd anchor at midpoint + 60° hue shift so it doesn't compete. |
| 1 brand color, no hint | split (split-complementary) | Default. Two accents 150°/210° away — high contrast without straight-complement tension. |
You can force a different scheme via --scheme:
complementary— primary + 180° opposite. Maximum contrast, but two-tone palettes can feel rigid (e.g. brand-only-red themes that want a strict identity).triadic— primary + 120° + 240°. Three roughly-equal anchors. Good when the brand wants playful balance across the spectrum.analogous— primary + ±30°. Harmonious, low-tension. Best for themes that should feel calm/editorial.mono— primary + low-S/high-L tint + low-S/low-L shade. Minimal aesthetic. Best when brand identity is a single recognisable hue and you don't want competing accents.split— primary + 150° + 210°. The default for a reason: gives heading + Compare-A from primary, em/Compare-B from one split anchor, highlight (numbered-list markers, big-stat) from the other. The three roles always have distinct identities without harshness.
The scheme math is deliberately simple HSL plus WCAG-2 contrast iteration. Smoke-render the result before treating it as finished; contrast math catches readability problems, but it does not replace visual judgment.
Decision rubric: which layout variant?
When --variant is auto:
| Condition | Variant suggested | Why |
|---|---|---|
| bg = dark + accent = vivid + heading S > 0.75 | glow | Neon palettes look great with text-shadow glow on headings + big-stat. |
| accent = pastel | rounded | Soft palette + soft corners reinforces the calm aesthetic. |
| accent = muted | uppercase-divider | Editorial muted palette pairs with a typographic section-divider treatment. |
| otherwise | none | Minimal-signature themes are also valid — dark.css itself ships no variant. |
You can also force one explicitly:
glow— text-shadow on headings + heavy glow on.big-stat-hero .stat. Reads as 80s/synthwave/neon.rounded— 16pxborder-radiuson.compare-grid > divand.matrix > div. Reads as friendly/modern.uppercase-divider— uppercase + 0.08em letter-spacing + bottom border onsection.section-dividerheadings. Reads as editorial.circle-badges—border-radius: 50%on.process-flow ol li::before. Reads as soft/approachable.square-badges—border-radius: 0; clip-path: noneon the same. Reads as utilitarian/blockish.
Variants must remain single-selector overrides. Do not let a variant change positioning, flex direction, or grid templates — that breaks the layout contract that lets a deck render under any theme.
Palette slots the generator fills
The contract from <skills-dir>/marp-slides/themes/README.md. The script generates every slot below; do not hand-edit unless contrast iteration produced a colour you actively dislike.
Root vars on section:
background-color,color--card-tint— fill for.compare-grid > div,.matrix > div--table-row-bg/--table-row-alt-bg--compare-a-accent/--compare-a-tint--compare-b-accent/--compare-b-tint
Type slots: heading, em, strong (= body fg), link, inline code fg/bg, code-block fg/bg.
hljs token slots: comment, keyword, string, number, function, built-in, attr, operator, punctuation.
Tables: th bg, th fg (= heading), td fg (= body), border colour.
Layout overrides:
section.title-cover h1colour (the "cover title" — typically the most attention-getting accent).section.section-divider :is(h1, h2, h3)colour.section.prose-with-tldr blockquoteborder + bg + text + strong colour.section.big-stat-heroheading +.statcolour.section.pull-quoteglyph + body + h1-inside + attribution colours.section.process-flow ol li::beforebg + fg.section.comparison .compare-grid > div:first-child / :last-childborder + border-left.
Contrast validation
The generator enforces these minimums (WCAG 2.1 AA normal text = 4.5:1):
| Pair | Threshold | Iteration behaviour |
|---|---|---|
| heading vs bg | 4.5:1 | Walk L by 0.025 in the direction that increases contrast (up if bg-L < 0.5, down otherwise). Max 25 steps. |
| body fg vs bg | 7:1 (AAA) | Body is the most-read text — go for AAA. |
| em vs bg | 4.5:1 | Iterate L. |
| link vs bg | 4.5:1 | Iterate L. |
| highlight vs bg | 4.5:1 | Iterate L. |
| inline code fg vs code-block bg | 4.5:1 | Iterate L (against the code bg, not slide bg). |
| TL;DR text vs TL;DR bg | 4.5:1 | Iterate L. On light themes the TL;DR has its own pale-yellow-ish bg; on dark it uses the slide bg. |
Why iterate L, not S or H? Hue is the brand identity — never change it. Saturation changes look like "we picked a different brand colour". Lightness is the lever every palette tool actually pulls. If iteration can't reach threshold within 25 steps, the script ships the best-effort colour and flags the issue in the report (the user can then choose to lower expectations or pick a different scheme).
Output anatomy
The CSS file written by the script has this shape:
/* @theme <slug> */
@import 'default';
section { ...palette + var declarations... }
section :is(h1...h6) { color: ... }
section p, ul, ol, li { ... }
section blockquote { ... }
section em, strong, a { ... }
section :not(pre) > code { ...inline code... }
section pre { ...code block... }
section .hljs-* { ...token colours... }
section table { ... }
section hr, dt, dd, header, footer { ... }
/* Layout palette overrides */
section.title-cover h1 { ... }
section.section-divider :is(h1, h2, h3) { ... }
section.prose-with-tldr blockquote { ... }
section.big-stat-hero { ... }
section.pull-quote { ... }
section.process-flow ol li::before { ... }
section.comparison .compare-grid > div:first-child / :last-child { ... }
/* Layout classes — appended from layouts.css (byte-identical to other themes) */
... ~360 lines of layout block ...
/* Optional variant override at the very end */
section :is(h1...h6) { text-shadow: ... } /* or rounded / uppercase-divider / etc. */
What NOT to do
- Do not modify the layout block in the middle of the file. The layout block is byte-identical across all themes — that's what lets one deck render under any theme. If a structural change is genuinely needed, it belongs in
<skills-dir>/marp-slides/layouts.css(and propagates to every theme), not in one theme. - Do not generate themes that override layout structure broadly in their variant block. Variants must target a single layout selector (heading text-shadow, process-flow badge shape, section-divider typography, card border-radius). Anything that rewrites
display,flex-direction,grid-template-columns, or positioning belongs in the shared layout block. - Do not add
theme:at the project's.marprc.ymlconfig level — that overrides per-deck frontmatter and breaks theme selection across decks. Only add tothemeSet:. - Do not invent new palette slots. If a slot doesn't exist in the contract, the layout block won't read it and nothing will render. Extend the contract in
layouts.cssfirst if you genuinely need a new slot. - Do not skip the smoke render. Contrast math is necessary but not sufficient — the only way to catch a "technically passes WCAG but looks muddy/garish/clashy" theme is to render an actual slide and look at it.
- Do not silently change the brand hue. Saturation/L are fair game; hue is identity. If a brand colour fundamentally won't work on the chosen bg-tone (e.g. pure #FFFF00 yellow on light bg), report back and ask whether the user wants to flip bg-tone instead.
Troubleshooting
Contrast report shows FAIL for heading vs bg.
The script ran 25 L-iterations and still couldn't reach 4.5:1. Almost always means the brand hue has a saturation ceiling that caps achievable contrast (e.g. pure red on near-black, or pure yellow on white). Options:
- Flip
--bgto the opposite tone. - Try
--accent muted— the script lowers saturation, which on yellow/cyan/magenta makes L-iteration more effective. - Pick
--scheme mono— uses a hue-locked palette where the script can tweak only L freely.
TL;DR text reports low contrast on a light theme.
TL;DR uses a tinted yellow-family bg. If the highlight colour is already yellow, contrast against a yellow-tinted bg collapses. The script picks a desaturated dark variant of the highlight for the text — if it's still failing, the highlight base is probably yellow-on-light-bg which is structurally hard. Try --scheme split to push highlight to a non-yellow anchor.
Variant glow looks blurry on a high-contrast or pastel theme.
Glow needs a saturated heading colour on a dark bg — text-shadow: 0 0 8px currentColor reads as fuzz when the heading is desaturated. Either drop the variant or use --accent vivid and --bg dark.
The compare-A border on a light theme is hard to see.
The script picks compare-A-border at L ≈ 0.85 (light) so it reads as a faint card edge, not a competing accent — this is intentional. If the user wants a bolder card edge, they can hand-edit --compare-a-tint to use the accent at higher alpha.
Quality checklist
Before declaring done:
- CSS file exists at
--outpath and starts with/* @theme <slug> */. - Contrast report shows
OKon every critical pair (heading, body, em, link, highlight, inline-code, TL;DR). - At least one smoke render produced — open slides 1 and a comparison slide and read the PNG.
- Title-cover h1 is the most-saturated/most-attention-getting colour in the palette, not the heading colour.
- Compare-A and Compare-B accents are visually distinct (not both in the same hue range).
- No emoji in the CSS file unless the user explicitly asked.
- Theme registered in the appropriate
.marprc.yml(project-local or user-global, depending on--out).
Pitfalls
- Light themes are harder than dark themes. Yellow, cyan, and magenta all sit at high L by default — pushing them down to meet 4.5:1 against white may shift them visibly toward olive, teal, or burgundy. That's not a bug, that's WCAG. If brand fidelity is more important than accessibility for a particular slot, the user has to make that trade-off explicitly.
- The script reads
<skills-dir>/marp-slides/layouts.cssat run time. If that file is missing (e.g. the user runs the script in an isolated checkout), the CSS will be palette-only and won't render layout classes correctly. The script warns to stderr and proceeds. - HSL is not perceptually uniform. The iteration loop is mathematically correct for WCAG-2 contrast, but equal HSL lightness does not always look equally bright across hues. Smoke-render the result and adjust by hand if a brand-critical colour looks wrong.
- Variant + scheme interaction matters.
glowonmonolooks weak (low saturation = weak shadow).uppercase-dividerontriadiccompetes with already-loud accents.autovariant picks try to honour these but the user can always override. - Smoke render is the only catch for muddy palettes. A theme can pass all contrast checks and still look bad if Compare-A and Compare-B are too close in hue. Single-input split-complementary schemes usually work; analogous schemes can read as one-tone.
What ships with it: 1 file
29.6 KB alongside SKILL.md, 1 of them executable
scripts/
- generate-theme.mjsruns29.6 KB