RamosLabs DS

Patterns / Form Elements

Every screen persuades. The form is where they act.

It is also the single easiest place to lose the people the rest of the product worked to earn. A hidden label, the wrong keyboard, an error that only says "invalid": each is a small tax, and forms are where small taxes compound into abandonment. This page is the last word on how we build them. The spine is one rule: reach for the native control first, tune it with the right attributes, and build custom only where the platform genuinely falls short.

Part 1Doctrine, the rules under every field
1

Native-first, and doubly so on mobile

The default is always the plain HTML control with the right type, inputmode and autocomplete. On a phone this is not a preference, it is leverage: a semantic <input type> raises the correct on-screen keyboard, opens the platform picker, and arrives with keyboard support, focus handling and screen-reader semantics already built in. MDN is blunt about it: prefer the semantic input type over hacks, for better accessibility and built-in validation.

Go custom only when one of two things is true: the pattern does not exist in HTML (a real multi-select, a filtering combobox, tags, rating), or the brand must style something the platform will not let you style (the popup of a <select>, the calendar of a date picker). When you do, prefer desktop, keep a native control underneath as the progressive-enhancement base, and follow the matching WAI-ARIA APG pattern to the letter. On mobile, bias even harder toward native: the OS picker is already accessible and familiar.

Native controls are hard to style, and that is the whole tension. web.dev says it plainly: today's HTML form elements are difficult to customize. You can style the field box (border, radius, padding, type) of almost anything. What you cannot reliably restyle cross-browser is the <select> arrow and popup, and the date and time picker widgets. That single limit, not taste, is what pushes a control from native to custom.

The clearest case is a date. GOV.UK research is the reference here: for a date the user already knows or can look up without a calendar, such as a birthday or an issue date, do not open a calendar at all. Use three plain text inputs, grouped in a <fieldset> with a <legend>, each with inputmode="numeric". Reserve the native calendar picker for dates the user picks by looking, like choosing a future session to attend.

✓ Recommended: a memorable date, as three fields
Date of birthFor example, 27 3 1998.
✕ Avoid: a calendar for a birthday
A picker that opens on the current month forces the user to page back through decades to reach a year they could have simply typed.
2

Theming the accent without giving up native

You can brand native controls without rebuilding them. Three CSS properties carry it, and each one has a precise reach. Set them once at the root and every native checkbox, radio, range and text caret in the product turns indigo, and every control follows the OS light or dark theme, with zero per-control work.

:root { color-scheme: light dark; /* native controls follow the OS theme */ accent-color: var(--color-primary); /* tints checkbox, radio, range, progress */ caret-color: var(--color-primary); /* the text insertion cursor */ } <!-- in <head>, prevents a theme flash on load --> <meta name="color-scheme" content="light dark">

Read the coverage exactly. accent-color tints four controls and nothing else. It does not touch <select>, the date and time inputs, type="color", type="file", or any text field. Getting this wrong is the usual reason a "themed" form still shows a stock blue selected option.

PropertyWhat it doesReachesDoes not reach
accent-colorTints the control's accent to the brand, keeping native behaviourcheckbox, radio, range, progressselect, date, time, color, file, text
caret-colorColours the text insertion cursortext, search, tel, email, password, textareadate, color, checkbox, radio
color-schemeNative controls adopt the OS dark or light look, no extra CSSall native controls, scrollbarsyour own custom colours

Here it is live. Every control below is a stock browser control tinted with accent-color: var(--color-primary). No custom markup, no JavaScript. Toggle your OS theme and they follow it, while the indigo accent stays put in both.

accent-color, checkbox
accent-color, radio
accent-color, rangeprogress65%
caret-color, text

Trust the browser on accent contrast. Provide the brand colour and the browser picks a legible checkmark or thumb colour against it for you. For the controls accent-color cannot reach, take the decision ladder: accept the native chrome (best on mobile), style only the field box and keep the native popup, or, only if the brand demands it, rebuild with the matching APG pattern on desktop with a native control underneath.

3

The anatomy of a field

A field is four parts, and dropping any one shifts the work onto the user. The label asks and stays visible. The control takes the answer. Help pre-empts the mistake. The error appears only when something is wrong, and says exactly how to fix it.

Label
Control
HelpWe send your tickets and receipts here. Never shared.
Error Enter an email like name@domain.com so we can reach you.

The label stays visible, above the field, never inside it. A placeholder is not a label: it vanishes the instant someone types, exactly when they want to re-read the question, and screen readers treat it as unreliable. A single top-aligned column is also the fastest form to scan, because the eye runs straight down from question to answer.

