Skip to content
Sections
Language, currently English
Rastrillo Design System

Display

The small pieces of vocabulary a screen is assembled from: status, identity, progress and the one alert shape.

Each example is live, but its links go nowhere. HTML shows the markup to copy.

status-pill

A record's status as a tinted pill, colour and label.

Preview

neutral

<span rst-status rst-tone="neutral">Draft</span>

positive

<span rst-status rst-tone="positive">Published</span>

warning

<span rst-status rst-tone="warning">Scheduled</span>

negative

<span rst-status rst-tone="negative">Failed</span>

neutral

{{template "status-pill" dict "Label" "Draft"}}

positive

{{template "status-pill" dict "Label" "Published" "Tone" "positive"}}

warning

{{template "status-pill" dict "Label" "Scheduled" "Tone" "warning"}}

negative

{{template "status-pill" dict "Label" "Failed" "Tone" "negative"}}

neutral Tone defaults to neutral, so this call leaves it out.

badge

An uppercase bordered chip for small static markers. Distinct from status-pill, which is record status.

Preview

No tone (quiet default)

<span rst-badge>Draft</span>

neutral

<span rst-badge="neutral">Internal</span>

positive

<span rst-badge="positive">Live</span>

warning

<span rst-badge="warning">Beta</span>

negative

<span rst-badge="negative">Sold out</span>

No tone (quiet default)

{{template "badge" dict "Label" "Draft"}}

neutral

{{template "badge" dict "Label" "Internal" "Tone" "neutral"}}

positive

{{template "badge" dict "Label" "Live" "Tone" "positive"}}

warning

{{template "badge" dict "Label" "Beta" "Tone" "warning"}}

negative

{{template "badge" dict "Label" "Sold out" "Tone" "negative"}}

meter

A capacity bar with its number.

Preview

Empty

<span rst-meter>
  <meter rst-meter-bar value="0" min="0" max="100" aria-hidden="true"></meter>
  <span rst-meter-num>0/500</span>
</span>

Part way

<span rst-meter>
  <meter rst-meter-bar value="42" min="0" max="100" aria-hidden="true"></meter>
  <span rst-meter-num>210/500</span>
</span>

Nearly full

<span rst-meter>
  <meter rst-meter-bar value="82" min="0" max="100" aria-hidden="true"></meter>
  <span rst-meter-num>412/500</span>
</span>

Full

<span rst-meter>
  <meter rst-meter-bar value="100" min="0" max="100" aria-hidden="true"></meter>
  <span rst-meter-num>500/500</span>
</span>

Over budget

<span rst-meter>
  <meter rst-meter-bar value="100" min="0" max="100" aria-hidden="true"></meter>
  <span rst-meter-num>700/500</span>
</span>

Empty

{{template "meter" dict "Percent" 0 "Text" "0/500"}}

Part way

{{template "meter" dict "Percent" 42 "Text" "210/500"}}

Nearly full

{{template "meter" dict "Percent" 82 "Text" "412/500"}}

Full

{{template "meter" dict "Percent" 100 "Text" "500/500"}}

Over budget

{{template "meter" dict "Percent" 140 "Text" "700/500"}}

Over budget The fill is clamped to 100% in the partial.

stat

One number on a dashboard, with a label and how much it changed.

The big number

Preview

Put this inside <div rst-stats>.

<div rst-stat="lead">
  <span rst-stat-label>Revenue this month</span>
  <span rst-stat-num>€48,210</span>
  <span rst-stat-delta rst-tone="positive">&#43;12%</span>
  <span rst-stat-note>vs. last month</span>
</div>

Put this inside <div rst-stats>.

{{template "stat" dict
    "Label" "Revenue this month"
    "Value" "€48,210"
    "Delta" "+12%"
    "Tone" "positive"
    "Note" "vs. last month"
    "Lead" true}}

Add Lead to one stat in the row to make it big. It is the same component, just larger.

A smaller number

Preview

Put this inside <div rst-stats>.

<div rst-stat>
  <span rst-stat-label>Unpaid invoices</span>
  <span rst-stat-num>24</span>
</div>

Put this inside <div rst-stats>.

{{template "stat" dict "Label" "Unpaid invoices" "Value" "24"}}

You only need a label and a number. Everything else is optional.

When going down is good

Preview

Put this inside <div rst-stats>.

<div rst-stat>
  <span rst-stat-label>Time to first reply</span>
  <span rst-stat-num>1h 12m</span>
  <span rst-stat-delta rst-tone="positive">−38%</span>
  <span rst-stat-note>vs. last week</span>
