Browse by section

Web Design 日本語

HTML Form Features Worth Using Today

Switching form validation styling from :invalid to :user-invalid alone transforms the experience. The unpleasant behaviour where fields turn red before anyone has typed anything simply goes away. It reached Baseline in October 2023 and works everywhere now.

One clarification first: type="email" and required are not 2023 additions. They date from HTML5, around 2011. This article covers those foundations briefly and then spends its length on what genuinely became usable from 2023 onwards: :user-invalid, showPicker(), requestSubmit(), field-sizing, and accent-color.

Sponsored

The foundations that still carry forms

These have worked across browsers for years, and you should check whether they cover your case before writing JavaScript.

Feature Role Since
type="email" / url / tel Format checking and mobile keyboard switching HTML5
required Mandatory input HTML5
pattern Format via regular expression HTML5
<datalist> Input suggestions HTML5
<output> Displaying a calculated result HTML5
novalidate Disabling built-in validation HTML5
<form>
  <label for="email">Email</label>
  <input type="email" id="email" name="email"
         autocomplete="email" required>

  <label for="zip">Postal code</label>
  <input type="text" id="zip" name="zip"
         inputmode="numeric" autocomplete="postal-code"
         pattern="\d{3}-?\d{4}"
         title="Enter as 123-4567 or 1234567">

  <button type="submit">Send</button>
</form>

Three things worth knowing:

  • pattern is implicitly anchored: you do not write ^ and $, and you cannot do a partial match
  • Always pair pattern with title: the title text is used in the error message. Without it users only see a generic complaint
  • Never use placeholder as a label: it disappears on first keystroke, taking the field’s meaning with it

Most importantly, browser-side validation is not a security control. Anyone can add novalidate in DevTools. Server-side validation is always required.

:user-invalid fixes the “red before you type” problem

The flaw in :invalid is that every empty required field is styled as an error from the moment the page loads. You are telling off a user who has done nothing yet.

:user-invalid only matches after the user has interacted with the field and moved on.

/* red from page load */
input:invalid {
  border-color: #d33;
}

/* red only after the user has touched it */
input:user-invalid {
  border-color: #d33;
}
input:user-valid {
  border-color: #2a7;
}

You can drive the error message from CSS as well:

<div class="field">
  <label for="mail">Email</label>
  <input type="email" id="mail" name="mail" required>
  <p class="error">Please enter a valid email address</p>
</div>
.error { display: none; color: #d33; font-size: .875rem; }

input:user-invalid ~ .error { display: block; }
Selector Matches when Use for
:invalid The value is invalid, always Gating the submit button via :has()
:user-invalid Invalid after user interaction Field appearance
:user-valid Valid after user interaction Success indicators
:placeholder-shown The placeholder is visible Floating labels

Sponsored

showPicker() opens date, colour, and suggestion pickers on demand

By default the calendar for <input type="date"> only opens if you hit a small icon precisely. showPicker() lets you open it from anywhere in the field. It has been available in all modern browsers since September 2022.

const dateInput = document.getElementById('date');

dateInput.addEventListener('click', () => {
  if (!('showPicker' in HTMLInputElement.prototype)) return;
  try {
    dateInput.showPicker();
  } catch (e) {
    // NotAllowedError etc. — fall back to default behaviour
  }
});

It works with date, month, week, time, datetime-local, color, file, and inputs with a <datalist>.

Three constraints mean you must wrap it in try:

  • NotAllowedError without user activation—you cannot call it from a setTimeout
  • SecurityError in a cross-origin iframe (except file and color)
  • InvalidStateError if the input is readonly or disabled

Auto-submit with requestSubmit(), not this.submit()

Submitting a form when a filter select changes is common, but do not use this.submit().

<!-- validation skipped entirely, submit event never fires -->
<form onchange="this.submit()">
  ...
</form>

form.submit() bypasses validation and does not fire the submit event, so any pre-submit logic other scripts installed—double-submit prevention, token injection—is silently skipped.

Use requestSubmit(), which behaves exactly as if the submit button had been pressed.

<form id="filter-form" action="/search">
  <select name="category">
    <option value="all">All</option>
    <option value="book">Books</option>
  </select>
  <noscript><button type="submit">Filter</button></noscript>
</form>
const form = document.getElementById('filter-form');

form.addEventListener('change', () => {
  form.requestSubmit(); // validation and submit event both run
});

Related: the submit event exposes which button caused the submission via event.submitter, which is handy for a single form with “Save draft” and “Publish”.

form.addEventListener('submit', (e) => {
  if (e.submitter?.value === 'draft') {
    // save as draft
  }
});

Sponsored

Auto-resizing fields with field-sizing

Growing a textarea with its content used to require measuring scrollHeight in JavaScript. CSS field-sizing: content reduces it to one line. It reached Baseline in June 2026.

textarea {
  field-sizing: content;
  min-height: 4lh;   /* keep at least four lines */
  max-height: 20lh;  /* stop it running away */
  width: 100%;
}

input[type="text"] {
  field-sizing: content;
  min-width: 8ch;
}

Setting min-height and max-height together is the trick. Without them the field shrinks to one line when empty and grows to fill the screen with long text. The lh unit lets you express the limits in lines.

Recolouring checkboxes and sliders in one line

Recolouring checkboxes, radios, range, and progress used to mean rebuilding them with appearance: none. accent-color does it in one line.

:root {
  accent-color: #2a7fff;
}

You keep the native appearance, keyboard behaviour, and accessibility while swapping in your brand colour. Rebuilding controls with appearance: none purely for looks means reimplementing focus rings and screen reader behaviour yourself—try this first.

Checklist

What I check every time I build a form:

  • Connect label and id. Do not substitute placeholder or aria-label (the latter is a last resort when no visible label is possible)
  • Set autocomplete correctly: name, email, tel, postal-code, street-address, one-time-code. The reduction in typing is visible
  • Match inputmode to the keyboard you want: numeric versus decimal
  • Style errors with :user-invalid
  • Auto-submit with requestSubmit()
  • Always validate on the server

Input-level attributes are covered in the HTML input element, confirmation dialogs in the HTML dialog element, and state-driven layout in the CSS :has() selector.

Summary

  • type="email" and required are HTML5 features, not 2023 additions
  • Style errors with :user-invalid / :user-valid (Baseline October 2023)
  • showPicker() opens date, colour, and suggestion pickers on demand. Wrap it in try
  • Auto-submit with requestSubmit(), never this.submit()
  • Auto-resizing textareas: field-sizing: content (Baseline June 2026)
  • Control colours: accent-color. Do not rebuild with appearance: none
  • Browser validation is not security

The range of forms you can build without JavaScript has widened considerably. Check the platform before reaching for a validation library.