Polyglot
Languages Web (HTML / CSS / JS) html forms
Web (HTML / CSS / JS) § html-forms

Forms and inputs

HTML forms are the conventional mechanism for user input on the Web. The <form> element wraps input controls — <input>, <select>, <textarea>, <button> — admitting submission to a server (or interception by JavaScript). The substantial input types (text, email, password, number, date, file, checkbox, radio, etc.) admit substantial built-in validation, mobile keyboard variants, and accessibility integration. The FormData API admits programmatic form processing in JavaScript. The combination — declarative form markup, substantial input types with built-in validation, the FormData programmatic surface — is the substance of HTML’s input surface.

A simple form

<form action="/submit" method="POST">
    <label for="name">Name:</label>
    <input type="text" id="name" name="name" required>

    <label for="email">Email:</label>
    <input type="email" id="email" name="email" required>

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

The principal attributes:

  • action — URL to submit to.
  • methodGET (in URL) or POST (in body).
  • enctype — encoding (application/x-www-form-urlencoded default; multipart/form-data for files; text/plain rare).
  • novalidate — admit bypassing built-in validation.
  • autocomplete — admit/suppress browser autocompletion.
  • target — where to display the response (rare; _blank, etc.).

Input types

The substantial input vocabulary:

<!-- Text inputs: -->
<input type="text" name="username">
<input type="email" name="email">
<input type="password" name="pw">
<input type="search" name="q">
<input type="tel" name="phone">
<input type="url" name="website">

<!-- Numeric: -->
<input type="number" name="age" min="0" max="150" step="1">
<input type="range" name="volume" min="0" max="100" value="50">

<!-- Date and time: -->
<input type="date" name="birthday">
<input type="time" name="appointment">
<input type="datetime-local" name="event">
<input type="month" name="period">
<input type="week" name="week">

<!-- Selection: -->
<input type="checkbox" name="agree" value="yes">
<input type="radio" name="size" value="small">
<input type="radio" name="size" value="medium">
<input type="radio" name="size" value="large">

<!-- File: -->
<input type="file" name="upload">
<input type="file" name="photos" multiple accept="image/*">

<!-- Specialised: -->
<input type="color" name="bg">
<input type="hidden" name="csrf" value="token123">

<!-- Buttons: -->
<input type="submit" value="Submit">
<input type="reset" value="Reset">
<input type="button" value="Click me">

<!-- More substantial admit via <button>: -->
<button type="submit">Submit</button>
<button type="reset">Reset</button>
<button type="button">Click me</button>

The mobile keyboards adapt to type:

  • email — keyboard with @ and .com.
  • tel — numeric keyboard.
  • number — numeric keyboard.
  • url — keyboard with .com and /.

Labels

Every input must have an associated label — admits substantial accessibility:

<!-- Implicit (input inside label): -->
<label>
    Name:
    <input type="text" name="name">
</label>

<!-- Explicit (matched by id): -->
<label for="name">Name:</label>
<input type="text" id="name" name="name">

The label admits substantial click-target expansion (clicking the label focuses the input) and substantial screen-reader association.

For grouping form controls (radio buttons, checkboxes), <fieldset> and <legend>:

<fieldset>
    <legend>Size</legend>
    <label><input type="radio" name="size" value="small"> Small</label>
    <label><input type="radio" name="size" value="medium"> Medium</label>
    <label><input type="radio" name="size" value="large"> Large</label>
</fieldset>

Select and textarea

<!-- Dropdown: -->
<label for="country">Country:</label>
<select id="country" name="country">
    <option value="">-- Select --</option>
    <option value="us">United States</option>
    <option value="uk">United Kingdom</option>
    <option value="de">Germany</option>
</select>

<!-- Grouped options: -->
<select name="car">
    <optgroup label="European">
        <option value="vw">Volkswagen</option>
        <option value="bmw">BMW</option>
    </optgroup>
    <optgroup label="Asian">
        <option value="toyota">Toyota</option>
        <option value="honda">Honda</option>
    </optgroup>
</select>

<!-- Multiple selection: -->
<select name="languages" multiple size="5">
    <option value="en">English</option>
    <option value="es">Spanish</option>
    <option value="fr">French</option>
</select>

<!-- Multi-line text: -->
<label for="comment">Comment:</label>
<textarea id="comment" name="comment" rows="5" cols="40"></textarea>

Validation

HTML5 admits substantial built-in validation:

<!-- Required: -->
<input type="text" name="name" required>

<!-- Type-based (email, url, number): -->
<input type="email" name="email" required>
<input type="number" name="age" min="0" max="150">
<input type="url" name="website">

<!-- Pattern (regex): -->
<input type="text" name="zip" pattern="\d{5}" title="5-digit ZIP code">

<!-- Length: -->
<input type="text" name="username" minlength="3" maxlength="20">
<textarea name="bio" maxlength="500"></textarea>

