Forms and inputs

Field anatomy, text and select inputs, checkboxes and toggles, the date and time pickers, faceted search, validation, and form layout.

Saga forms are quiet: a label above, a bordered white box, a blue focus, clay for errors. Native controls are welcome where they fit: a short plain <select> or a single <input type="date"> is accessible by default and handles keyboard, touch and viewport edges for free. Reach for the painted .saga-select listbox and .saga-datepicker when the control needs search, groups, annotations, a range, or one look across browsers (see "Native or painted?" under Select). One caveat to weigh: a native popup paints outside the tab, so it is missing from a tab share.

Field anatomy#

<div class="saga-field">
  <label class="saga-label" for="title">Session Title</label>
  <input class="saga-input" id="title" placeholder="Out of window test">
  <p class="saga-help">Shown to tutors on their schedule.</p>
</div>

A field-level annotation that is computed rather than typed (the "DURATION 30m" readout next to the time pair) is a tiny uppercase label in --text-xs --color-text-secondary with the value in --weight-semibold beneath it.

Text input#

Token Value
--input-height 2.4rem
--input-padding 0.3rem 0.75rem
--input-radius --radius-md (6px)
--input-bg white
--input-border-color --color-border-strong (#d9d9d9), 1px
--input-border-color-hover --color-gray (#9f9f9f)
--input-border-color-focus --color-primary, plus the global focus ring
--input-border-color-invalid --color-danger
--input-color-placeholder --saga-beige-dark (#a59c94), italic
text inherits (--text-base), --color-text; .saga-input--sm drops to --text-sm at 80% height

Placeholder text is italic and beige, so an empty field and a filled field are different at a glance even in a screenshot. Placeholders describe the format ("Search whole name, Student ID, school"), not the label.

.saga-textarea is the same box, minimum three lines, resizable vertically.

Select#

Both kinds of select share one box: .saga-select wraps either a native <select> or a painted trigger, and both get the input border with a blue caret at the right (DS-101, DS-124). The painted select is a trigger that looks like an input and opens a painted listbox; it is the one to use when the list needs search, groups, multi-select or rich rows:

Keyboard: arrow keys move, Enter picks, Esc closes, typing filters. Multi-select rows carry a checkbox; single-select rows carry no indicator, the highlight is the mark.

<div class="saga-field">
  <label class="saga-label" id="ratio-label">Tutor:Student Ratio</label>
  <div class="saga-select">
    <button type="button" class="saga-select__trigger" aria-haspopup="listbox" aria-expanded="false" aria-labelledby="ratio-label"><span>1:3</span></button>
    <ul class="saga-select__menu" role="listbox" aria-labelledby="ratio-label" hidden>
      <li class="saga-select__option" role="option" aria-selected="false">1:1</li>
      <li class="saga-select__option" role="option" aria-selected="true">1:3</li>
      <li class="saga-select__option" role="option" aria-selected="false">1:4</li>
    </ul>
  </div>
</div>

Native or painted? A native <select> wrapped in .saga-select takes the same box and caret (appearance: none) and is the right control when the shoe fits (DS-124):

<div class="saga-select"><select aria-label="Ratio"><option>1:1</option><option selected>1:3</option></select></div>

Checkbox, radio, toggle#

All three are the native input drawn with appearance: none, sized by the spacing tokens, wrapped in a label.saga-check that puts the text --space-sm to the right (DS-116).

<label class="saga-check"><input type="checkbox" class="saga-checkbox" checked> Email me a copy</label>
<label class="saga-check"><input type="radio" name="pattern" class="saga-radio" checked> Same Every Week</label>
<label class="saga-check"><input type="checkbox" class="saga-toggle" role="switch"> Show Entire District</label>

Large choices become option cards (Same Every Week / Varies by Day Type; the four Rotation Pattern tiles): a .saga-card with the radio or an icon in the top-left, a --text-base semibold title, and a --text-sm secondary description. The selected card gets a 2px --color-primary border and a --color-primary-light fill. Use option cards when each choice needs a sentence of explanation; use plain radios when the labels are enough.

Date picker#

A single plain date in a form is a native <input type="date"> carrying the .saga-input class; it is accessible, localized and touch-friendly for free (DS-125). Use .saga-datepicker when the day grid needs annotations (sessions on a day, disabled days), a range, quick options, or the picker must look the same in every browser. It is a .saga-datepicker__trigger that looks like an input with a calendar glyph in --color-text-secondary at the right ("10/12/2026"), sized to its content rather than the column. Clicking opens a .saga-datepicker__popover (date_picker.png):

<div class="saga-datepicker">
  <button type="button" class="saga-datepicker__trigger" aria-haspopup="dialog" aria-expanded="true" aria-label="When: 10/12/2026">
    <span>10/12/2026</span><iconify-icon icon="material-symbols-light:calendar-today-outline-rounded"></iconify-icon>
  </button>
  <div class="saga-datepicker__popover" role="dialog" aria-label="Choose a date">
    <div class="saga-datepicker__nav">
      <button class="saga-btn saga-btn--quiet saga-btn--icon saga-btn--sm" aria-label="Previous month"><iconify-icon icon="material-symbols-light:chevron-left-rounded"></iconify-icon></button>
      <span class="saga-datepicker__month">October 2026</span>
      <button class="saga-btn saga-btn--quiet saga-btn--icon saga-btn--sm" aria-label="Next month"><iconify-icon icon="material-symbols-light:chevron-right-rounded"></iconify-icon></button>
    </div>
    <div class="saga-datepicker__grid" role="grid">
      <span class="saga-datepicker__weekday">Su</span> … <span class="saga-datepicker__weekday">Sa</span>
      <button class="saga-datepicker__day saga-datepicker__day--muted">27</button> …
      <button class="saga-datepicker__day" aria-selected="true">12</button> …
    </div>
  </div>
</div>

In the painted picker, dates are entered by picking; the native date input also accepts typing, which is fine for a plain single date.

Time picker#

The time field is a short text input (6em) that accepts typing and also opens a list on focus (time_picker.png):

A time range is two of these with an en dash between them and the DURATION readout to the right.

Date range with quick options#

A reporting range ("Oct 9 – Oct 13, 2026") is a clickable blue label, not a bordered box. It opens a two-column popover: a list of quick options on the left (This week, Last week, Last 30 days, Custom) and a month grid on the right. Picking a quick option applies and closes; clicking two days in the grid sets start and end and applies live. The checked quick option is derived from the applied range, so "Custom" lights up whenever the dates do not match a preset.

A search box that grows chips (dashboard_rosters_page.png top right, the sessions search in calendar_view.png):

Validation and errors#

Required, read-only, disabled#

Layout#

Do and don't#

Do

Don't