Skip to content
Honor Society®Brand system

v0.6.4

Forms and controls

Form

Labels, controls, checks, errors, the form alert and the password field for every form a member fills in.

Block
.hs-form
CSS
components/form.css (in /brand.css)
JS
components/password.js: load it only on a page with this component
Status
Decided
Decided in
Components round 2b
Markdown
/components/form.md

What it is

Label 16px/600 ink. Control 40px, 16px ink, 1px slate-500 border (3.7:1), 4px radius. Focus is the shared gold outline and navy ring, and the border turns navy. Field order: label, control, hint, error. Mark the optional fields, not the required ones.

When to use

  • Any form a visitor or member fills in: login, registration, /member/induction, the profile edit dialogs.
  • A form that failed on submit: put the form alert at the top of the form.
  • A new password: the password field with password.js.
  • A password on a sign-in or account page: the same password field and eye toggle, without the meter and the rules.
  • Date parts or a city and ZIP pair on one line: .hs-fieldset__row inside a fieldset.

When not to use

  • To show a coupon or promotion code a member copies, use the Code box.
  • To filter one list by category, use Filter chips and tags.
  • To confirm an action that worked, use a Callout with role="status". The form alert is for errors only.
  • To open a short form over the page, put the form inside a Dialog.
  • For the routes and the help line around a form (“Forgot your password?”, “Need help signing in?”), use Form link, divider and support note.

Rules

  • Keep this order inside .hs-field: .hs-label, .hs-control, .hs-hint, .hs-field-error. In a .hs-fieldset the hint follows .hs-fieldset__options.
  • Never use a placeholder as the label. Every control keeps a visible <label for>. A placeholder may only show an example value: placeholder="3.75".
  • Mark the optional fields with <span class="hs-optional">(optional)</span> inside the label. Leave the required fields unmarked.
  • Keep a hint to one short line. An autofill list or the phone keyboard can cover a long hint.
  • Write each error as the fix: “Enter your GPA as a number from 0 to 5, for example 3.75.” Set aria-invalid="true" on the control and put the .hs-field-error under it.
  • After a failed submit, add the form alert (.hs-callout.hs-callout--danger.hs-form-alert) at the top, with one link per error to the field id.
  • Password field: .hs-field.hs-password holds .hs-password__input, the toggle (shipped hidden), .hs-meter and .hs-password__rules. Each rule is <li data-rule="length|letter|digit" data-min="N">; the rules are the site’s HsUser\PasswordRules. Load password.js.
  • Give every toggle aria-controls with the input id. The script reads it first.
  • Toggle: one pattern on every password field, the eye / eye-off icon button inside the field. Put both SVGs in the button, .hs-password__icon--show then .hs-password__icon--hide; CSS shows one by aria-pressed. Never write a text toggle (“Show” / “Hide”).
  • Leave the meter and the rules off a password the member types to prove who they are (autocomplete="current-password"). They belong to a new password only (autocomplete="new-password").

Accessibility

  • Give every control a <label for>, and every group a fieldset with a legend.hs-label. Link the hint and the error to the control with aria-describedby.
  • Contrast on white: label ink 13.0:1, hint slate 6.2:1, placeholder slate 6.2:1, error danger 5.4:1. The control border is slate 500 at 3.7:1 (WCAG 1.4.11 asks 3:1). Disabled text is exempt.
  • Focus is the shared gold outline and navy ring, and the border turns navy. Control text stays 16px, so iOS does not zoom on focus.
  • Checkboxes and radios are native, in navy through accent-color: the browser keeps the checked, indeterminate and forced-colours states. The 40px row and the whole label are the tap target.
  • The form alert takes role="alert" and tabindex="-1". Move focus to it after submit.
  • Password: the toggle is a button with aria-pressed and aria-controls; its aria-label flips between “Show password” and “Hide password”. Its hit area is 44px square, and it takes the shared focus style. The icon is slate 6.2:1, navy on hover; both SVGs are aria-hidden. Without password.js the toggle stays hidden, the field stays a plain password input, and the rules stay visible. The meter length repeats its colour, and a hidden aria-live label says “Strength: Fair”. Transitions stop under prefers-reduced-motion: reduce.