<!-- Numeric range: -->
<input type="number" name="rating" min="1" max="5" step="0.5">

The browser shows a built-in validation message; the :invalid and :valid CSS pseudo-classes admit substantial styling.

For programmatic validation in JavaScript:

const input = document.querySelector("input[name=email]");

input.addEventListener("invalid", (e) => {
    e.preventDefault();                            // suppress default popup
    showCustomError(input, e.target.validationMessage);
});

// Check validity:
if (!input.checkValidity()) {
    console.log(input.validationMessage);
}

// Set custom validity:
input.setCustomValidity("This email is already registered");

File uploads

<form action="/upload" method="POST" enctype="multipart/form-data">
    <input type="file" name="photo" accept="image/*">
    <input type="file" name="docs" multiple>
    <button type="submit">Upload</button>
</form>

The enctype="multipart/form-data" is required for file uploads. The accept admits hint to the file picker; not strict validation.

For programmatic handling:

const input = document.querySelector("input[type=file]");
input.addEventListener("change", (e) => {
    const files = e.target.files;                  // FileList
    for (const file of files) {
        console.log(file.name, file.size, file.type);
    }
});

Autocompletion

The autocomplete admits substantial control:

<!-- Common values: -->
<input type="text" name="name" autocomplete="name">
<input type="email" name="email" autocomplete="email">
<input type="tel" name="phone" autocomplete="tel">
<input type="text" name="address" autocomplete="street-address">
<input type="text" name="city" autocomplete="address-level2">
<input type="text" name="postal" autocomplete="postal-code">
<input type="text" name="country" autocomplete="country">

<!-- Credit card: -->
<input type="text" name="cc-name" autocomplete="cc-name">
<input type="text" name="cc-number" autocomplete="cc-number">
<input type="text" name="cc-exp" autocomplete="cc-exp">
<input type="text" name="cc-csc" autocomplete="cc-csc">

<!-- Login: -->
<input type="text" name="username" autocomplete="username">
<input type="password" name="pw" autocomplete="current-password">
<input type="password" name="new-pw" autocomplete="new-password">

<!-- Disable: -->
<input type="text" name="custom" autocomplete="off">

The autocomplete tokens are conventional — admit substantial password manager and form-filling integration.

FormData API

The FormData admits programmatic form serialisation:

const form = document.querySelector("form");

form.addEventListener("submit", async (e) => {
    e.preventDefault();
    const data = new FormData(form);

    // Read values:
    console.log(data.get("name"));
    console.log(data.get("email"));

    // All entries:
    for (const [key, value] of data.entries()) {
        console.log(key, value);
    }

    // As object:
    const obj = Object.fromEntries(data);

    // Send via fetch:
    const response = await fetch("/submit", {
        method: "POST",
        body: data
    });
});

// Construct from scratch:
const data = new FormData();
data.append("name", "Alice");
data.append("file", fileBlob, "filename.txt");

Submission

The conventional submission via fetch:

form.addEventListener("submit", async (e) => {
    e.preventDefault();

    const data = new FormData(form);
    const submitButton = form.querySelector('[type="submit"]');
    submitButton.disabled = true;                  // prevent double submit

    try {
        const response = await fetch(form.action, {
            method: form.method,
            body: data
        });

        if (!response.ok) throw new Error(`HTTP ${response.status}`);

        const result = await response.json();
        showSuccess(result);
    } catch (err) {
        showError(err);
    } finally {
        submitButton.disabled = false;
    }
});

Common patterns

Login form

<form action="/login" method="POST">
    <fieldset>
        <legend>Sign in</legend>

        <label for="username">Username</label>
        <input type="text" id="username" name="username"
               autocomplete="username" required autofocus>

        <label for="password">Password</label>
        <input type="password" id="password" name="password"
               autocomplete="current-password" required minlength="8">

        <label>
            <input type="checkbox" name="remember" value="yes">
            Remember me
        </label>

        <button type="submit">Sign in</button>

        <p><a href="/forgot">Forgot password?</a></p>
    </fieldset>
</form>

Search form

<form action="/search" method="GET" role="search">
    <label for="q" class="sr-only">Search</label>
    <input type="search" id="q" name="q" placeholder="Search..." autocomplete="off">
    <button type="submit">Search</button>
</form>

Contact form

<form action="/contact" method="POST">
    <fieldset>
        <legend>Contact us</legend>

        <label for="name">Name</label>
        <input type="text" id="name" name="name" required autocomplete="name">

        <label for="email">Email</label>
        <input type="email" id="email" name="email" required autocomplete="email">

        <label for="topic">Topic</label>
        <select id="topic" name="topic" required>
            <option value="">-- Select --</option>
            <option value="support">Support</option>
            <option value="sales">Sales</option>
            <option value="other">Other</option>
        </select>

        <label for="message">Message</label>
        <textarea id="message" name="message" required minlength="10" maxlength="1000"
                  rows="5"></textarea>

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

