Skip to content
HomePagesHomePages template kit

Frames and formats

How an image or document template adapts to each of its declared formats — the frame as a viewport you style with geometry, per-format variants as the destination escape hatch, bleed and trim, and where the platform stops enforcing and trusts your eye.

A website template renders into a browser window whose size you never know. An image or document template renders into a frame: a box whose exact size you declared. A social image is 1080×1350 and nothing else; a letter sheet is 8.5in by 11in at the dpi you named. Nothing about the machine looking at it can change that.

A frame template still adapts, because it declares more than one format. formats is its acceptance set — the geometries a deliverable built from this template may be produced at — and one codebase serves all of them. That is what “responsive” means here: one template, N declared geometries, resolved while you author. It is not what it means on a website, where one page must survive every width at once.

A frame is a page, and its window is the artwork. Wherever a frame renders, the window it is given is exactly the format’s bleed box — so viewport units and @media over width, height and aspect ratio read the frame’s own geometry, by construction. Styling a frame is styling a page against its viewport, which is the vocabulary you already have.

image and document are the rendered classes — the two whose output is a file captured from a frame rather than a page a browser serves. Where the rest of the corpus says “a rendered-class template”, it means one of these.

Because a frame is a page, and you already know how to style a page. Content fills the window, a width query means width, vw is one percent of it. That is the model your muscle memory runs on, and inside a frame it is simply true: the frame is the page and the sections within it are components — media queries for the page, container queries for the components inside it, exactly as on the web.

Because a frame-relative measurement has to survive the sections above it. Container units bind to the nearest ancestor declaring a container, and a section that declares its own — an ordinary thing for a component to do — would quietly capture every frame-relative measurement beneath it. Nothing in the markup separates that from intent, so nothing could warn you about it. vw and vh have no such ancestor to lose: they mean the frame from anywhere in the tree.

Because geometry generalizes and a list of format ids does not. A section styled by width and aspect ratio adapts on its own to a format declared later — including one added long after you shipped the section. A design branched per format id has to be re-audited every time the acceptance set grows.

That leaves the window itself as the one thing the whole model rests on, and it is checked rather than trusted: the window witness is the frame saying so out loud when something hands it the wrong box.

Enforcement stops there, on purpose. The platform refuses what is verifiably wrong and takes no position on what is a judgment call. It will not tell you that your headline is too near the cut, that your stack is fuller than it should be, or that a photo wants more room at story ratio. Those are design decisions and they are yours; what the platform owes you is the instrument to see them, which is what the format switcher and the trim and bleed guides are. A gate that scored fit would be guessing at intent, and a gate that guesses is one you learn to route around.

The kit wraps each page’s sections in the frame shell — you never write it. The shell is the bleed box: the trim size extended symmetrically by the format’s bleed, and the window every host owes the document. It sets three custom properties you can read from any section:

Token What it holds
--frame-w trim width, CSS px
--frame-h trim height, CSS px
--frame-bleed bleed on each edge, CSS px — 0 for a format that declares none

Only a physical document format (width/height in inches or millimetres, with a dpi) can declare bleed. Image formats and pixel-sheet document formats resolve to a bleed of zero, so their bleed box and trim box are the same rect and everything below still holds, as a no-op.

Physical sizes resolve at 96 CSS px per inch, always. dpi is the capture multiplier and never changes layout: an 8.5in sheet is 816 CSS px wide whether it prints at 150 or 300.

The shell fills its window and clips at its own edge, exactly as capture does, and stamps the rendering format’s id on the root as a data attribute — which is what the per-format variants below hook onto.

The frame root is the kit’s element, and the names on it are reserved. You never write the root, and what sits on it is the kit’s: the tr-frame class, the data-format stamp, the three geometry tokens above, the --tr-frame-window-* custom properties, and the root’s ::after pseudo-element, which is the window witness. Read the geometry tokens as much as you like — that is what they are for — and repurpose none of them. Everything else, inside your own sections, is yours.

Sections are the frame’s direct children, in composePages order, laid out as a flex column. Each section spans the full bleed box, and none of them may shrink: a stack that is too tall overflows the frame visibly instead of being quietly squeezed to fit. Seeing your content spill past the edge in the preview is the intended feedback.

Distributing slack is ordinary flex authoring against that column — flex-1 on the section that should absorb the leftover height of a one-section tile, mt-auto on a footer that should sit against the bottom edge. There is no frame-specific API for it, and a section’s own contract is unchanged: the same Section primitive, the same slots, computes, fixtures and theme a website section uses.

Style a frame section the way you would style a page, because that is what it is.

