Skip to content
Menu
Language, currently English
EnglishGaeilge简体中文Españolहिन्दीPortuguêsবাংলাРусский日本語廣東話Tiếng Việtالعربية

rastrillo design system

An overview of everything the design system provides. Theme: day. Language: English.

Form

Every control, in the states a real form puts it in. Each one sits in a padded rst-box, because that is the rule these partials assume.

Pre-built, consistent UI elements, rendered server-side.

The framework's own vocabulary calls these partials: ui.Templates() returns partials, and docs/site/templates.md documents them under that name. The word on this page changed; the code's did not.

Links here are inactive

Links inactive, sample source provided.

Each sample below in its own frame.

Sample content in English. Sample shells translated.

field

Label, input, optional hint, help and error. The general text-like control, with an explicit ID.

Bare

Preview
<section rst-box><form rst-form method="post" action="#"><div rst-field><label rst-field-label for="f_bare">Title</label>
<input rst-input type="text" id="f_bare" name="f_bare">
</div></form></section>

With a hint

Preview
<section rst-box><form rst-form method="post" action="#"><div rst-field><label rst-field-label for="f_hint">Slug <span rst-field-hint>(lowercase, no spaces)</span></label>
<input rst-input type="text" id="f_hint" name="f_hint">
</div></form></section>

With help under the control

Preview
<section rst-box><form rst-form method="post" action="#"><div rst-field><label rst-field-label for="f_help">Email</label>
<input rst-input type="email" id="f_help" name="f_help" autocomplete="email" aria-describedby="f_help-help">
<p rst-field-help id="f_help-help">We only use this to send you replies.</p>
</div></form></section>

Required

Preview
<section rst-box><form rst-form method="post" action="#"><div rst-field><label rst-field-label for="f_required">Full name</label>
<input rst-input type="text" id="f_required" name="f_required" required>
</div></form></section>

With an error

Preview
<section rst-box><form rst-form method="post" action="#"><div rst-field><label rst-field-label for="f_error">Email</label>
<input rst-input type="email" id="f_error" name="f_error" value="grace@" aria-invalid="true" aria-describedby="f_error-error">
<p rst-field-error id="f_error-error" role="alert">That does not look like an email address.</p>
</div></form></section>

Short

Preview
<section rst-box><form rst-form method="post" action="#"><div rst-field><label rst-field-label for="f_short">Postcode</label>
<input rst-input="short" type="text" id="f_short" name="f_short" value="D02 XY45">
</div></form></section>

The headline field

Preview
<section rst-box><form rst-form method="post" action="#"><div rst-field><label rst-field-label for="f_primary">Title</label>
<input rst-input="primary" type="text" id="f_primary" name="f_primary" value="Quarterly board meeting">
</div></form></section>

field-text

One labelled text input, ids derived from Name. The required marker's star is aria-hidden. The input's required attribute is the programmatic signal.

Bare

Preview
<section rst-box><form rst-form method="post" action="#"><div rst-field>
  <label rst-field-label for="ft_bare">Title</label>
  <input rst-input id="ft_bare" name="ft_bare" type="text">
</div></form></section>

With a hint

Preview
<section rst-box><form rst-form method="post" action="#"><div rst-field>
  <label rst-field-label for="ft_hint">Headline</label>
  <input rst-input id="ft_hint" name="ft_hint" type="text" aria-describedby="ft_hint-hint">
  <small rst-field-hint id="ft_hint-hint">Shown in the list.</small>
</div></form></section>

Required

Preview
<section rst-box><form rst-form method="post" action="#"><div rst-field>
  <label rst-field-label for="ft_required">Display name <span rst-field-required aria-hidden="true">*</span></label>
  <input rst-input id="ft_required" name="ft_required" type="text" autocomplete="nickname" required>
</div></form></section>

With an error

Preview
<section rst-box><form rst-form method="post" action="#"><div rst-field>
  <label rst-field-label for="ft_error">Title</label>
  <input rst-input id="ft_error" name="ft_error" type="text" value="Release notes" aria-invalid="true" aria-describedby="ft_error-error">
  <small rst-field-error id="ft_error-error">A post with that title already exists.</small>
</div></form></section>

The headline field

Preview
<section rst-box><form rst-form method="post" action="#"><div rst-field>
  <label rst-field-label for="ft_primary">What is it?</label>
  <input rst-input="primary" id="ft_primary" name="ft_primary" type="text" value="Quarterly board meeting">
</div></form></section>

Add Primary to the main form field, e.g. a title or a name to get a more visually prominent input.

field-textarea

field-text's wrapper around a textarea.

Bare

Preview
<section rst-box><form rst-form method="post" action="#"><div rst-field>
  <label rst-field-label for="ta_bare">Notes</label>
  <textarea rst-textarea id="ta_bare" name="ta_bare" rows="3"></textarea>
