Appearance
Components
Components let you define reusable elements with their own structure, attributes, slots, and data. They reduce repetition and keep templates readable.
Defining Components
Components are defined in the <components> top-level block. The tag name becomes the component's name:
xml
<components>
<footer>
<frame width="100%" direction="row" padding-top="12pt" font-size="8pt">
<frame width="fill">{{ data.company }}</frame>
<frame width="fill" text-align="center">
<page-number /> / <total-pages />
</frame>
<frame width="fill" text-align="right">{{ data.phone }}</frame>
</frame>
</footer>
</components>Use it anywhere in your document:
xml
<page>
<flow name="body" />
<footer />
</page>Attributes
Components accept attributes through {{ attrs.attribute-name }}. Set defaults in the component's opening tag:
xml
<components>
<amount currency="£" value="">
{{ attrs.currency }}{{ attrs.value }}
</amount>
</components>Use with custom values:
xml
<amount value="1,200" /> <!-- outputs: £1,200 -->
<amount value="850" currency="$" /> <!-- outputs: $850 -->Attributes set on usage override the defaults in the definition.
Conditional Logic in Components
Use ternary expressions to handle optional attributes:
xml
<components>
<labelled label="" label-style="bold" gap="8pt" font-size="8pt">
<frame direction="row">
<frame font-style="{{ attrs.label-style }}"
space-after-desired="{{ attrs.gap }}">
{{ attrs.label }}
</frame>
<frame><slot /></frame>
</frame>
</labelled>
</components>xml
<labelled label="Phone:">+44 7700 123456</labelled>
<labelled label="Email:" label-style="regular">info@example.com</labelled>Slots
Default slot
The <slot /> element is a placeholder for content passed into the component:
xml
<components>
<info-card title="" background="#f9fafb">
<frame background-color="{{ attrs.background }}" padding="16pt"
border-radius="4pt" break="never">
<h3 space-after-desired="8pt">{{ attrs.title }}</h3>
<slot />
</frame>
</info-card>
</components>Everything placed between the component's tags replaces <slot />:
xml
<info-card title="Key Metric" background="#eff6ff">
<frame direction="row">
<frame font-size="28pt" font-style="bold">94%</frame>
<frame font-size="10pt" font-color="#6b7280" v-align="center" padding-left="8pt">
customer satisfaction
</frame>
</frame>
</info-card>Named Slots
By default, <slot /> captures all child content passed into a component. Named slots let you direct different pieces of content to specific locations within a component by giving each slot a name attribute.
When using a component with named slots, wrap content in a tag matching the slot name. Any content not wrapped in a named tag goes to the default <slot />.
xml
<components>
<card title="">
<frame padding="16pt" border-radius="4pt" break="never">
<h3>{{ attrs.title }}</h3>
<slot />
<frame padding-top="12pt" font-size="8pt" font-color="#6b7280">
<slot name="footer" />
</frame>
</frame>
</card>
</components>xml
<card title="Q4 Summary">
This is the main body content and fills the default slot.
<footer>
Last updated: 14 Jan 2026
</footer>
</card>Single-Item Named Slots
Add single="true" to a named slot to consume one child element at a time instead of all matching content at once. This is useful inside <repeat> to iterate through slot content:
xml
<components>
<alternating-table as="table" even-style="@even" odd-style="@odd">
<thead>
<slot name="thead" />
</thead>
<tbody>
<repeat>
<slot name="tbody" single="true" style="{{ attrs.even-style }}" />
<slot name="tbody" single="true" style="{{ attrs.odd-style }}" />
</repeat>
</tbody>
</alternating-table>
</components>xml
<styles>
<alias name="even">
<background-color>#EFEFEF</background-color>
</alias>
<alias name="odd">
<background-color>#FFFFFF</background-color>
</alias>
</styles>xml
<alternating-table>
<thead>
<tr>
<th>Name</th>
<th>Amount</th>
</tr>
</thead>
<tbody>
<tr><td>Item A</td><td>£100</td></tr>
<tr><td>Item B</td><td>£200</td></tr>
<tr><td>Item C</td><td>£300</td></tr>
</tbody>
</alternating-table>Each <slot name="tbody" single="true"> pulls one <tr> at a time from the <tbody> content. The <repeat> cycles through the two slots, alternating the @even and @odd styles across rows.
Components with show-if
Control component visibility with show-if, just like any other element:
xml
<components>
<page-footer show-page-num="true" font-size="8pt">
<frame width="100%" direction="row" v-align="center" padding-top="12pt">
<frame width="fill">{{ data.website }}</frame>
<frame width="fill" text-align="center" h-align="center">
<frame show-if="{{ attrs.show-page-num }}">
<page-number /> / <total-pages />
</frame>
</frame>
<frame width="fill" text-align="right">{{ data.phone }}</frame>
</frame>
</page-footer>
</components>xml
<!-- First page: no page number -->
<page-footer show-page-num="false" />
<!-- Subsequent pages: with page number -->
<page-footer />Merging Components
When using template/payload separation, both can define components. Payload components are merged with template components -- this lets you add new components via the API without modifying the template.
The Standard Library
Press includes a set of pre-defined components that map common HTML-like tags to Press primitives:
| Component | Description |
|---|---|
<p> | Paragraph |
<h1> - <h6> | Heading levels (auto-added to "headings" outline) |
<grid> | Shorthand for <frame direction="row"> |
<b> / <strong> | Bold text |
<i> / <em> | Italic text |
<u> | Underlined text |
<s> | Strikethrough text |
<pre> | Preformatted text |
<code> | Monospace/code text |
<hr> | Horizontal rule |
<blockquote> | Block quotation |
<inline-block> | Inline-level frame |
<td>, <tr>, <th> | Table components |
These are defined as components, not special syntax, so they can be overridden in your own <components> block if you want to customise their behaviour. Overriding components such as <p> can also change how Markdown is rendered in your template, giving you complete control over how things behave and look.
Example: Components for an Invoice
xml
<components>
<amount style="@mono">
{{ data.currency ? data.currency : '£' }}{{ attrs.value }}
</amount>
<labelled label="" font-size="8pt" space-after-desired="4pt">
<frame direction="row">
<frame font-style="bold" space-after-desired="8pt">{{ attrs.label }}</frame>
<frame><slot /></frame>
</frame>
</labelled>
<page-footer show-page-num="true" space-before-desired="16pt" font-size="8pt">
<frame width="100%" direction="row" v-align="center" padding-top="12pt">
<frame width="fill">{{ data.website }}</frame>
<frame width="fill" text-align="center">
<frame show-if="{{ attrs.show-page-num }}">
<page-number /> / <total-pages />
</frame>
</frame>
<frame width="fill" text-align="right">{{ data.phone }}</frame>
</frame>
</page-footer>
</components>