Viewport units are the frame-relative unit. 1vw is one percent of the frame’s width and 1vh one percent of its height, at every declared format — so a measurement written text-[5vw] or p-[4vw] holds its proportion across a whole family of formats with no branch at all. Most intra-family adaptation belongs here and needs nothing else.

Query the geometry where the design genuinely changes. Width, height and aspect-ratio media features all read the frame. Tailwind’s own width ladder is legal and means frame width, so md:grid-cols-2 applies at frames at least that wide; name thresholds of your own in theme.ts’s breakpoints group beside your colours and type, or write a one-off inline as min-[600px]:grid-cols-2. The decision no width alone can express — is this format portrait or landscape — is an aspect-ratio query.

Base plus overrides is the ordinary mobile-first ladder. Write the design unprefixed, flip the format switcher, and fix only what actually breaks — formats that share the base layout carry no overrides whatsoever, which is the common case inside one family. Its semantics are the familiar ones, including the part authors forget: a rule written at a min-height threshold for one format also fires at every taller format. That is ordinary responsive discipline rather than a frame quirk, and the switcher is how you see it.

Two formats with the same dimensions render identically, and that is the intent. A 1200×630 feed image and a 1200×627 one are the same design; rendering is a function of geometry, so nothing has to be said twice to make that true.

Geometry cannot express one thing: two destinations that are the same size and still want different artwork. The story family is the case — Instagram story, TikTok and Reels are all 1080×1920, and each covers a different part of that rectangle with its own interface.

For that, each format id you declare is also a variant prefix, compiled from your declaration; there is nothing to register. A format with id: "instagram-story" gives you instagram-story:pt-32, applying to the frame root and everything inside it when that format is the one rendering — the root’s data-format stamp is what it selects on.

House style is geometry first, destination second: a format variant states which destination is rendering, never how big it is. If you catch yourself reaching for one to make type smaller or a grid narrower, that is geometry — write it as geometry and it stays right at the next format too, including one you have not declared yet.

A prefix that names no format the template declares would compile to nothing and say nothing, so unknown-format-variant fails check on it. That is what catches both a typo and the subtler case: a section shared by two templates, prefixed for a format only one of them declares.

Bleed is the margin of error at the cut. A trimmed sheet is never cut exactly where the artwork says, so anything meant to reach the paper’s edge must be printed past it. Trim is where the blade is aimed; bleed is how far past that line the ink runs.

Two consequences shape how you author:

  • Backgrounds run to the bleed edge by default. Sections span the whole bleed box, so a section background already extends past trim with nothing extra from you. This is the correct print default and a no-op where bleed is zero. A background that must stop exactly at trim is the rare deliberate case, and it insets itself by --frame-bleed.
  • Content stays inside trim, and that is your padding’s job. Text, logos, prices and anything else the recipient must actually receive belong inside the trim box, with real margin between them and the cut. Pad from the token rather than from a guessed number, so the same section is safe at a format with bleed and at one without: p-[calc(var(--frame-bleed)+1.5rem)].

Safe areas are the same idea for screens, and the kit does not model them. A social format’s artwork is usually seen underneath the platform’s own interface — a story’s status bar and profile row along the top, its caption and call-to-action band along the bottom, a feed post’s overlaid controls. That chrome belongs to whoever is displaying the image, it differs per surface, and it changes without telling you. Keep type and logos clear of the bands you know about, using the same padding technique, and check the current keep-out guidance from the platform you are targeting rather than trusting a number you remember. Where two same-size formats differ only in the bands they lose, that difference is exactly what a format variant is for. Nothing enforces any of this for you, which is the honest answer — a keep-out zone the kit hard-coded would be wrong for some surface the day it shipped and stale for the rest within a release.

The model rests on one promise: whatever renders a frame gives that document a window that is exactly the format’s bleed box. The kit does not take it on trust. A frame document is the one place holding both halves of the answer — the geometry its format declares, and the window it was actually given — so it checks them against each other, and on a mismatch paints a panel over the whole frame naming both.

Seeing that panel means the window is wrong, not your CSS. There is nothing in your section to fix. Every viewport unit and every geometry query in the frame has resolved against a box that is not the artwork’s, so what is underneath the panel is not a preview of anything and must not read as one — which is why it covers the frame rather than sitting in a corner. Note the two geometries it names and raise it against whatever rendered the frame; do not restyle around it.

It costs a correct frame nothing. When the two agree the witness is fully transparent and outside the layout, so a properly windowed page renders exactly as it would if the witness did not exist. Its mechanics are in the CSS pipeline.

