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__rowinside 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-fieldsetthe 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-errorunder 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 fieldid. - Password field:
.hs-field.hs-passwordholds.hs-password__input, the toggle (shippedhidden),.hs-meterand.hs-password__rules. Each rule is<li data-rule="length|letter|digit" data-min="N">; the rules are the site’sHsUser\PasswordRules. Loadpassword.js. - Give every toggle
aria-controlswith the inputid. 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--showthen.hs-password__icon--hide; CSS shows one byaria-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 afieldsetwith alegend.hs-label. Link the hint and the error to the control witharia-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"andtabindex="-1". Move focus to it after submit. - Password: the toggle is a button with
aria-pressedandaria-controls; itsaria-labelflips 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 arearia-hidden. Withoutpassword.jsthe toggle stays hidden, the field stays a plain password input, and the rules stay visible. The meter length repeats its colour, and a hiddenaria-livelabel says “Strength: Fair”. Transitions stop underprefers-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
<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
<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
Fix 2 fields to save your academic record
<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>