How to make a poster¶
A page of a declared size, with something anchored to the bottom edge. This is the recipe behind examples/hop3/, four marketing posters at A4 and social sizes.
1. Declare the page¶
A figure sizes itself; a poster does not. Give it a height:
| Format | Size |
|---|---|
| A4 portrait | 842 × 1191 |
| A3 portrait | 1191 × 1684 |
| Social (4:5) | 1080 × 1350 |
| Slide (16:9) | 1920 × 1080 |
Units are not pixels — the SVG carries a viewBox, so it scales. Treating a unit as a point makes A4 come out right for print.
2. Pin the footer with a spring¶
With a fixed height the root stack gets the whole page, and any child marked grow=True absorbs what is left over:
from svg_plus import Spacer
doc.add(
masthead,
headline,
body,
Spacer(0.0, grow=True), # eats the slack
call_to_action,
footnote,
)
That is the whole trick. Everything above the spring flows from the top, everything below sits on the bottom edge, and adding a sentence in the middle moves nothing but the spring.
3. Check the content actually fits¶
A poster that overflows its page just overflows — nothing clips it. Measure before you trust it:
from svg_plus import Stack
root = Stack(*doc.blocks, gap=doc.theme.gap)
content = root.measure(doc.width - 2 * doc.pad, doc.theme)
assert content <= doc.height - 2 * doc.pad, f"over by {content - (doc.height - 2 * doc.pad):.0f}"
Worth putting in a test. Porting four hand-written posters found a headline that ran 118 units off the page, which nobody had noticed because nothing was measuring it.
4. Set the type scale in one place¶
A poster is mostly typography. Put the scale in the family's settings module, not in each figure:
def headline(*lines, size):
"""Composed lines, not a broken paragraph — a headline breaks where you say."""
return Stack(
*(Text(text, size=size, weight=700, fill=fill, wrap=False, leading=size * 1.14)
for text, fill in lines),
gap=0.0,
)
wrap=False is the point: a headline breaks where the writer says, not where the measure falls. leading around 1.14 × size keeps display type tight; body text wants the theme's 1.3–1.4.
Use it with a colour per line, which is how a two-tone headline is built:
5. Full-bleed bands¶
For a band that runs to the page edge, set the document padding to zero and let each block bring its own:
Doc(1080, height=1350, pad=0).add(
Frame(hero_stack, fill="accent", stroke="none", pad=28.0, radius=0.0),
Frame(body_stack, pad=24.0, stroke="none"),
Spacer(0.0, grow=True),
Frame(footer_row, fill="surface", stroke="none", pad=16.0, radius=0.0),
)
radius=0.0 matters — a rounded corner at the page edge reads as a mistake.
6. Put the logo on it¶
from svg_plus import Image, Row, Text
Row(
Image("logo.svg", height=34.0),
Text("Annual report 2026", size=20.0, weight=700, align="right", wrap=False),
)
The file is embedded as a data URI, so the poster stays one self-contained SVG that survives being emailed. Give a height; the width follows from the image.
7. Light and dark from one tree¶
Colours are names, so a second theme is a second poster and no second block tree:
from dataclasses import replace
NIGHT = replace(DAY, ink="#ffffff", muted="#8b94a7", accent="#7c8dff",
surface="#151d33", rule="#232b40", page="#0b1020")
build(DAY).save("light.svg")
build(NIGHT).save("dark.svg")
The same applies to a second language: the text measures itself, so a French poster and an English one lay themselves out without anything being nudged.
8. Load the real font¶
Built-in metrics are approximate — several percent off the face a browser resolves, which shows in justified text. For anything going to print:
from svg_plus import Theme, find_font, load_font
neue = find_font("HelveticaNeue")
THEME = Theme(
body=load_font(neue, family="Helvetica Neue, Helvetica, sans-serif"),
bold=load_font(neue, family="Helvetica Neue, Helvetica, sans-serif", weight=700, index=1),
italic=load_font(neue, family="Helvetica Neue, Helvetica, sans-serif", italic=True, index=2),
)
index= picks a face out of a .ttc collection, where the bold and the regular share one file.
9. Output¶
doc.save("poster.svg") # the master
doc.save("poster.pdf") # for print
doc.save("poster.png") # needs the [png] extra
Check the PDF before trusting it: svglib resolves fonts itself, and a family it does not know falls back to Helvetica.
Checklist¶
-
Doc(width, height=…)with the format you actually need. - One
Spacer(grow=True)above the footer. - Content measured against the page, in a test.
- Headlines
wrap=False, with the breaks you chose. - Real font files loaded, not the built-in metrics.
- Full-bleed bands have
radius=0.0. -
doc.describe(...)set.