</div>

Put this inside <div rst-stats>.

{{template "stat" dict
    "Label" "Time to first reply"
    "Value" "1h 12m"
    "Delta" "−38%"
    "Tone" "positive"
    "Note" "vs. last week"}}

You set the colour, not the component. Here the number went down and that is good news, so the change is green.

When going up is bad

Preview

Put this inside <div rst-stats>.

<div rst-stat>
  <span rst-stat-label>Failed payments</span>
  <span rst-stat-num>9</span>
  <span rst-stat-delta rst-tone="negative">&#43;3</span>
  <span rst-stat-note>vs. last week</span>
</div>

Put this inside <div rst-stats>.

{{template "stat" dict
    "Label" "Failed payments"
    "Value" "9"
    "Delta" "+3"
    "Tone" "negative"
    "Note" "vs. last week"}}

Here the number went up and that is bad news, so the change is red.

When you cannot say

Preview

Put this inside <div rst-stats>.

<div rst-stat>
  <span rst-stat-label>Average session length</span>
  <span rst-stat-num>4m 12s</span>
  <span rst-stat-delta rst-tone="neutral">&#43;18s</span>
  <span rst-stat-note>since Friday</span>
</div>

Put this inside <div rst-stats>.

{{template "stat" dict
    "Label" "Average session length"
    "Value" "4m 12s"
    "Delta" "+18s"
    "Note" "since Friday"}}

Leave the colour out and the change is grey. Do that when you cannot say if it is good or bad.

A shortened number

Preview

Put this inside <div rst-stats>.

<div rst-stat>
  <span rst-stat-label>Messages sent</span>
  <span rst-stat-num><data value="4120">4.1k</data></span>
</div>

Put this inside <div rst-stats>.

{{template "stat" dict "Label" "Messages sent" "Value" "4.1k" "Exact" "4120"}}

Showing 4.1k instead of 4120? Put the full number in Exact. Screen readers only read what is on screen, so keep anything a person needs in the number itself.

person

Initials avatar, name and email. Quickly identifies people records.

Name and email

Preview
<a rst-person href="/people/1">
  <span rst-person-av aria-hidden="true">G</span>
  <span rst-person-meta>
    <span rst-person-name>Grace Hopper</span>
    <span rst-person-email>grace@example.com</span>
  </span>
</a>
{{template "person" dict
    "Href" "/people/1"
    "Name" "Grace Hopper"
    "Email" "grace@example.com"
    "Initial" "G"}}

Large, the show-header size

Preview
<a rst-person="lg" href="/people/2">
  <span rst-person-av aria-hidden="true">A</span>
  <span rst-person-meta>
    <span rst-person-name>Ada Lovelace</span>
    <span rst-person-email>ada@example.com</span>
  </span>
</a>
{{template "person" dict
    "Href" "/people/2"
    "Name" "Ada Lovelace"
    "Email" "ada@example.com"
    "Initial" "A"
    "Large" true}}

No initial

Preview
<a rst-person href="/people/3">
  <span rst-person-av="empty" aria-hidden="true"></span>
  <span rst-person-meta>
    <span rst-person-name>Unclaimed invitation</span>
    <span rst-person-email>invited@example.com</span>
  </span>
</a>
{{template "person" dict
    "Href" "/people/3"
    "Name" "Unclaimed invitation"
    "Email" "invited@example.com"}}

actor

Who did something, as words: a person's name, or the agent and the person it acted for. No badge and no icon.

An agent acting for a person

Preview
<span rst-actor>Claude for Grace Hopper</span>
{{template "actor" dict "Actor" .Actor}}

Set ui.ActorName once at boot to turn a subject into a name. The app itself, the zero Actor, renders nothing.

A person

Preview
<span rst-actor>Grace Hopper</span>
{{template "actor" dict "Actor" .Actor}}

callout

The one alert vocabulary: tinted strip, tone icon, optional bold title, one body paragraph.

info

Preview
<div rst-callout rst-tone="info">
  <span rst-callout-ic aria-hidden="true">
    <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>
  </span>
  <div rst-callout-body>
    <p>Posts are visible to members only until the site is published.</p>
  </div>
</div>
{{template "callout" dict
    "Body" "Posts are visible to members only until the site is published."
    "Tone" "info"}}

positive

Preview
<div rst-callout rst-tone="positive">
  <span rst-callout-ic aria-hidden="true">
    <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/></svg>
  </span>
  <div rst-callout-body>
    <strong>Payments are connected</strong>
    <p>You can take payment for tickets now.</p>
  </div>