Variants

Field with a hint

The hint is 14px slate, under the control. Link it with aria-describedby.

Preview on white

Include the country code, for example +1 for the United States.

<div class="hs-field">
  <label class="hs-label" for="ph-b">Phone</label>
  <input class="hs-control" id="ph-b" type="tel" autocomplete="tel" aria-describedby="ph-b-hint">
  <p class="hs-hint" id="ph-b-hint">Include the country code, for example +1 for the United States.</p>
</div>

Input, select, textarea, error, disabled

aria-invalid="true" gives a 2px danger edge; .hs-field-error under the control says what to fix. --short 10rem, --medium 20rem.

Preview on white

Enter your GPA as a number from 0 to 5, for example 3.75.

A few sentences about your goals. Members and schools see this on your profile.

<form class="hs-form" action="#" novalidate>
  <div class="hs-field">
    <label class="hs-label" for="role">Role</label>
    <input class="hs-control" id="role" name="role" type="text" autocomplete="organization-title">
  </div>
  <div class="hs-field">
    <label class="hs-label" for="month">Start month <span class="hs-optional">(optional)</span></label>
    <select class="hs-control hs-control--medium" id="month" name="month">
      <option value="">Month</option>
      <option>September</option>
      <option>October</option>
    </select>
  </div>
  <div class="hs-field">
    <label class="hs-label" for="gpa">GPA</label>
    <input class="hs-control hs-control--short" id="gpa" name="gpa" type="text" inputmode="decimal" value="4.3" aria-invalid="true" aria-describedby="gpa-error">
    <p class="hs-field-error" id="gpa-error">Enter your GPA as a number from 0 to 5, for example 3.75.</p>
  </div>
  <div class="hs-field">
    <label class="hs-label" for="about">About <span class="hs-optional">(optional)</span></label>
    <textarea class="hs-control" id="about" name="about" aria-describedby="about-hint"></textarea>
    <p class="hs-hint" id="about-hint">A few sentences about your goals. Members and schools see this on your profile.</p>
  </div>
  <div class="hs-field">
    <label class="hs-label" for="email">Email</label>
    <input class="hs-control" id="email" type="email" value="avery.example@example.com" disabled>
  </div>
</form>

Checkbox, radio and fieldset

Native 20px box in navy. The row is 40px tall and the label is the tap target. In a fieldset the hint follows the options.

Preview on white

Educational or professional status

Check all that apply.

Preferred contact
<fieldset class="hs-fieldset" aria-describedby="status-hint">
  <legend class="hs-label">Educational or professional status</legend>
  <div class="hs-fieldset__options">
    <div class="hs-check">
      <input class="hs-check__input" id="st-ug" type="checkbox" name="status" value="undergraduate" checked>
      <label class="hs-check__label" for="st-ug">Undergraduate student</label>
    </div>
    <div class="hs-check">
      <input class="hs-check__input" id="st-gr" type="checkbox" name="status" value="graduate">
      <label class="hs-check__label" for="st-gr">Graduate student</label>
    </div>
    <div class="hs-check">
      <input class="hs-check__input" id="st-pro" type="checkbox" name="status" value="professional">
      <label class="hs-check__label" for="st-pro">Working professional</label>
    </div>
  </div>
  <p class="hs-hint" id="status-hint">Check all that apply.</p>
</fieldset>

<fieldset class="hs-fieldset">
  <legend class="hs-label">Preferred contact</legend>
  <div class="hs-fieldset__options">
    <div class="hs-check">
      <input class="hs-check__input" id="ct-email" type="radio" name="contact" value="email" checked>
      <label class="hs-check__label" for="ct-email">Email</label>
    </div>
    <div class="hs-check">
      <input class="hs-check__input" id="ct-phone" type="radio" name="contact" value="phone">
      <label class="hs-check__label" for="ct-phone">Phone</label>
    </div>
  </div>