Marking the required field is not optional decoration. The * is drawn with aria-hidden, and the word "required" rides along in an sr-only span so a screen reader announces it in full. Pair the real required attribute with that text, and state the convention once at the top of the form.

Mark the rarer one. If most fields are required, mark the few optional ones in words; if most are optional, mark the required ones. An asterisk alone is not accessible, so pair it with text or an aria-label and state the convention once.

✓ Recommended: label above, help visible
Shown on the public page and on every ticket.
✕ Avoid: placeholder as the label
The question disappears the instant the user types, with nowhere left to put help.
4

Validate at the right moment, in the right words

Start from HTML constraint validation, required, type, pattern, min, max, step. It is free, works without JavaScript, and is the source of truth. Present its state with the newer :user-invalid and :user-valid pseudo-classes, not :invalid and :valid. The user variants only match after the person has interacted or tried to submit, which is exactly what stops a pristine form from lighting up red on load.

/* matches only after interaction, never on first paint */ input:user-invalid { border-color: var(--color-error); } input:user-valid { border-color: var(--color-success-strong); }

On timing, the rule is reward early, punish late. Confirm a field as valid while the user types, but hold a new error until they leave it. Flagging the first letter of an email as a broken email is technically true and deeply unhelpful.

The rule has a second half that starts the moment a field has already shown an error. From then on, switch that field to re-validate on every keystroke and clear the error the instant the value becomes valid. Once someone is fixing a known problem, the reassurance that they succeeded should arrive immediately, never held back until they leave and re-enter the field.

While typing
Reward, never punish
Show a success tick the moment input becomes valid. Hold the first error back; an incomplete field is not yet a wrong one. After a field has erred, though, re-check on input and clear it the instant it is valid.
On blur
Now you may correct
When focus leaves the field, the answer is final enough to judge. Surface the inline error, right under the field.
On submit
Catch the rest
Re-check everything, move focus to the first error, summarise in a live region. Never let a form fail silently.

A good error message is visible and specific, says what went wrong in plain language, and tells the user how to fix it. "Invalid input" fails all three. "Enter a date after today, the event cannot start in the past" passes all three. WCAG 3.3.1 requires the error to be identified in text, so the field carries aria-invalid="true" and points at the message with aria-describedby. The colour reinforces; the words are the message.

Reward: valid while typing
Looks good.
Punish late: actionable error on blur
Add the domain ending, like .co or .com, so the address can receive mail.

Errors use --color-error, the only warm colour on the page. Everything interactive stays indigo; red's single job is to mark a genuine problem. Do not tint help text, borders or icons red for emphasis. Reserve it, so its appearance always means something is wrong.

5

Built for the thumb: the mobile contract

On a phone the type attribute is the biggest usability lever in the form. It tells the OS which keyboard to raise. inputmode tunes that keyboard without changing meaning, enterkeyhint relabels the return key, and autocomplete lets the browser fill the field from a saved profile in one tap, which also satisfies WCAG 1.3.5 Identify Input Purpose. Avoid autocomplete="off" except for genuine one-offs like a CAPTCHA answer.

You are asking forUseKeyboard raisedAutofill token
Emailtype="email"Text with a visible @, no shift neededautocomplete="email"
Phonetype="tel"Large numeric dial padautocomplete="tel"
Websitetype="url"Text with .com and / shortcutsautocomplete="url"
One-time codeinputmode="numeric"Digits only, text semantics keptautocomplete="one-time-code"
Amount of moneyinputmode="decimal"Digits with a decimal separatorNone
Quantity of ticketstype="number"Number pad with stepperNone
Full nametype="text"Standard, autocapitalized wordsautocomplete="name"
Searchtype="search"Text with a Search return keyenterkeyhint="search"

Two numbers are non-negotiable. Inputs render at font-size: 16px or larger, because iOS Safari auto-zooms any field below that on focus, and the fix is the size, never disabling user-scalable, which breaks zoom for everyone. Interactive targets are at least 24 by 24 CSS pixels to clear WCAG 2.5.8, and the comfortable goal is 44 by 44, the AAA target size and Apple's minimum. Native UA controls are exempt from the strict measure, which is one more reason to lean native on small screens.

Every input on this page renders at 16px. The type validates and picks the keyboard; inputmode only hints at it, so use the semantic type first and reach for inputmode to fine-tune. For SMS logins, autocomplete="one-time-code" lets iOS and Android drop the texted code straight into the field.

