Jason Caldwell
    Back to notebook
    November 5, 2026•Design•8 min read

    Practical Tips for Better Web Forms

    Autofill, keyboards, error messages and other small details that make forms faster to fill in.

    Nobody opens a website hoping to fill in a form. They want what's on the other side of it: the account, the order, the appointment. Every extra tap, retyped address and confusing error stands between them and that.

    Most of that friction comes from small details. A missing attribute. A field that's two pixels too small. A grey button that won't say what's wrong. Here are eight that are quick to fix, each with a demo so you can feel the difference before you copy the code.

    1.Tell the browser what each field is

    Browsers and password managers can fill in a whole form with one tap, as long as they know what each field is for. The autocomplete attribute tells them. It takes a value from a fixed list, such as name, email, tel, postal-code or cc-number.[1]

    Without it, the browser has to guess from the field's name, id and label. Sometimes it guesses right. Sometimes it fills half the form and leaves the rest to the user.

    name
    email
    tel
    address-line1
    postal-code

    A simulation that fills the form from a sample profile. Switch the attributes off and fill again to see what's left to guesswork. These are real fields, so your own browser's autofill works in them too.
    HTML
    <label for="email">Email</label>
    <input id="email" name="email" type="email" autocomplete="email">
    
    <label for="zip">ZIP code</label>
    <input id="zip" name="zip" autocomplete="postal-code">

    Sign-in forms need two more. Use current-password when someone signs in and new-password when they create or change a password, so the password manager knows whether to fill a saved password or offer to save a new one. If people sign in with their email address, give that field autocomplete="username". That way the password manager saves the email with the password.[2]

    HTML
    <!-- Signing in (labels left out to keep it short) -->
    <input type="email" autocomplete="username">
    <input type="password" autocomplete="current-password">
    
    <!-- Signing up, or changing a password -->
    <input type="password" autocomplete="new-password">

    2.Don't use type="number" for codes

    type="number" is meant for amounts you might nudge up or down, like the number of guests at a table. Plenty of things are made of digits without being numbers: verification codes, card numbers, account numbers, ZIP codes. Treat them as numbers and odd things happen.

    Both fields below have maxlength="6" and start with 012345. Press the up arrow in each, then try typing a seventh digit or the letter e.

    value "012345"
    value "012345"
    The readout under each field shows the value your code would receive. On a phone, skip the arrow key and try the seventh digit.

    The number field adds one and drops the leading zero. Scrolling while the field is focused can change the value the same way. maxlength doesn't apply to number fields at all, and anything the browser can't read as a number comes back as an empty value.

    GOV.UK's design system dropped type="number" in 2020 for these reasons, and because some assistive technology, including Dragon and NVDA, had trouble with it. Use a text field with inputmode="numeric" instead. Phones still show a number keyboard, and you get exactly what was typed.[3]

    HTML
    <!-- Before -->
    <input type="number">
    
    <!-- After -->
    <input type="text" inputmode="numeric">

    Keep type="number" for real quantities, where the arrows are a feature.

    3.Ask for the right keyboard

    On a phone, the keyboard covers half the screen, so it's worth asking for the right one. Three attributes control it. type and inputmode decide which keyboard appears, and enterkeyhint decides what the Enter key says.[4]

    Email

    Letters, plus @ and . keys. Enter says next.

    HTML
    <input type="email" autocomplete="email" enterkeyhint="next">
    Tap a field to see the keyboard it asks for. On a phone, your real keyboard comes up too. The drawing is a rough guide, since every phone draws its keyboard a little differently.

    The number pads in the drawing have no Enter key, and neither does the iPhone's. If a field brings up a number pad, make sure there's a visible button to carry on.

    enterkeyhint only changes the label. The key still does whatever it did before, so if it says Next, moving to the next field is up to you:

    JavaScript
    form.addEventListener('keydown', (event) => {
      const field = event.target;
      if (event.key !== 'Enter' || event.isComposing) return;
      if (field.enterKeyHint !== 'next') return;
    
      event.preventDefault();
      const fields = [...form.querySelectorAll('input, select, textarea')];
      fields[fields.indexOf(field) + 1]?.focus();
    });

    The isComposing check is for people typing in Japanese, Chinese or Korean, who press Enter to confirm characters as they type.

    4.Keep form text at 16px or larger

    When someone taps a field whose text is smaller than 16px, Safari on iPhone zooms in so they can read it. The page stays zoomed after they leave the field, so they have to pinch back out before they can carry on.

    Sign in

    Email, 14px textEmail, 16px text

    Tap a field on the phone.

    On an iPhone? Try the real thing

    A simulation of what happens on an iPhone. If you're reading this on one, the two real fields zoom for real.

    It's tempting to fix this with maximum-scale=1 or user-scalable=no in the viewport tag. Don't. Those work by limiting zoom, and on many browsers that blocks pinch-to-zoom too, which locks out anyone who needs bigger text. Make the text bigger instead:

    CSS
    input,
    select,
    textarea {
      font-size: max(16px, 1rem);
    }

    max() keeps fields at 16px even if the rest of your interface uses 14px text, and lets them grow for people who've set a larger default font size.

    5.Make one-time codes a single field

    Six separate boxes look tidy in a mockup. They also break paste, autofill and screen readers unless someone writes extra code for each. One field gets all three right without any extra code:

    HTML
    <label for="code">Verification code</label>
    <input
      id="code"
      name="code"
      type="text"
      inputmode="numeric"
      autocomplete="one-time-code"
      maxlength="6">

    autocomplete="one-time-code" lets Safari on iPhone, iPad and Mac offer the code from Messages right above the keyboard. Paste works because there's only one place for the code to go.[5]

    Your verification code is 482913

    Six inputs

    One input

    Copy the code, then paste it into each version.

    If the design calls for boxes, draw them. The version on the right is one real input sitting on top of six boxes that are only there for looks.

    6.Let text areas grow

    A fixed-height text area makes people scroll inside a small box to reread what they wrote. field-sizing: content lets it grow with the text. Every major browser supports it now that Firefox added it in June 2026.[6]

    Both start at three lines. Add a few sentences, or type your own, and compare.

    Once field-sizing is set, rows stops doing anything, so set limits with min-height and max-height. The lh unit is one line of text, which makes those limits easy to read. Browsers that don't support the property keep a normal fixed box, which is a fine fallback.

    CSS
    textarea {
      field-sizing: content;
      min-height: 3lh;
      max-height: 10lh;
    }

    7.Show errors after people leave a field

    When should a field tell someone they've made a mistake? Type an email address into each of these, then tab away. Then make a typo, leave the field, and fix it.

    The same rule in all three: an email address like name@example.com.

    Checking on every keystroke shows an error before people have finished typing. Checking only when they leave the field means the error stays up after they've fixed it. The third field combines the two. It waits until you leave before it shows an error. Once there is one, it checks again on every keystroke, so the message disappears the moment the value is right. Mihael Konjević called this "reward early, punish late."[7]

    JavaScript
    input.addEventListener('blur', () => {
      showError(input, !isValid(input.value));
    });
    
    input.addEventListener('input', () => {
      // Only re-check while an error is showing, so it clears as soon as it's fixed
      if (input.getAttribute('aria-invalid') === 'true') {
        showError(input, !isValid(input.value));
      }
    });

    GOV.UK recommends checking only when people submit, because errors that appear while someone is typing trip people up, especially slower typists. That's a safe default. If you do check as people go, use this timing.[8]

    CSS can handle the styling. :invalid matches as soon as the page loads, so empty required fields are red before anyone has typed in them. :user-invalid waits until someone has interacted with the field. Every major browser has supported it since 2023.[9]

    Enter an email address like name@example.com

    Enter an email address like name@example.com

    Both fields are required. The left one is red before you've touched it. Type a few letters into each and tab away.
    CSS
    input:user-invalid {
      border-color: var(--error);
    }
    
    input:user-invalid + .error-message {
      display: block;
    }

    8.Don't disable the submit button

    A disabled button says something is wrong without saying what. It's usually pale grey text on a pale grey background, and it can't take focus, so keyboard and screen reader users may tab straight past it.[10][11]

    Try to create an account with each form. They have the same two problems.

    Same fields, same problems. Only one of the forms tells you what they are.

    Keep the button enabled. When someone submits, list the problems at the top of the form and link each one to its field. Keep everything they typed, and move focus to the list so screen readers announce it.

    HTML
    <div class="error-summary" tabindex="-1">
      <h2>There's a problem</h2>
      <ul>
        <li><a href="#email">Enter an email address like name@example.com</a></li>
        <li><a href="#terms">Agree to the terms to create an account</a></li>
      </ul>
    </div>
    
    <!-- After rendering the summary: summary.focus() -->

    There's one good time to disable a button: right after it's pressed, so the form can't be sent twice. Change the label while it works, so people know something is happening.

    Attributes for common fields

    The attributes for the fields that come up most often. Pick one, copy the markup, and tap the preview on your phone to check the keyboard.

    HTML
    <label for="code">Verification code</label>
    <input
      id="code"
      name="code"
      type="text"
      inputmode="numeric"
      autocomplete="one-time-code"
      maxlength="6">

    A single field. Safari can fill it from Messages.

    Preview

    None of these take long, and most are a single attribute. Together, they decide whether people get through your form or give up on it.

    Sources

    [1] Autofill: web.dev Learn Forms ↩

    [2] Sign-up form best practices: web.dev ↩

    [3] Why the GOV.UK Design System team changed the input type for numbers: GOV.UK, 2020 ↩

    [4] enterkeyhint: MDN ↩

    [5] SMS OTP form best practices: web.dev ↩

    [6] field-sizing: MDN ↩

    [7] Inline validation in forms: designing the experience: Mihael Konjević, 2016 ↩

    [8] Recover from validation errors: GOV.UK Design System ↩

    [9] :user-valid and :user-invalid: web-features ↩

    [10] Buttons: NHS digital service manual ↩

    [11] Usability pitfalls of disabled buttons: Vitaly Friedman, Smashing Magazine, 2021 ↩

    Subscribe to Jason's Notebook

    JMSKARTLMC
    1,500+ readers