Skip to content
Sections
Language, currently English
Rastrillo Design System

Choices and actions

The controls that pick from options, and the rows that end a form. Each one sits in a padded rst-box, because that is the rule these partials assume.

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

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

Put this inside <section rst-box><form rst-form>.

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

Put this inside <section rst-box><form rst-form>.

{{template "field-select" dict
    "ID" "sel_few"
    "Name" "sel_few"
    "Label" "Role"
    "Options" (list
        (dict "Label" "Admin" "Selected" true "Value" "admin")
        (dict "Label" "Editor" "Value" "editor")
        (dict "Label" "Member" "Value" "member"))}}

Twelve options: the filterable combobox

Preview

Put this inside <section rst-box><form rst-form>.

<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" data-rst-select-no-matches="No matches" 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>

Put this inside <section rst-box><form rst-form>.

{{template "field-select" dict
    "ID" "sel_many"
    "Name" "sel_many"
    "Label" "City"
    "Options" (list
        (dict "Label" "Dublin" "Selected" true "Value" "dublin")
        (dict "Label" "Lisbon" "Value" "lisbon")
        (dict "Label" "Madrid" "Value" "madrid")
        (dict "Label" "Berlin" "Value" "berlin")
        (dict "Label" "Rome" "Value" "rome")
        (dict "Label" "Athens" "Value" "athens")
        (dict "Label" "Helsinki" "Value" "helsinki")
        (dict "Label" "Reykjavík" "Value" "reykjavík")
        (dict "Label" "Warsaw" "Value" "warsaw")
        (dict "Label" "Zagreb" "Value" "zagreb")
        (dict "Label" "Tallinn" "Value" "tallinn")
        (dict "Label" "Valletta" "Value" "valletta"))
    "Help" "Start typing to filter."}}

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

Put this inside <section rst-box><form rst-form>.

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

Put this inside <section rst-box><form rst-form>.

{{template "field-select" dict
    "ID" "sel_plain"
    "Name" "sel_plain"
    "Label" "City"
    "Options" (list
        (dict "Label" "Dublin" "Selected" true "Value" "dublin")
        (dict "Label" "Lisbon" "Value" "lisbon")
        (dict "Label" "Madrid" "Value" "madrid")
        (dict "Label" "Berlin" "Value" "berlin")
        (dict "Label" "Rome" "Value" "rome")
        (dict "Label" "Athens" "Value" "athens")
        (dict "Label" "Helsinki" "Value" "helsinki")
        (dict "Label" "Reykjavík" "Value" "reykjavík")
        (dict "Label" "Warsaw" "Value" "warsaw")
        (dict "Label" "Zagreb" "Value" "zagreb")
        (dict "Label" "Tallinn" "Value" "tallinn")
        (dict "Label" "Valletta" "Value" "valletta"))
    "Plain" true}}

Required, with an error

Preview

Put this inside <section rst-box><form rst-form>.

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

Put this inside <section rst-box><form rst-form>.

{{template "field-select" dict
    "ID" "sel_error"
    "Name" "sel_error"
    "Label" "Role"
    "Options" (list
        (dict "Label" "Admin" "Selected" true "Value" "admin")
        (dict "Label" "Editor" "Value" "editor")
        (dict "Label" "Member" "Value" "member"))
    "Required" true
    "Error" "Pick a role for this person."}}

Hand-written, with optgroups

Preview

Put this inside <section rst-box><form rst-form>.

<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" data-rst-select-no-matches="No matches">
    <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>

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

Put this inside <section rst-box><form rst-form>.

<label rst-switch>
  <input type="checkbox" name="notify_on" checked>
  <span rst-switch-track aria-hidden="true"></span>
  Email me about replies
</label>

Put this inside <section rst-box><form rst-form>.

{{template "field-check" dict
    "Name" "notify_on"
    "Label" "Email me about replies"
    "Checked" true}}

Off

Preview