6

The accessibility floor

An accessible form is the same form built correctly. Four WCAG criteria carry most of it, and each maps straight to a habit from the sections above. This is the floor, AA is the minimum we ship, and we reach for AAA target sizing wherever the layout allows.

CriterionLevelWhat it requiresHow the field meets it
3.3.2 Labels or InstructionsAEvery control has a visible label or instruction.A persistent <label for> above each field; help linked with aria-describedby.
3.3.1 Error IdentificationAErrors are identified and described in text.Inline text message plus aria-invalid="true"; red reinforces, never carries it alone.
3.3.3 Error SuggestionAAWhen a fix is known, suggest it.The message says how to correct the value, not only that it is wrong.
1.3.5 Identify Input PurposeAACommon fields expose their purpose programmatically.Correct autocomplete tokens, which also drive autofill.
2.5.8 Target SizeAAInteractive targets are at least 24 by 24 CSS pixels.Checkboxes and toggles clear 24px; the comfortable target is 44px (AAA 2.5.5).

The label is a click target, for free. A real <label for> pointed at the input's id names the field for assistive technology and extends the tap area to the words, so a checkbox becomes far easier to hit. Group related controls in a <fieldset> with a <legend> so the group's question is announced too. This is the accessible default, not optional polish.

Patterns / Accessibility: focus order, live regions, contrastPatterns / Interactive: the full state model
Part 2The catalog, every control we reach for

One card per control, grouped by family. Each names the native element, a verdict of Native, Custom or Hybrid, the attributes that matter, and the two notes that decide quality: what happens on mobile, and the accessibility pattern to follow. When a control has no good native form, the note names the WAI-ARIA APG pattern to build against.

Text and numeric fields
TextNative

<input type="text">

autocompleteenterkeyhintmaxlength
Enter your full name so tickets match your ID.
  • Standard keyboard; keep font-size at 16px.
  • Visible <label for>; error via aria-describedby and aria-invalid.
EmailNative

<input type="email">

inputmode="email"autocomplete="email"
Looks good.
  • Raises a keyboard with @ and a dot, no shift needed.
  • Built-in format validation; still write a fix-it error message.
PasswordNative

<input type="password">

autocomplete="current-password"new-password
  • Offer a show-password toggle; never block paste.
  • Use new-password on sign-up so managers offer a strong one.
NumberNative

<input type="number">

inputmode="numeric"minmaxstep
  • For true quantities. For codes and IDs use text plus inputmode.
  • Native role is spinbutton; arrows step the value.
TelNative

<input type="tel">

inputmode="tel"autocomplete="tel"
  • Raises the phone dial pad. No format validation, so pair with a hint.
  • Add a country selector only when you serve many regions (see Composite).
URLNative

<input type="url">

inputmode="url"autocomplete="url"
  • Keyboard adds / and .com shortcuts.
  • Validates for a scheme; accept input without https:// and normalise it.
SearchNative

<input type="search">

enterkeyhint="search"
Type to see the native clear button appear.
  • Return key reads "Search"; a clear button appears on many browsers.
  • Carries an implicit searchbox role.
TextareaNative

<textarea>

rowsmaxlengthenterkeyhint="enter"
92 characters left
  • Keep 16px; let it auto-grow rather than trapping text in a scroll.
  • If you show a counter, mirror it in an aria-live region.
Selection controls
Select, singleHybrid

<select>

<option>required
Styled box, native popup.
  • Prefer native: it opens the OS wheel, already accessible.
  • Custom only to style the popup, then follow APG Select-only Combobox. Long lists: switch to an autocomplete.
Multi-selectCustom

no good native form

aria-multiselectablearia-selected
Accessibility needs
A checkbox group beats select multiple on touch.
  • <select multiple> is hostile on touch; use checkboxes or a sheet.
  • Build against APG Listbox, multi-select, or just decompose into a checkbox group.
CheckboxNative

<input type="checkbox">

accent-colorchecked
  • Target 44px; accent-color tints it indigo, no rebuild.
  • For a group, wrap in <fieldset> with a <legend>.
Radio groupNative

<input type="radio"> x N

nameaccent-color
Admission
  • One shared name; each option a 44px target.
  • APG Radio Group: one tab-stop, arrows move and select.
Switch, toggleCustom

checkbox as the base

role="switch"aria-checked
  • For an instant on/off setting; the change applies at once.
  • APG Switch: Space toggles, and the label does not change with state.
Range, sliderNative

<input type="range">