</div></form></section>

With a hint

Preview
<section rst-box><form rst-form method="post" action="#"><div rst-field>
  <label rst-field-label for="ta_hint">Bio</label>
  <textarea rst-textarea id="ta_hint" name="ta_hint" rows="3" aria-describedby="ta_hint-hint"></textarea>
  <small rst-field-hint id="ta_hint-hint">Shown on your profile.</small>
</div></form></section>

With an error

Preview
<section rst-box><form rst-form method="post" action="#"><div rst-field>
  <label rst-field-label for="ta_error">Summary</label>
  <textarea rst-textarea id="ta_error" name="ta_error" rows="3" aria-invalid="true" aria-describedby="ta_error-error">…</textarea>
  <small rst-field-error id="ta_error-error">A summary needs at least twenty characters.</small>
</div></form></section>

Required

Preview
<section rst-box><form rst-form method="post" action="#"><div rst-field>
  <label rst-field-label for="ta_required">Reason for refund <span rst-field-required aria-hidden="true">*</span></label>
  <textarea rst-textarea id="ta_required" name="ta_required" rows="3" required></textarea>
</div></form></section>

field-select

field's envelope around a native select. Past ten options the select arms the filterable combobox in select.js; the native select is still what the form submits.

Three options: a native select

Preview
<section rst-box><form rst-form method="post" action="#"><div rst-field><label rst-field-label for="sel_few">Role</label>
<select rst-input id="sel_few" name="sel_few"><option value="Admin" selected>Admin</option><option value="Editor">Editor</option><option value="Member">Member</option></select>
</div></form></section>

Twelve options: the filterable combobox

Preview
<section rst-box><form rst-form method="post" action="#"><div rst-field><label rst-field-label for="sel_many">City</label>
<select rst-input id="sel_many" name="sel_many" data-rst-select data-rst-select-filter="Type to filter" data-rst-select-results="{n} results" data-rst-select-result-one="1 result" aria-describedby="sel_many-help"><option value="Dublin" selected>Dublin</option><option value="Lisbon">Lisbon</option><option value="Madrid">Madrid</option><option value="Berlin">Berlin</option><option value="Rome">Rome</option><option value="Athens">Athens</option><option value="Helsinki">Helsinki</option><option value="Reykjavík">Reykjavík</option><option value="Warsaw">Warsaw</option><option value="Zagreb">Zagreb</option><option value="Tallinn">Tallinn</option><option value="Valletta">Valletta</option></select>
<p rst-field-help id="sel_many-help">Start typing to filter.</p>
</div></form></section>

Past ten options the partial arms select.js. Below ten it does not: search over a handful of items is furniture, not help.

Twelve options, Plain — the enhancement opted out

Preview
<section rst-box><form rst-form method="post" action="#"><div rst-field><label rst-field-label for="sel_plain">City</label>
<select rst-input id="sel_plain" name="sel_plain"><option value="Dublin" selected>Dublin</option><option value="Lisbon">Lisbon</option><option value="Madrid">Madrid</option><option value="Berlin">Berlin</option><option value="Rome">Rome</option><option value="Athens">Athens</option><option value="Helsinki">Helsinki</option><option value="Reykjavík">Reykjavík</option><option value="Warsaw">Warsaw</option><option value="Zagreb">Zagreb</option><option value="Tallinn">Tallinn</option><option value="Valletta">Valletta</option></select>
</div></form></section>

Required, with an error

Preview
<section rst-box><form rst-form method="post" action="#"><div rst-field><label rst-field-label for="sel_error">Role</label>
<select rst-input id="sel_error" name="sel_error" required aria-invalid="true" aria-describedby="sel_error-error"><option value="Admin" selected>Admin</option><option value="Editor">Editor</option><option value="Member">Member</option></select>
<p rst-field-error id="sel_error-error" role="alert">Pick a role for this person.</p>
</div></form></section>

Hand-written, with optgroups

Preview
<section rst-box><form rst-form method="post" action="#"><div rst-field><label rst-field-label for="sel_grouped">Region</label>
<select rst-input id="sel_grouped" name="sel_grouped" data-rst-select data-rst-select-filter="Type to filter" data-rst-select-results="{n} results" data-rst-select-result-one="1 result">
<optgroup label="Europe"><option value="dublin" selected>Dublin</option><option value="lisbon">Lisbon</option><option value="warsaw">Warsaw</option></optgroup>
<optgroup label="Americas"><option value="montreal">Montréal</option><option value="lima">Lima</option><option value="austin">Austin</option></optgroup>
<optgroup label="Asia"><option value="osaka">Osaka</option><option value="hanoi">Hanoi</option><option value="dhaka">Dhaka</option></optgroup>
<optgroup label="Africa"><option value="accra">Accra</option><option value="nairobi">Nairobi</option><option value="tunis">Tunis</option></optgroup>
</select>
</div></form></section>

