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>