Put this inside <section rst-box><form rst-form>.

<label rst-switch>
  <input type="checkbox" name="notify_off">
  <span rst-switch-track aria-hidden="true"></span>
  Email me about mentions
</label>

Put this inside <section rst-box><form rst-form>.

{{template "field-check" dict
    "Name" "notify_off"
    "Label" "Email me about mentions"}}

Disabled

Preview

Put this inside <section rst-box><form rst-form>.

<label rst-switch>
  <input type="checkbox" name="notify_disabled" disabled>
  <span rst-switch-track aria-hidden="true"></span>
  Email me a weekly digest
</label>

Put this inside <section rst-box><form rst-form>.

{{template "field-check" dict
    "Name" "notify_disabled"
    "Label" "Email me a weekly digest"
    "Disabled" true}}

choice-field

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

Pick one

Preview

Put this inside <section rst-box><form rst-form>.

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

Put this inside <section rst-box><form rst-form>.

{{template "choice-field" dict
    "Legend" "Plan"
    "Name" "plan"
    "Options" (list
        (dict "Desc" "One project, community support." "Title" "Free" "Value" "free")
        (dict "Checked" true "Desc" "Unlimited projects and a support inbox." "Title" "Pro" "Value" "pro"))}}

Pick any

Preview

Put this inside <section rst-box><form rst-form>.

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

Put this inside <section rst-box><form rst-form>.

{{template "choice-field" dict
    "Legend" "Send me"
    "Name" "digest"
    "Type" "checkbox"
    "Options" (list
        (dict "Checked" true "Desc" "As they happen." "Title" "Replies" "Value" "replies")
        (dict "Desc" "Monday mornings." "Title" "Weekly digest" "Value" "weekly"))}}

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

Put this inside <section rst-box><form rst-form>.

<div rst-form-foot>
  <button rst-btn="primary lg" type="submit">Save post</button>
  <a rst-btn="lg" href="/posts">Back to posts</a>
</div>

Put this inside <section rst-box><form rst-form>.

{{template "form-foot" dict
    "Submit" "Save post"
    "CancelHref" "/posts"
    "CancelLabel" "Back to posts"}}

Submit only

Preview

Put this inside <section rst-box><form rst-form>.

<div rst-form-foot>
  <button rst-btn="primary lg" type="submit">Send invitation</button>
</div>

Put this inside <section rst-box><form rst-form>.

{{template "form-foot" dict "Submit" "Send invitation"}}

Idle: the button before anything happens

Preview

Put this inside <section rst-box><form rst-form>.

<div rst-form-foot>
  <button rst-btn="primary lg" type="submit">Publish</button>
  <a rst-btn="lg" href="/posts">Back to posts</a>
</div>

Put this inside <section rst-box><form rst-form>.

{{template "form-foot" dict
    "Submit" "Publish"
    "CancelHref" "/posts"
    "CancelLabel" "Back to posts"}}

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

Put this inside <section rst-box><form rst-form>.

<div rst-form-foot>
  <button rst-btn="primary lg" type="submit" aria-busy="true" disabled>
    <span rst-spin aria-hidden="true"></span>
    Publish
  </button>
  <a rst-btn="lg" href="/posts">Back to posts</a>
</div>

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

Put this inside <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 lg" href="/posts/1">Cancel</a>
  <button type="submit" rst-btn="danger lg">Delete this post</button>
</form>

Put this inside <section rst-box>.

{{template "confirm-form" dict
    "Action" "/posts/1/delete"
    "Label" "Delete this post"
    "Danger" true
    "Hidden" (list (list "csrf" "tok-123") (list "id" "1"))
    "CancelHref" "/posts/1"}}

Ordinary, default cancel wording

Preview

Put this inside <section rst-box>.

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

Put this inside <section rst-box>.

{{template "confirm-form" dict
    "Action" "/posts/1/publish"
    "Label" "Publish"
    "CancelHref" "/posts/1"}}

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