typst-cards
Typst Cards
Purpose
Turn a textual context and optional brand materials into professional PNG images for online communication, using Typst as the rendering engine. The skill manages the full flow: interview → theme → generation → review.
Prerequisites — verify Typst
Before any other step, check that Typst is installed:
typst --version
If not available, install it like this (Linux/WSL x86_64):
curl -fsSL https://github.com/typst/typst/releases/latest/download/typst-x86_64-unknown-linux-musl.tar.xz -o /tmp/typst.tar.xz && tar -xf /tmp/typst.tar.xz -C /tmp/ && mkdir -p ~/.local/bin && mv /tmp/typst-x86_64-unknown-linux-musl/typst ~/.local/bin/
Note:
~/.local/binmust be in yourPATHfortypstto be found. Addexport PATH="$HOME/.local/bin:$PATH"to your shell profile if needed.
Phase 1 — Interview (single message, mandatory)
Before creating any file, ask these questions to the user in a single message:
Visual materials available?
- Do you have a logo? If yes, where is the file? (PNG, SVG, or other)
- Do you have a
DESIGN.mdor brand guidelines to share? - Do you have color preferences? (specific palette, or a mood description: "dark and technical", "colorful and lively", "institutional", etc.)
- Do you have specific fonts to use?
Desired output:
- What is the topic or content of the cards?
- How many slides or cards? (a single image or a carousel?)
- Which formats do you need? (propose those best suited to the context, see table below)
If the user has no brand materials, propose a palette consistent with the context and ask for a quick confirmation before proceeding.
Supported formats
| Name | Typical use | Pixels | width | height | PPI |
|---|---|---|---|---|---|
| Square 1:1 | Instagram post, carousels | 1080×1080 | 7.5in | 7.5in | 144 |
| Portrait 4:5 | Instagram portrait | 1080×1350 | 7.5in | 9.375in | 144 |
| Story 9:16 | Instagram/TikTok Stories, Reels | 1080×1920 | 7.5in | 13.33in | 144 |
| Landscape 16:9 | Twitter/X, YouTube thumb, LinkedIn header | 1920×1080 | 13.33in | 7.5in | 144 |
| LinkedIn/OG 1.91:1 | LinkedIn post, Open Graph meta | 1200×628 | 8.33in | 4.36in | 144 |
Math: pixels = inches × PPI → e.g. 7.5in × 144ppi = 1080px.
For carousels, always use the same format for every slide.
Phase 2 — Material analysis
With DESIGN.md: read it and extract — and apply in theme.typ:
- Colors: every token (primary, secondary, accent, surface, neutral…)
- Fonts: if the DESIGN.md specifies different fonts for different roles (e.g. display vs body), use them all in theme.typ as separate variables (
DISPLAY,SANS,MONO). For example: a serif display face for headlines, a sans-serif for body. - Visual style: restrained? bold? editorial? Steer the layout accordingly.
- Explicit rules: if the DESIGN.md says "do not use X on Y", honor it.
Do not collapse everything into a single font — the display/body distinction is part of the brand.
With logo: note the path. You'll include it in Typst with:
#image("path/to/logo.png", height: 0.5in) // fixed height
#image("path/to/logo.svg", width: 2in) // fixed width
Without materials: pick a palette based on context:
- Tech/data topic → dark background
#0d1117, accent blue#58a6ff - Consumer/lifestyle topic → white background, vivid colors
- Institutional/PA topic → sober palette, neutral tones with a measured accent
- Editorial/journalism topic → strong typography, high contrast
Phase 3 — File structure
Always create this structure in the project directory:
<project>/carousel/
├── theme.typ # design tokens and helper functions
├── slides.typ # content (imports theme.typ)
└── output/ # generated PNGs
├── slide-1.png
├── slide-2.png
└── ...
Recommended starting point
Instead of writing from scratch, copy one of the reference templates from the skill folder and adapt it:
references/theme-starter.typ→ palette, fonts, helpers (lbl,ctr,codebox,divider)references/slides-1x1-starter.typ→ 5 Instagram 1:1 slides (cover + 3 content + outro)references/slides-16x9-starter.typ→ 3 LinkedIn 16:9 slides (cover + two-column + 3 stat)
The templates are already tested and compile cleanly. They give you a proven structure; you change colors, fonts, content.
# example:
cp <SKILL_DIR>/references/theme-starter.typ <project>/carousel/theme.typ
cp <SKILL_DIR>/references/slides-1x1-starter.typ <project>/carousel/slides.typ
# then edit content in slides.typ and tokens in theme.typ
theme.typ — design tokens
Adapt colors to the materials gathered. This is a starting point:
// — palette —
#let BG-DARK = rgb("#0d1117")
#let BG-LIGHT = rgb("#f6f8fa") // light background
#let BG-WHITE = rgb("#ffffff") // pure white
#let ACC = rgb("#58a6ff") // accent on dark background
#let ACC-L = rgb("#0969da") // accent on light background
#let FG-DARK = rgb("#e6edf3") // text on dark
#let FG-LIGHT = rgb("#1c2128") // text on light
#let MUTED-D = rgb("#8b949e") // secondary on dark
#let MUTED-L = rgb("#656d76") // secondary on light
#let CODE-BG = rgb("#161b22")
#let CODE-BR = rgb("#30363d")
// — fonts —
// Safe fonts on Linux/WSL: DejaVu Sans, DejaVu Sans Mono
// Check availability: fc-list | grep -i "Font Name"
#let SANS = ("DejaVu Sans", "Liberation Sans", "Arial")
#let MONO = ("DejaVu Sans Mono", "Liberation Mono", "Courier New")
// — helper: section label —
#let lbl(body, dark: false) = text(
size: 9pt, weight: "bold", tracking: 2pt,
fill: if dark { ACC } else { ACC-L },
)[#upper(body)]
// — helper: slide counter (for carousels) —
#let ctr(n, total, dark: false) = align(right)[
#text(size: 9pt, fill: if dark { MUTED-D } else { MUTED-L })[#n / #total]
]
// — helper: code block —
#let codebox(body) = block(
fill: CODE-BG, stroke: 0.5pt + CODE-BR,
radius: 4pt, inset: (x: 14pt, y: 11pt), width: 100%,
)[
#set text(font: MONO, size: 10.5pt, fill: FG-DARK)
#body
]
slides.typ — base structure
#import "theme.typ": *
// Pick the format (only one active line):
#set page(width: 7.5in, height: 7.5in, margin: (x: 0.62in, y: 0.58in)) // 1:1
// #set page(width: 7.5in, height: 9.375in, margin: (x: 0.62in, y: 0.58in)) // 4:5
// #set page(width: 7.5in, height: 13.33in, margin: (x: 0.62in, y: 0.70in)) // 9:16
// #set page(width: 13.33in, height: 7.5in, margin: (x: 0.80in, y: 0.58in)) // 16:9
// #set page(width: 8.33in, height: 4.36in, margin: (x: 0.62in, y: 0.45in)) // 1.91:1
#set text(font: SANS, size: 15pt, fill: FG-LIGHT)
// — SLIDE 1 (dark background) —
#set page(fill: BG-DARK)
#v(1fr)
#lbl(dark: true)[tag · topic]
#v(0.15in)
#text(size: 44pt, weight: 900, fill: FG-DARK)[
Main title\
of the slide
]
#v(0.2in)
#text(size: 14pt, fill: MUTED-D)[Short subtitle or description]
#v(1fr)
#ctr(1, 6, dark: true)
// — SLIDE 2 (light background) — different settings → automatic page break
#set page(fill: BG-LIGHT)
#lbl[02 · section]
#v(0.2in)
#text(size: 31pt, weight: 900)[Section title]
#v(0.2in)
#text(size: 14pt, fill: MUTED-L)[Slide body text.]
#v(1fr)
#ctr(2, 6)
// — SLIDE 3 (same background as slide 2 → explicit pagebreak) —
#pagebreak()
// ... content ...
Compilation
cd <project>/carousel
typst compile slides.typ "output/slide-{p}.png" --ppi 144
The {p} is replaced by the page number → one PNG per slide.
Design principles for social cards
Text hierarchy: large title → subtitle → body. Max 3 levels. Don't crowd.
White space: generous margins (0.5–0.7in). Let the content breathe.
Colors: 2–3 max. A vivid accent on a neutral background always works.
Font sizes on a 1080px canvas (1pt Typst ≈ 2px output):
- Cover title: 42–50pt
- Section title: 28–34pt
- Body: 14–16pt
- Label/tag: 9–10pt bold uppercase with tracking
Carousels: counter at bottom right of every slide (1 / 6, 2 / 6, ...).
Story 9:16: center the content vertically, use larger fonts, avoid corners.
Known pitfalls
Grid with wide numbers/text: columns: (1fr, 1fr, 1fr) causes overflow if values are wide. Use a vertical layout or columns: (auto, 1fr) with a generous gutter.
Unavailable fonts: always specify fallbacks (("Chosen Font", "DejaVu Sans", "Arial")). Check with fc-list | grep -i "name". A warning is not an error: Typst uses the fallback.
#set page(fill: X) does not create a new page if X doesn't change: between slides with the same fill, use an explicit #pagebreak().
v(1fr) works at page level: it splits the leftover space. If you place two of them, the space is split evenly between the two points.
Logo with transparent background: prefer SVG when possible. PNG with alpha works but requires that the slide background does not contrast badly with it.
The < symbol in Typst content: it is interpreted as a label opening and causes an error. Replace with words ("less than", "lower than") or use $lt$ in math mode.
leading is not a parameter of text(): it belongs to par(). To control line height of a text block:
// WRONG — error "unexpected argument: leading"
#text(size: 14pt, leading: 1.4em)[...]
// CORRECT — use par leading inside a block
#block[
#set par(leading: 0.7em)
#text(size: 14pt)[...]
]
align(center + horizon) with long text causes incorrect wordwrap: words can fuse. Prefer to handle vertical and horizontal alignment separately, or use block(width: 100%) to contain the text.
Editorial pattern (kicker + huge title + footer):
// magazine/newspaper style — battle-tested on 1:1
#text(size: 9pt, weight: "bold", tracking: 3pt, fill: ACC)[#upper("category · section")]
#v(0.08in)
#line(length: 100%, stroke: 1pt + ACC)
#v(0.25in)
#text(size: 14pt, fill: DIM)[Eyebrow]
#v(0.05in)
#text(size: 60pt, weight: 900)[Big title]
#v(0.05in)
#text(size: 18pt, weight: "bold", fill: ACC)[Accent subtitle]
#v(1fr)
#line(length: 100%, stroke: 0.5pt + rgb("#333333"))
#v(0.1in)
#grid(columns: (1fr, 1fr, 1fr),
text(size: 11pt)[Date],
align(center, text(size: 11pt)[Place]),
align(right, text(size: 11pt, fill: ACC)[CTA]),
)
Phase 4 — Review and iteration
- Compile with
typst compileand read the resulting PNGs with the Read tool - Show the generated slides to the user
- Ask for feedback: colors, text sizes, layout, content
- Typst recompiles in <1s: iterate quickly until satisfied