minmaxstepaccent-color
Around 65 of 100. Always show the value.
  • Tinted by accent-color; pair with a visible value, dragging is imprecise.
  • Native covers APG Slider. Two-thumb range: APG Slider Multi-Thumb.

Look at those defaults: the reminder is pre-checked because people want it, the newsletter is left off because it costs their attention. A default must be the choice the user would make with full attention, and it must stay editable. Pre-fill to save effort, never to extract a choice.

Patterns / Persuasion: the ethics of defaults
Date, time and colour
Calendar dateNative

<input type="date">

minmax
A future date the user picks by looking, so the calendar earns its place.
  • Opens the OS calendar. Right for a date the user picks by looking.
  • Custom only to style the calendar: a button opening an APG Dialog grid.
Memorable dateHybrid

3 x <input type="text">

inputmode="numeric"fieldsetlegend
Date of birth
  • Birthdays and issue dates: three numeric fields beat a calendar.
  • The GOV.UK pattern: day, month, year, grouped in a fieldset.
TimeNative

<input type="time">

datetime-localstep
  • Native time wheel respects the 12 or 24 hour locale.
  • Use datetime-local to capture date and time in one control.
ColourNative

<input type="color">

value hex
Pair the picker with a typeable hex field.
  • Opens the OS colour picker. accent-color does not apply here.
  • Pair with a text hex field so the value is typeable too.
Specialized inputs
FileNative

<input type="file">

acceptcapturemultiple
Native picker; on phones capture can open the camera.
  • capture can open the camera directly for a photo upload.
  • Style the triggering <label>, never hide the input without a label.
Combobox, autocompleteCustom

<input> + <datalist> as base

role="combobox"aria-autocompletearia-expanded
Native datalist stands in for a full APG combobox.
  • Text keyboard; render suggestions as a sheet on small screens.
  • APG Combobox with aria-activedescendant. Enhance a real <select>.
One-time codeNative

<input> single or split

autocomplete="one-time-code"inputmode="numeric"
Sent by SMS; the phone can fill it in one tap.
  • iOS and Android autofill the SMS code straight into the field.
  • If you split into boxes, group them and announce progress.
Phone with countryHybrid

<input type="tel"> + selector

autocomplete="tel"tel-country-code
Native tel input; only the country picker is custom.
  • The number stays a native tel input; only the country picker is custom.
  • Country selector follows APG Combobox or Listbox.
Currency, maskedCustom

<input type="text"> + mask

inputmode="decimal"patternaria-describedby
Amount in COP. The decimal keypad keeps entry fast.
  • Avoid type="number": it fights thousands separators. Use text plus a mask.
  • State the expected format in help; do not break editing for AT.
Composite and advanced
Tags, chipsCustom

no native form

role="combobox"aria-label per chip
JazzLive
  • Chips are 44px targets; each removable by keyboard, not hover alone.
  • Input follows APG Combobox; each chip's remove button has a label.
Rating, starsCustom

radio group as base

role="radiogroup"name
Rate this event
  • Each star a 44px target; never depend on hover to set a value.
  • One star is one radio option, so APG Radio Group carries it.
Stepper, plus and minusNative

<input type="number">

minmaxsteprole="spinbutton"
  • Custom plus and minus buttons at 44px beat the tiny native spinners.
  • Native and custom both are APG Spinbutton: arrows, Home, End.
Rich textCustom

contenteditable, or a library

contenteditablerole="toolbar"
Description
An intimate night of live jazz under the stars.
Representative pattern; a real editor follows APG Toolbar.
  • Expensive to make accessible; use only when formatting truly matters.
  • Toolbar follows APG Toolbar; announce applied formatting.
CloseRules of the form

Rule Reach for the native control before building a custom one.

Why A native <select>, date input or checkbox ships the keyboard, picker and accessibility a hand-built widget has to re-earn from scratch.

Rule Brand native controls with accent-color, caret-color and color-scheme, and know their reach.

Why They tint checkbox, radio, range, progress and the caret without a rebuild; select, date and text they do not touch, so plan for those.

Rule Keep the label visible above every field, and mark the rarer of required or optional in words.

Why A placeholder vanishes on the first keystroke and takes the question with it; an asterisk alone reaches no screen reader.

Rule Match type, inputmode, enterkeyhint and autocomplete to the answer.

Why The right keyboard rises on the first tap and a saved profile fills the field in one, instead of a manual retype.

Rule Render inputs at 16px or larger and give interactive targets at least 24px, ideally 44px.

