Skip to content

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:

from svg_plus import Doc

doc = Doc(842, height=1191, theme=THEME, pad=64)   # A4 at 1 unit = 1 pt
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.

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.31.4.

Use it with a colour per line, which is how a two-tone headline is built:

headline(("Sans équipe DevOps,", "ink"), ("l'astreinte, c'est vous.", "#D94A3D"), size=68.0)

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.