Options here is a flat list, so grouped choices are markup the app writes. select.js renders the groups rather than dropping them; a hand-written select can also refuse the enhancement outright with data-rst-select="false".

field-check

The one toggle mechanism: a real checkbox skinned as a switch, keyboard and screen-reader intact.

On

Preview
<section rst-box><form rst-form method="post" action="#"><label rst-switch><input type="checkbox" name="notify_on" checked><span rst-switch-track aria-hidden="true"></span> Email me about replies</label></form></section>

Off

Preview
<section rst-box><form rst-form method="post" action="#"><label rst-switch><input type="checkbox" name="notify_off"><span rst-switch-track aria-hidden="true"></span> Email me about mentions</label></form></section>

Disabled

Preview
<section rst-box><form rst-form method="post" action="#"><label rst-switch><input type="checkbox" name="notify_disabled" disabled><span rst-switch-track aria-hidden="true"></span> Email me a weekly digest</label></form></section>

choice-field

Option cards — the preferred radio or checkbox group. Title stays short; Desc explains.

Pick one

Preview
<section rst-box><form rst-form method="post" action="#"><fieldset rst-choice><legend>Plan</legend><div rst-choice-cards><label><input type="radio" name="plan" value="free"><span><span rst-choice-title>Free</span><span rst-choice-desc>One project, community support.</span></span></label><label><input type="radio" name="plan" value="pro" checked><span><span rst-choice-title>Pro</span><span rst-choice-desc>Unlimited projects and a support inbox.</span></span></label></div></fieldset></form></section>

Pick any

Preview
<section rst-box><form rst-form method="post" action="#"><fieldset rst-choice><legend>Send me</legend><div rst-choice-cards><label><input type="checkbox" name="digest" value="replies" checked><span><span rst-choice-title>Replies</span><span rst-choice-desc>As they happen.</span></span></label><label><input type="checkbox" name="digest" value="weekly"><span><span rst-choice-title>Weekly digest</span><span rst-choice-desc>Monday mornings.</span></span></label></div></fieldset></form></section>

form-foot

A form's closing row: one primary submit and an optional cancel link. Cancel is an anchor, because leaving a form is navigation.

Save and cancel

Preview
<section rst-box><form rst-form method="post" action="#"><div rst-form-foot>
  <button rst-btn="primary" type="submit">Save post</button>
  <a rst-btn href="/posts">Back to posts</a>
</div></form></section>

Submit only

Preview
<section rst-box><form rst-form method="post" action="#"><div rst-form-foot>
  <button rst-btn="primary" type="submit">Send invitation</button>
</div></form></section>

Idle — the button before anything happens

Preview
<section rst-box><form rst-form method="post" action="#"><div rst-form-foot>
  <button rst-btn="primary" type="submit">Publish</button>
  <a rst-btn href="/posts">Back to posts</a>
</div></form></section>

A button that CHANGES something gets a loading state; a button that only reveals something, e.g. a disclosure, a dropdown, a tab, does not. rastrillo.js applies that rule to every submit button in every form, with nothing to opt into.

Working — what rastrillo.js writes on the way out

Preview
<section rst-box><form rst-form method="post" action="#"><div rst-form-foot>
<button rst-btn="primary" type="submit" aria-busy="true" data-idle-label="Publish" disabled><span rst-spin aria-hidden="true"></span>Publishing…</button>
<a rst-btn href="/posts">Back to posts</a>
</div></form></section>

Only the button that was clicked: every other submit button in the form keeps its name and its value, and the form itself is guarded against a second submit. data-busy="false" opts out, on the form or on one button; data-busy-label replaces the text. With scripts off none of this happens and the form submits exactly as it always did, so idempotency stays the server's job.

confirm-form

The confirm page's action row. Destructive actions get their own route rather than a dialog, so there is nothing to script.

Destructive

Preview
<section rst-box><form rst-form-actions method="post" action="/posts/1/delete"><input type="hidden" name="csrf" value="tok-123"><input type="hidden" name="id" value="1"><a rst-btn="ghost" href="/posts/1">Cancel</a><button type="submit" rst-btn="danger">Delete this post</button></form></section>

Ordinary, default cancel wording

Preview
<section rst-box><form rst-form-actions method="post" action="/posts/1/publish"><a rst-btn="ghost" href="/posts/1">Cancel</a><button type="submit" rst-btn="primary">Publish</button></form></section>

No CancelLabel, so the cancel link is worded from the catalog.