Why Below 16px iOS Safari auto-zooms on focus; below 24px the target fails WCAG 2.5.8, and the thumb misses.

Rule Reward valid input while typing with :user-valid, and surface errors on blur.

Why Flagging a half-typed value as wrong punishes progress; once a field has already erred, re-check on input and clear it the instant it is valid.

Rule Write errors that name the fix, in text, with aria-invalid and aria-describedby.

Why "Invalid" leaves no way to recover, and a red border alone reaches no screen reader; the words carry the message.

Rule Reserve --color-error for genuine errors and nothing else.

Why One warm colour on an otherwise indigo page means its appearance always signals a real problem.

Rule When you must go custom, build against the matching WAI-ARIA APG pattern and keep a native base underneath.

Why The APG pattern encodes the keyboard and roles users expect, and the native base keeps the field working if the script fails.

Decision ladder. Adding a field? Stop at the first rung that resolves it.

Does this field need to exist? Every field is a cost. If you can infer it, default it, or drop it, do that before styling anything.

Can a native control answer it? A <select>, type="date", a checkbox. Use it, and keyboard and accessibility come built in.

What is the answer's shape? Pick the type and inputmode that raise the matching keyboard, add enterkeyhint and the autocomplete token, and brand it with accent-color.

What can go wrong, and how is it fixed? Write the help and the recovery message before the layout. If you cannot say how to fix it, the rule is wrong, not the user.

Only now, assemble it from the primitives: visible label, control, help, error, in one column, states inherited from Patterns / Interactive. If the pattern is not in HTML, build the matching APG pattern over a native base.

Sources

  • MDN Web Docs, CSS and HTML references. accent-color (tints checkbox, radio, range, progress, and its limits), caret-color, color-scheme, <input> types, the inputmode and enterkeyhint global attributes, the autocomplete token list, and the :user-invalid pseudo-class. developer.mozilla.org/en-US/docs/Web/CSS/accent-color
  • web.dev, "accent-color" and the Learn Forms course. Four themable controls, automatic accent contrast, interaction with color-scheme, and the note that HTML form elements are difficult to customize. web.dev/articles/accent-color
  • caniuse, accent-color support. Roughly 94 percent, available since Chrome and Edge 93, Firefox 92, Safari 15.4 (March 2022), so widely available with a Safari caveat to check in QA. caniuse.com/mdn-css_properties_accent-color
  • WAI-ARIA Authoring Practices Guide (APG). Patterns for Combobox, Listbox, Switch, Slider, Slider Multi-Thumb, Spinbutton, Radio Group, Checkbox, Dialog and Toolbar. The canonical path is w3.org/WAI/ARIA/apg/patterns/{pattern}. SPA The index is a single-page app; individual pattern pages open directly.
  • GOV.UK Design System and Service Manual. Progressive enhancement (build on working HTML, enhance after), the Select component (long drop-downs are not user friendly), and the Date input (three text fields with inputmode="numeric" in a fieldset for memorable dates). design-system.service.gov.uk/components/date-input
  • W3C, Understanding WCAG 2.2. SC 3.3.2 (A), 3.3.1 (A) with technique ARIA21 aria-invalid, 3.3.3 (AA), 1.3.5 Identify Input Purpose (AA), 2.5.8 Target Size Minimum 24px (AA) and 2.5.5 44px (AAA). w3.org/WAI/WCAG22/Understanding
  • Mihael Konjevic, "Inline Validation in Web Forms" (Smashing Magazine, 2022). Source of "reward early, punish late": validate to a positive while typing, hold errors until the user leaves the field, then clear on input once corrected. smashingmagazine.com/2022/09/inline-validation-web-forms-ux
  • CSS-Tricks and defensivecss.dev, iOS input zoom. Inputs at 16px or larger prevent Safari iOS from auto-zooming on focus; do not disable user scaling. css-tricks.com/16px-or-larger-text-prevents-ios-form-zoom
  • Apple Human Interface Guidelines, Text fields. Content-appropriate keyboards, a 44pt minimum target, content types for autofill, and secure entry. developer.apple.com/design/human-interface-guidelines/text-fields SPA
  • Nielsen Norman Group and Baymard Institute. Placeholders cannot replace labels; error messages must be visible, specific and constructive; single-column forms complete faster and validate on blur. nngroup.com/articles/form-design-placeholders, baymard.com/blog/avoid-multi-column-forms
  • The reward-while-typing timing and the single-accent, error-only use of --color-error are this system's house rules layered on the cited guidance, not verbatim claims from the authors.