</fieldset>

Form alert

A danger callout at the top of a form that failed, with one link per error. It takes role="alert" and focus after submit.

Preview on white

<div class="hs-callout hs-callout--danger hs-form-alert" role="alert" tabindex="-1">
  <svg class="hs-callout__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/><path d="M12 7.5v5.5"/><path d="M12 16.4v.1"/></svg>
  <div class="hs-callout__body">
    <p class="hs-callout__title">Fix 2 fields to save your academic record</p>
    <ul class="hs-form-alert__list">
      <li><a href="#gpa">Enter your GPA as a number from 0 to 5, for example 3.75.</a></li>
      <li><a href="#school">Enter your school.</a></li>
    </ul>
  </div>
</div>

Password field

Type in it. The strength meter is one brand bar: a third danger, two thirds warning, full success; a hidden live label says the word. The rules tick when met. Needs password.js.

Preview on white

  • 8+ characters
  • 1 letter
  • 1 number
<div class="hs-field hs-password">
  <label class="hs-label" for="new-pass">Password</label>
  <div class="hs-password__input">
    <input class="hs-control" id="new-pass" name="pass" type="password" autocomplete="new-password" aria-describedby="new-pass-rules">
    <button class="hs-password__toggle" type="button" aria-label="Show password" aria-pressed="false" aria-controls="new-pass" hidden>
      <svg class="hs-password__icon hs-password__icon--show" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M2 12s3.6-7 10-7 10 7 10 7-3.6 7-10 7S2 12 2 12z"/><circle cx="12" cy="12" r="3"/></svg><svg class="hs-password__icon hs-password__icon--hide" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M10.6 5.1A10.4 10.4 0 0 1 12 5c6.4 0 10 7 10 7a17.6 17.6 0 0 1-2.6 3.5M6.6 6.6C3.7 8.5 2 12 2 12s3.6 7 10 7a9.9 9.9 0 0 0 5.4-1.6"/><path d="M9.9 9.9a3 3 0 0 0 4.2 4.2"/><path d="M3 3l18 18"/></svg>
    </button>
  </div>
  <div class="hs-meter">
    <span class="hs-meter__fill"></span>
    <span class="hs-meter__label hs-visually-hidden" aria-live="polite"></span>
  </div>
  <ul class="hs-password__rules" id="new-pass-rules">
    <li class="hs-password__rule" data-rule="length" data-min="8">8+ characters</li>
    <li class="hs-password__rule" data-rule="letter" data-min="1">1 letter</li>
    <li class="hs-password__rule" data-rule="digit" data-min="1">1 number</li>
  </ul>
</div>

Password field, sign-in

A password the member already has: the eye toggle, no meter, no rules. Without password.js the toggle stays hidden and the field works as a plain password input. Decided in round 4: one toggle pattern on every password field.

Preview on white

<div class="hs-field hs-password">
  <label class="hs-label" for="signin-pass">Password</label>
  <div class="hs-password__input">
    <input class="hs-control" id="signin-pass" name="pass" type="password" autocomplete="current-password">
    <button class="hs-password__toggle" type="button" aria-label="Show password" aria-pressed="false" aria-controls="signin-pass" hidden>
      <svg class="hs-password__icon hs-password__icon--show" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M2 12s3.6-7 10-7 10 7 10 7-3.6 7-10 7S2 12 2 12z"/><circle cx="12" cy="12" r="3"/></svg><svg class="hs-password__icon hs-password__icon--hide" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M10.6 5.1A10.4 10.4 0 0 1 12 5c6.4 0 10 7 10 7a17.6 17.6 0 0 1-2.6 3.5M6.6 6.6C3.7 8.5 2 12 2 12s3.6 7 10 7a9.9 9.9 0 0 0 5.4-1.6"/><path d="M9.9 9.9a3 3 0 0 0 4.2 4.2"/><path d="M3 3l18 18"/></svg>
    </button>
  </div>
</div>