File upload with preview

<form action="/upload" method="POST" enctype="multipart/form-data">
    <input type="file" id="photo" name="photo" accept="image/*">
    <img id="preview" alt="" hidden>
    <button type="submit">Upload</button>
</form>

<script>
    const input = document.getElementById("photo");
    const preview = document.getElementById("preview");

    input.addEventListener("change", (e) => {
        const file = e.target.files[0];
        if (file) {
            preview.src = URL.createObjectURL(file);
            preview.hidden = false;
        }
    });
</script>

Validation styling

input:invalid {
    border-color: red;
}

input:valid {
    border-color: green;
}

input:invalid:not(:placeholder-shown) {
    background-color: #fee;
}

The :placeholder-shown admits substantial UX — only show invalid state after user has typed.

Multi-step form

<form id="checkout">
    <fieldset id="step-1">
        <legend>Step 1: Shipping</legend>
        <!-- shipping fields -->
        <button type="button" onclick="goToStep(2)">Next</button>
    </fieldset>

    <fieldset id="step-2" hidden>
        <legend>Step 2: Payment</legend>
        <!-- payment fields -->
        <button type="button" onclick="goToStep(1)">Back</button>
        <button type="submit">Place order</button>
    </fieldset>
</form>

Custom validation

<form id="signup">
    <input type="password" id="pw" name="password" required>
    <input type="password" id="pw-confirm" name="password-confirm" required>
    <button type="submit">Sign up</button>
</form>

<script>
    const form = document.getElementById("signup");
    const pw = document.getElementById("pw");
    const confirm = document.getElementById("pw-confirm");

    function validatePasswords() {
        if (pw.value !== confirm.value) {
            confirm.setCustomValidity("Passwords don't match");
        } else {
            confirm.setCustomValidity("");
        }
    }

    pw.addEventListener("input", validatePasswords);
    confirm.addEventListener("input", validatePasswords);
</script>

Disabled state

<input type="text" name="locked" value="readonly" readonly>
<input type="text" name="disabled" value="disabled" disabled>
<button disabled>Disabled button</button>

<fieldset disabled>
    <!-- All inputs inside are disabled -->
    <input type="text" name="x">
    <button>Locked</button>
</fieldset>

The readonly admits the value being submitted (just not edited); disabled admits neither.

Datalist (autocomplete suggestions)

<label for="browser">Browser:</label>
<input list="browsers" id="browser" name="browser">
<datalist id="browsers">
    <option value="Chrome">
    <option value="Firefox">
    <option value="Safari">
    <option value="Edge">
</datalist>

The <datalist> admits substantial autocompletion — admits typing freely with suggestions.

Output

<form oninput="result.value = parseInt(a.value) + parseInt(b.value)">
    <input type="number" id="a" name="a" value="0">
    +
    <input type="number" id="b" name="b" value="0">
    =
    <output name="result" for="a b">0</output>
</form>

The <output> admits computed-result display.

Required indicators

<style>
    label.required::after {
        content: " *";
        color: red;
    }
</style>

<label for="name" class="required">Name</label>
<input type="text" id="name" name="name" required aria-required="true">

Programmatic submission

const form = document.querySelector("form");

// Submit programmatically:
form.submit();                                     // does NOT trigger submit event

// To trigger validation and the submit event:
form.requestSubmit();                              // modern alternative

// Reset:
form.reset();

Form data validation in JavaScript

function validate(formData) {
    const errors = [];

    if (!formData.get("email").includes("@")) {
        errors.push("Invalid email");
    }

    const age = parseInt(formData.get("age"));
    if (isNaN(age) || age < 0 || age > 150) {
        errors.push("Invalid age");
    }

    return errors;
}

form.addEventListener("submit", (e) => {
    const data = new FormData(form);
    const errors = validate(data);
    if (errors.length > 0) {
        e.preventDefault();
        showErrors(errors);
    }
});

A note on the conventional discipline

The contemporary HTML forms advice:

  • Always associate <label> with inputs — admit substantial accessibility.
  • Use semantic input types — admit substantial mobile keyboards and validation.
  • Use required, min, max, pattern — admit substantial built-in validation.
  • Use autocomplete tokens — admit substantial password manager integration.
  • Use enctype="multipart/form-data" for file uploads.
  • Use FormData API for programmatic form processing.
  • Use e.preventDefault() in submit handlers to admit fetch-based submission.
  • Use <fieldset>/<legend> for grouped controls.
  • Use aria-* attributes for substantial custom validation messaging.
  • Use :invalid:not(:placeholder-shown) for substantial UX-friendly validation styling.

The combination — declarative form markup, substantial input types with built-in validation, the FormData programmatic surface, the substantial accessibility integration through labels and ARIA — is the substance of HTML’s input surface. The discipline produces accessible, validated, mobile-friendly forms with substantial built-in functionality.