template-kit dev is where frame authoring happens. A rendered class defaults to one editing canvas over its whole page set, so the canvas shows every page at once — one frame per page, stacked and labeled — and you judge the artifact whole rather than a page at a time. Beyond that, the canvas gives a frame template two controls the website canvas does not have:

  • Format — the declared acceptance set, each option naming the format’s id, its output pixels and, for a physical format, its trim size. Switching re-renders every page at that format’s exact bleed geometry. This is the home of the base-plus-overrides workflow; the platform’s own editor has no such control, because a real deliverable has exactly one chosen format.
  • Trim & bleed guides — a toggle that overlays the trim line and shades the band between it and the frame edge, over every page on the canvas. It is host chrome drawn over the page, never markup, so turning it on changes nothing about the bytes rendered. This is the instrument the enforcement posture above leaves you: turn it on, look at where your content sits, decide.

The panel’s footer hands over the real artifact, through Download — the one action a rendered class offers, here as in the platform. For an image template it gives you every page of the canvas as one zip, each captured at the active format’s exact pixel geometry — the archive a user receives, not a single slide of it. For a document template it gives you the whole page set printed to a single PDF at trim-plus-bleed geometry. Both go through the same capture constants the platform’s own renderer uses, so page count, sheet size and where a page breaks are all answerable before submission. Both need Playwright installed in your workspace.

The artifact has no runtime; the page that produced it did. An island runs wherever a browser exists — the dev canvas, the platform’s editor canvas, and the headless browser that takes the capture — and capture flattens whatever it has painted into the pixels or the PDF. So a rendered class composes islands exactly as a website does: same "use client" module, same default export, same export const editor, same serializable props. There is no capture tier and no second island contract to learn.

What changes is not the contract but the audience. A rendered-class island paints its final state — there is no viewer. Nobody will click it, hover it, or scroll it, so an affordance is a dead control printed onto the artifact; write the island for the one frame it gets. That is authoring judgment, not a rule: nothing gates it, because nothing can tell a deliberate final state from an unreached one.

Motion is the exception that is still gated. rendered-no-motion refuses CSS animation and transition, and it takes no position on motion an island drives — JS-driven movement is not statically visible, so the capture preview is the instrument that shows you which frame flattens.

Everything that applies to a website section applies unchanged — marker coverage, determinism, purity lint, CSS budgets. On top of that, a rendered-class template answers to four rules, and each one refuses a defect rather than a preference:

Rule Refuses
rendered-no-fixed position: fixed and sticky, both of which bind wrongly in the stacked print document
rendered-no-motion keyframes, animation and transition in a class that captures one still frame
rendered-class-conflict one section composed by both a website template and a rendered-class one
unknown-format-variant a variant prefix naming no declared format

The CSS rules are graded over your compiled stylesheet, so a Tailwind utility and a hand-written declaration are judged identically — a lg: prefix cannot slip through by never appearing in a .css file.

Not a container. The frame declares no size container of its own, so cqw, cqh and container queries inside a section resolve against a container that section declares — ordinary component styling, and the right tool when you mean this component’s width rather than the frame’s. With no container ancestor at all they fall back to the viewport, which is the frame; do not lean on that, because a container declared above them later would take them without a word. Write vw and vh when you mean the frame.

Not a fit gate. Nothing measures whether your content fits the frame, whether a headline crowds the trim line, or whether a photo is cropped well. The rigid stack makes an overflow visible and the guides make the cut visible; the judgment is yours. Runtime overflow from a customer’s own edits is a separate concern the fill caps and the editor handle, not something you gate against at authoring time.

Not a second section contract. A frame section is written exactly like a website section — same primitive, same schema, same fixtures, same theme, and now the same responsive vocabulary. What differs is the target: a website section must survive every width there is, while a frame section is designed against a declared, finite set of geometries and may not shrink to fit. That is why a section may not be composed by both kinds of template — the same markup cannot be right for an unknown geometry and for a known one.

Not a per-format set of files. There is one section, one renderer, one stylesheet. A format is a prefix on a class, not a fork of your code — and if a design truly needs a fork, that is a second template sharing sections, not a branch inside one.

  • Sections and pages in template.ts — declaring output, the formats acceptance set, and the pages a frame template renders.
  • The CSS pipeline — the stylesheet mechanics behind the frame shell and its witness, and where breakpoints come from.
  • Preview with dev — the canvas, the endpoints behind it, and the rest of the preview loop.
  • The theme model — the tokens a frame section spends, which are the same ones a website section spends.
  • Run check — the acceptance gate the rules above run in.