Skip to content

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:

ComponentDescription
<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>