</div>
{{template "callout" dict
    "Body" "You can take payment for tickets now."
    "Title" "Payments are connected"
    "Tone" "positive"}}

warning

Preview
<div rst-callout rst-tone="warning">
  <span rst-callout-ic aria-hidden="true">
    <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg>
  </span>
  <div rst-callout-body>
    <strong>Connect payments to start selling</strong>
    <p>Your event is live but cannot take payment yet.</p>
  </div>
</div>
{{template "callout" dict
    "Body" "Your event is live but cannot take payment yet."
    "Title" "Connect payments to start selling"
    "Tone" "warning"}}

negative

Preview
<div rst-callout rst-tone="negative">
  <span rst-callout-ic aria-hidden="true">
    <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/></svg>
  </span>
  <div rst-callout-body>
    <strong>The last import failed</strong>
    <p>Nothing was changed. Fix row 91 and try again.</p>
  </div>
</div>
{{template "callout" dict
    "Body" "Nothing was changed. Fix row 91 and try again."
    "Title" "The last import failed"
    "Tone" "negative"}}

negative, announced as an alert

Preview
<div rst-callout rst-tone="negative" role="alert">
  <span rst-callout-ic aria-hidden="true">
    <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/></svg>
  </span>
  <div rst-callout-body>
    <strong>The import stopped</strong>
    <p>Nothing was changed. Fix row 91 and try again.</p>
  </div>
</div>
{{template "callout" dict
    "Body" "Nothing was changed. Fix row 91 and try again."
    "Title" "The import stopped"
    "Tone" "negative"
    "Alert" true}}

Alert adds role="alert" to display important messages.

detail-list

Definition list about a record on a detail screen. Mono marks for machine-ish values.

Four facts, one machine-ish

Preview

Put this inside <section rst-box>.

<dl rst-detail>
  <dt>Audience</dt>
  <dd>Members</dd>
  <dt>Main page</dt>
  <dd>No</dd>
  <dt>Published</dt>
  <dd>
    <time datetime="2026-08-02">2 August 2026</time>
  </dd>
  <dt>Reference</dt>
  <dd class="rst-mono">post_01H9ZQ</dd>
</dl>

Put this inside <section rst-box>.

{{template "detail-list" dict
    "Items" (list
        (dict "Label" "Audience" "Value" "Members")
        (dict "Label" "Main page" "Value" "No")
        (dict "DateTime" "2026-08-02" "Label" "Published" "Value" "2 August 2026")
        (dict "Label" "Reference" "Mono" true "Value" "post_01H9ZQ"))}}

notice

A one-line confirmation after a redirect. Takes a plain string, not a dict, and renders nothing for an empty one.

A saved message

Preview
<p rst-notice role="status">Post saved.</p>
{{template "notice" "Post saved."}}

region:notice

The notice every shell keeps on the page, showing the page data's Notice, so a screen reader hears a flash and live.js can fill it in place.

A saved message

Preview
<div id="notice" role="status">
  <p rst-notice>Post saved.</p>
</div>
{{template "region:notice" dict "Notice" "Post saved."}}

form-error

A form's summary error line, role="alert". Takes a plain string; renders nothing when there is no error.

A rejected submission

Preview
<p rst-form-error role="alert">That title is already taken.</p>
{{template "form-error" "That title is already taken."}}

job-status

A background job's polled fragment. The data-poll attribute is emitted only while the job runs: leaving it out is how polling stops.

Running

Preview

<div rst-job data-poll="/jobs/1/fragment" data-poll-every="2">
  <span rst-spin aria-hidden="true"></span> <bdi><strong>Import</strong></bdi> is running: <bdi>128 of 400</bdi>…
  <progress rst-job-bar value="32" min="0" max="100" aria-hidden="true"></progress>
</div>
{{template "job-status" dict
    "Name" "Import"
    "Percent" 32
    "PollSeconds" 2
    "PollURL" "/jobs/1/fragment"
    "Progress" "128 of 400"
    "Status" "running"}}

This one polls: it is the only sample on the page that asks the network for anything, and on a static site it simply never resolves.

Finished

Preview

<div rst-job>
  <bdi><strong>Import</strong></bdi> finished.
</div>
{{template "job-status" dict "Name" "Import" "Status" "done"}}

Failed

Preview

<div rst-job>
  <bdi><strong>Import</strong></bdi> failed: <bdi>row 91: no such column &#34;published&#34;</bdi>
</div>
{{template "job-status" dict
    "Err" "row 91: no such column \"published\""
    "Name" "Import"
    "Status" "failed"}}