Browse Components

Validate

Shared form validation with accessible errors and native HTML rules.

Basic

Edit a field and leave it, or check the form, to see accessible inline errors. Nothing is sent.

Fields marked * are required.

Use an address where we can contact you.

View Code
HTML
<form class="stack" id="validate-basic" data-validate="check">
  <p class="muted">
    Fields marked * are required.
  </p>
  <p data-validate-summary role="status">
  </p>
  <div class="field">
    <label for="contact-name">
      Name
      <span class="required-marker" aria-hidden="true">
        *
      </span>
    </label>
    <input
      class="input"
      id="contact-name"
      name="contact-name"
      autocomplete="name"
      required
    >
  </div>
  <div class="field">
    <label for="contact-email">
      Email Address
      <span class="required-marker" aria-hidden="true">
        *
      </span>
    </label>
    <input
      class="input"
      id="contact-email"
      name="contact-email"
      type="email"
      autocomplete="email"
      required
      aria-describedby="contact-email-help"
    >
    <p class="muted" id="contact-email-help">
      Use an address where we can contact you.
    </p>
  </div>
  <div class="cluster">
    <button class="btn" type="submit">
      Check Form
    </button>
    <button class="btn secondary" type="reset">
      Reset
    </button>
  </div>
  <p class="muted" role="status" data-validate-success hidden>
    The form is valid. Nothing was sent.
  </p>
</form>

Complete form

A complete project brief using shared form components and HTML validation rules. No per-form JavaScript. This example uses data-validate="check" to validate and show confirmation without sending data.

Start a project

Tell us about your team and what you want to build. Fields marked * are required.

Your Details
Project Details

When would you like to get started?

Describe your goals and who you’re building for. At least 20 characters.

Preferences

Interactive demo only. Nothing is sent or saved.

View Code
HTML
<form
  class="stack w-full max-w-form mx-auto gap-xl"
  id="validate-complete"
  data-validate="check"
>
  <div class="stack gap-sm">
    <h3 class="m-0">
      Start a project
    </h3>
    <p class="muted m-0">
      Tell us about your team and what you want to build. Fields marked * are required.
    </p>
  </div>
  <p data-validate-summary="" role="status">
  </p>
  <fieldset class="stack form-section">
    <legend>
      Your Details
    </legend>
    <div class="grid items-start">
      <div class="field">
        <label for="project-name">
          Full Name
          <span class="required-marker" aria-hidden="true">
            *
          </span>
        </label>
        <input
          class="input"
          id="project-name"
          name="project-name"
          autocomplete="name"
          maxlength="100"
          required=""
        >
      </div>
      <div class="field">
        <label for="project-email">
          Work Email
          <span class="required-marker" aria-hidden="true">
            *
          </span>
        </label>
        <input
          class="input"
          id="project-email"
          name="project-email"
          type="email"
          autocomplete="email"
          required=""
        >
      </div>
    </div>
  </fieldset>
  <fieldset class="stack form-section">
    <legend>
      Project Details
    </legend>
    <div class="grid items-start">
      <div class="field">
        <label for="project-type">
          Project Type
          <span class="required-marker" aria-hidden="true">
            *
          </span>
        </label>
        <div class="select-control">
          <select id="project-type" name="type" required="">
            <option value="">
              Choose a project type
            </option>
            <option value="website">
              Website
            </option>
            <option value="app">
              Web application
            </option>
            <option value="mobile">
              Mobile application
            </option>
            <option value="system">
              Design system
            </option>
          </select>
        </div>
      </div>
      <div class="field">
        <label for="project-team">
          Team Size
          <span class="required-marker" aria-hidden="true">
            *
          </span>
        </label>
        <div class="number" data-number="">
          <button
            class="number-step"
            type="button"
            data-number-step="-1"
            aria-label="Decrease Team Size"
            aria-controls="project-team"
            hidden=""
          >
            <svg
              aria-hidden="true"
              focusable="false"
              xmlns="http://www.w3.org/2000/svg"
              viewbox="0 0 24 24"
              fill="none"
              stroke="currentColor"
              stroke-width="1.75"
              stroke-linecap="round"
              stroke-linejoin="round"
            >
              <path d="M5 12h14">
              </path>
            </svg>
          </button>
          <input
            class="input"
            id="project-team"
            name="project-team"
            type="number"
            min="1"
            max="100"
            step="1"
            value="1"
            inputmode="numeric"
            required=""
          >
          <button
            class="number-step"
            type="button"
            data-number-step="1"
            aria-label="Increase Team Size"
            aria-controls="project-team"
            hidden=""
          >
            <svg
              aria-hidden="true"
              focusable="false"
              xmlns="http://www.w3.org/2000/svg"
              viewbox="0 0 24 24"
              fill="none"
              stroke="currentColor"
              stroke-width="1.75"
              stroke-linecap="round"
              stroke-linejoin="round"
            >
              <path d="M12 5v14M5 12h14">
              </path>
            </svg>
          </button>
        </div>
      </div>
      <div class="field">
        <label for="project-title">
          Project Name
          <span class="required-marker" aria-hidden="true">
            *
          </span>
        </label>
        <input
          class="input"
          id="project-title"
          name="project-title"
          maxlength="120"
          required=""
        >
      </div>
      <div class="field">
        <label for="project-start">
          Start Date
          <span class="required-marker" aria-hidden="true">
            *
          </span>
        </label>
        <div class="datepicker" data-datepicker="">
          <input
            class="input"
            type="date"
            id="project-start"
            name="start"
            required=""
            aria-describedby="project-date-help"
          >
          <button
            type="button"
            data-datepicker-open=""
            aria-label="Choose a date"
            hidden=""
          >
            <span class="icon" aria-hidden="true" data-icon="calendar">
            </span>
          </button>
        </div>
        <p class="muted" id="project-date-help">
          When would you like to get started?
        </p>
      </div>
    </div>
    <div class="field">
      <label for="project-description">
        Project Brief
        <span class="required-marker" aria-hidden="true">
          *
        </span>
      </label>
      <textarea
        class="input textarea"
        id="project-description"
        name="description"
        rows="4"
        required=""
        minlength="20"
        maxlength="2000"
        aria-describedby="project-description-help"
        data-error-too-short="Add at least 20 characters about your project."
      >
      </textarea>
      <p class="muted" id="project-description-help">
        Describe your goals and who you’re building for. At least 20 characters.
      </p>
    </div>
  </fieldset>
  <fieldset class="stack form-section">
    <legend>
      Preferences
    </legend>
    <label class="switch">
      <input type="checkbox" role="switch" name="updates" value="email" checked="">
      <span>
        Email me project updates
      </span>
    </label>
    <div class="field">
      <label class="checkbox">
        <input
          type="checkbox"
          name="consent"
          value="yes"
          required=""
          data-error-value-missing="Confirm that the team can contact you about this project."
        >
        <span>
          The team can contact me about this project
          <span class="required-marker" aria-hidden="true">
            *
          </span>
        </span>
      </label>
    </div>
  </fieldset>
  <div class="stack gap-md">
    <div class="cluster">
      <button class="btn" type="submit">
        Send Project Brief
      </button>
      <button class="btn secondary" type="reset">
        Reset
      </button>
    </div>
    <p class="muted m-0">
      Interactive demo only. Nothing is sent or saved.
    </p>
    <p role="status" data-validate-success="" hidden="" class="m-0 font-semibold">
      Project brief ready. Demo only; nothing was sent.
    </p>
  </div>
</form>

Native rules

Required choices, numeric limits and step increments use the same shared behavior.

Fields marked * are required.

Choose 5–50 seats in increments of 5.

View Code
HTML
<form class="stack" id="validate-rules" data-validate="check">
  <p class="muted">
    Fields marked * are required.
  </p>
  <p data-validate-summary role="status">
  </p>
  <div class="field">
    <label for="seats">
      Seats
      <span class="required-marker" aria-hidden="true">
        *
      </span>
    </label>
    <input
      class="input"
      id="seats"
      name="seats"
      type="number"
      min="5"
      max="50"
      step="5"
      required
      aria-describedby="seats-help"
    >
    <p class="muted" id="seats-help">
      Choose 5–50 seats in increments of 5.
    </p>
  </div>
  <div class="field">
    <label for="plan">
      Plan
      <span class="required-marker" aria-hidden="true">
        *
      </span>
    </label>
    <div class="select-control">
      <select id="plan" name="plan" required>
        <option value="">
          Choose a plan
        </option>
        <option value="personal">
          Personal
        </option>
        <option value="team">
          Team
        </option>
      </select>
    </div>
  </div>
  <div class="field">
    <label class="checkbox">
      <input type="checkbox" name="agreement" required>
      <span>
        I agree to the demo terms
        <span class="required-marker" aria-hidden="true">
          *
        </span>
      </span>
    </label>
  </div>
  <div class="cluster">
    <button class="btn" type="submit">
      Check Form
    </button>
    <button class="btn secondary" type="reset">
      Reset
    </button>
  </div>
  <p class="muted" role="status" data-validate-success hidden>
    The form is valid. Nothing was sent.
  </p>
</form>

Matching fields

Use data-match to require matching fields, such as email or password confirmation. No custom script is needed.

Fields marked * are required.

View Code
HTML
<form class="stack" id="validate-custom" data-validate="check">
  <p class="muted">
    Fields marked * are required.
  </p>
  <p data-validate-summary role="status">
  </p>
  <div class="field">
    <label for="new-email">
      Email Address
      <span class="required-marker" aria-hidden="true">
        *
      </span>
    </label>
    <input
      class="input"
      id="new-email"
      name="new-email"
      type="email"
      required
      autocomplete="email"
    >
  </div>
  <div class="field">
    <label for="confirm-email">
      Confirm Email
      <span class="required-marker" aria-hidden="true">
        *
      </span>
    </label>
    <input
      class="input"
      id="confirm-email"
      data-match="new-email"
      data-error-match="Enter the same email address in both fields."
      name="confirm-email"
      type="email"
      required
      autocomplete="off"
    >
  </div>
  <div class="cluster">
    <button class="btn" type="submit">
      Check Form
    </button>
    <button class="btn secondary" type="reset">
      Reset
    </button>
  </div>
  <p class="muted" role="status" data-validate-success hidden>
    The form is valid. Nothing was sent.
  </p>
</form>

Server errors

Pass a field-name to message object to form.validate.setErrors. Errors appear beside the matching controls and focus moves to the first invalid field. This button simulates a server response; nothing is sent.

Fields marked * are required.

Use 3–20 letters, numbers or underscores.

View Code
HTML
<form class="stack" id="validate-server" data-validate="check">
  <p class="muted">
    Fields marked * are required.
  </p>
  <p data-validate-summary role="status">
  </p>
  <div class="field">
    <label for="username">
      Username
      <span class="required-marker" aria-hidden="true">
        *
      </span>
    </label>
    <input
      class="input"
      id="username"
      name="username"
      required
      pattern="[a-zA-Z0-9_]{3,20}"
      aria-describedby="username-help"
    >
    <p class="muted" id="username-help">
      Use 3–20 letters, numbers or underscores.
    </p>
  </div>
  <button class="btn secondary" type="button" data-server>
    Simulate Unavailable Name
  </button>
  <div class="cluster">
    <button class="btn" type="submit">
      Check Form
    </button>
    <button class="btn secondary" type="reset">
      Reset
    </button>
  </div>
  <p class="muted" role="status" data-validate-success hidden>
    The form is valid. Nothing was sent.
  </p>
</form>
Server response example
addEventListener('DOMContentLoaded', () => {
  const form = document.getElementById('validate-server');
  form.querySelector('[data-server]').addEventListener('click', () => {
    form.validate.setErrors({ username: 'This username is already taken.' });
  });
});

Right to left

Translate error copy and summary messages while keeping native constraints.

שדות המסומנים ב־* הם שדות חובה.

View Code
HTML
<form
  class="stack"
  id="validate-hebrew"
  data-validate="check"
  lang="he"
  dir="rtl"
  data-validate-summary="בדקו את השדות המסומנים ונסו שוב."
>
  <p class="muted">
    שדות המסומנים ב־* הם שדות חובה.
  </p>
  <p data-validate-summary role="status">
  </p>
  <div class="field">
    <label for="he-name">
      שם
      <span class="required-marker" aria-hidden="true">
        *
      </span>
    </label>
    <input
      class="input"
      id="he-name"
      name="he-name"
      required
      autocomplete="name"
      data-error-value-missing="הזינו את השם שלכם."
    >
  </div>
  <div class="field">
    <label for="he-email">
      כתובת אימייל
      <span class="required-marker" aria-hidden="true">
        *
      </span>
    </label>
    <input
      class="input"
      id="he-email"
      name="he-email"
      required
      type="email"
      autocomplete="email"
      data-error-value-missing="הזינו כתובת אימייל."
      data-error-type-mismatch="הזינו כתובת אימייל תקינה."
    >
  </div>
  <div class="cluster">
    <button class="btn" type="submit">
      בדיקת הטופס
    </button>
    <button class="btn secondary" type="reset">
      איפוס
    </button>
  </div>
  <p class="muted" role="status" data-validate-success hidden>
    הטופס תקין. זוהי הדגמה בלבד; לא נשלח מידע.
  </p>
</form>

Enhanced inputs

Password, OTP, Number, Autocomplete and Textarea share error timing, correction and reset. All fields are required.

Fields marked * are required.

Use a sample password here. This example does not send or save it.

Enter the 6-digit code sent to your email.

Choose a suggestion or enter another city.

Include goals, context, and any important details.

View Code
HTML
<form class="stack" id="validate-controls" data-validate="check">
  <p class="muted">
    Fields marked * are required.
  </p>
  <p data-validate-summary role="status">
  </p>
  <div class="field">
    <label for="account-password">
      Password
      <span class="required-marker" aria-hidden="true">
        *
      </span>
    </label>
    <div class="password" data-password="">
      <input
        class="input"
        id="account-password"
        type="password"
        name="password"
        autocomplete="current-password"
        autocapitalize="none"
        spellcheck="false"
        aria-describedby="password-help"
        required=""
      >
      <button
        type="button"
        aria-controls="account-password"
        aria-label="Show Password"
        data-password-toggle=""
        data-show="Show Password"
        data-hide="Hide Password"
        hidden=""
      >
        <svg
          class="icon password-eye"
          aria-hidden="true"
          focusable="false"
          xmlns="http://www.w3.org/2000/svg"
          viewbox="0 0 24 24"
          fill="none"
          stroke="currentColor"
          stroke-width="1.75"
          stroke-linecap="round"
          stroke-linejoin="round"
        >
          <path d="M2.5 12s3.5-6 9.5-6 9.5 6 9.5 6-3.5 6-9.5 6-9.5-6-9.5-6Z">
          </path>
          <circle cx="12" cy="12" r="2.5">
          </circle>
        </svg>
        <svg
          class="icon password-eye-off"
          aria-hidden="true"
          focusable="false"
          xmlns="http://www.w3.org/2000/svg"
          viewbox="0 0 24 24"
          fill="none"
          stroke="currentColor"
          stroke-width="1.75"
          stroke-linecap="round"
          stroke-linejoin="round"
        >
          <path d="m3 3 18 18M9 6.5A10 10 0 0 1 12 6c6 0 9.5 6 9.5 6a17 17 0 0 1-3 3.5M6 7.5A18 18 0 0 0 2.5 12s3.5 6 9.5 6a11 11 0 0 0 4-1M10.2 10.2a2.5 2.5 0 0 0 3.6 3.6">
          </path>
        </svg>
      </button>
    </div>
    <p id="password-help" class="muted">
      Use a sample password here. This example does not send or save it.
    </p>
  </div>
  <div class="field">
    <label for="verification-code">
      Verification Code
      <span class="required-marker" aria-hidden="true">
        *
      </span>
    </label>
    <input
      class="input otp"
      data-otp=""
      id="verification-code"
      name="code"
      type="text"
      inputmode="numeric"
      autocomplete="one-time-code"
      maxlength="6"
      pattern="[0-9]{6}"
      required=""
      spellcheck="false"
      autocapitalize="off"
      dir="ltr"
      aria-describedby="verification-help"
    >
    <p class="muted" id="verification-help">
      Enter the 6-digit code sent to your email.
    </p>
  </div>
  <div class="field">
    <label for="quantity">
      Quantity
      <span class="required-marker" aria-hidden="true">
        *
      </span>
    </label>
    <div class="number" data-number="">
      <button
        class="number-step"
        type="button"
        data-number-step="-1"
        aria-label="Decrease Quantity"
        aria-controls="quantity"
        hidden=""
      >
        <svg
          aria-hidden="true"
          focusable="false"
          xmlns="http://www.w3.org/2000/svg"
          viewbox="0 0 24 24"
          fill="none"
          stroke="currentColor"
          stroke-width="1.75"
          stroke-linecap="round"
          stroke-linejoin="round"
        >
          <path d="M5 12h14">
          </path>
        </svg>
      </button>
      <input
        class="input"
        id="quantity"
        name="quantity"
        type="number"
        min="0"
        max="20"
        step="1"
        inputmode="decimal"
        required=""
      >
      <button
        class="number-step"
        type="button"
        data-number-step="1"
        aria-label="Increase Quantity"
        aria-controls="quantity"
        hidden=""
      >
        <svg
          aria-hidden="true"
          focusable="false"
          xmlns="http://www.w3.org/2000/svg"
          viewbox="0 0 24 24"
          fill="none"
          stroke="currentColor"
          stroke-width="1.75"
          stroke-linecap="round"
          stroke-linejoin="round"
        >
          <path d="M12 5v14M5 12h14">
          </path>
        </svg>
      </button>
    </div>
  </div>
  <div class="field">
    <label for="destination">
      Destination
      <span class="required-marker" aria-hidden="true">
        *
      </span>
    </label>
    <input
      class="input"
      id="destination"
      name="destination"
      data-autocomplete=""
      list="destinations"
      placeholder="Search cities"
      aria-describedby="destination-help"
      required=""
    >
    <datalist id="destinations">
      <option value="Amsterdam">
      </option>
      <option value="Barcelona">
      </option>
      <option value="Berlin">
      </option>
      <option value="Copenhagen">
      </option>
      <option value="Lisbon">
      </option>
      <option value="London">
      </option>
      <option value="Madrid">
      </option>
      <option value="Paris">
      </option>
      <option value="Rome">
      </option>
      <option value="Stockholm">
      </option>
    </datalist>
    <p class="field-help" id="destination-help">
      Choose a suggestion or enter another city.
    </p>
  </div>
  <div class="field">
    <label for="project-notes">
      Project Notes
      <span class="required-marker" aria-hidden="true">
        *
      </span>
    </label>
    <textarea
      class="input textarea"
      id="project-notes"
      name="notes"
      rows="4"
      aria-describedby="notes-help"
      placeholder="What should the team know?"
      required=""
    >
    </textarea>
    <p id="notes-help" class="muted">
      Include goals, context, and any important details.
    </p>
  </div>
  <div class="cluster">
    <button class="btn" type="submit">
      Check Form
    </button>
    <button class="btn secondary" type="reset">
      Reset
    </button>
  </div>
  <p role="status" class="muted" data-validate-success hidden>
    The form is valid. Nothing was sent.
  </p>
</form>

Choices and slider

Required choices and a slider with a native minimum of 25%. Each control uses the shared validation behavior.

Fields marked * are required.

Delivery Frequency
Billing Period
50%
View Code
HTML
<form class="stack" id="validate-choices" data-validate="check" "">
  <p class="muted">
    Fields marked * are required.
  </p>
  <p data-validate-summary="" role="status">
  </p>
  <fieldset class="radio-group">
    <legend>
      Delivery Frequency
      <span class="required-marker" aria-hidden="true">
        *
      </span>
    </legend>
    <label class="radio">
      <input type="radio" name="frequency" value="daily" required="">
      <span>
        Daily
      </span>
    </label>
    <label class="radio">
      <input type="radio" name="frequency" value="weekly" required="">
      <span>
        Weekly
      </span>
    </label>
    <label class="radio">
      <input type="radio" name="frequency" value="monthly" required="">
      <span>
        Monthly
      </span>
    </label>
  </fieldset>
  <fieldset class="segmented">
    <legend>
      Billing Period
      <span class="required-marker" aria-hidden="true">
        *
      </span>
    </legend>
    <div class="segmented-options">
      <label>
        <input type="radio" name="billing-period" value="monthly" required="">
        <span>
          Monthly
        </span>
      </label>
      <label>
        <input type="radio" name="billing-period" value="yearly" required="">
        <span>
          Yearly
        </span>
      </label>
    </div>
  </fieldset>
  <label class="checkbox">
    <input type="checkbox" name="updates" value="email" required="">
    <span>
      I agree to the demo terms
      <span class="required-marker" aria-hidden="true">
        *
      </span>
    </span>
  </label>
  <label class="switch">
    <input type="checkbox" role="switch" name="notifications" value="enabled" required="">
    <span>
      Enable account protection
      <span class="required-marker" aria-hidden="true">
        *
      </span>
    </span>
  </label>
  <div class="slider-field">
    <div class="slider-heading">
      <label for="volume">
        Volume
      </label>
      <output for="volume" data-unit="%">
        50%
      </output>
    </div>
    <input id="volume" name="volume" type="range" min="25" max="100" step="1" value="50">
  </div>
  <div class="cluster">
    <button class="btn" type="submit">
      Check Form
    </button>
    <button class="btn secondary" type="reset">
      Reset
    </button>
  </div>
  <p role="status" class="muted" data-validate-success hidden="">
    The form is valid. Nothing was sent.
  </p>
</form>

Pickers and upload

Date range and compact Color Picker focus their visible controls. Upload rejection messages participate in validation. All fields are required.

Fields marked * are required.

Date Range

Choose a start date in any month or year.

Choose an end date on or after the start date.

Choose a color or enter a six-digit hex value, such as #294cda.

PDF, PNG, or JPEG. Up to 5 files, 5 MB each.

View Code
HTML
<form class="stack" id="validate-pickers" data-validate="check">
  <p class="muted">
    Fields marked * are required.
  </p>
  <p data-validate-summary role="status">
  </p>
  <fieldset class="stack" data-date-range="">
    <legend>
      Date Range
      <span class="required-marker" aria-hidden="true">
        *
      </span>
    </legend>
    <div class="datepicker-fields">
      <div class="field">
        <label for="start-date">
          Start Date
          <span class="required-marker" aria-hidden="true">
            *
          </span>
        </label>
        <div class="datepicker" data-datepicker="">
          <input
            class="input"
            type="date"
            data-range-start=""
            id="start-date"
            name="start"
            aria-describedby="date-help"
            required=""
          >
          <button
            type="button"
            data-datepicker-open=""
            aria-label="Choose a date"
            hidden=""
          >
            <svg
              class="icon"
              aria-hidden="true"
              focusable="false"
              xmlns="http://www.w3.org/2000/svg"
              viewbox="0 0 24 24"
              fill="none"
              stroke="currentColor"
              stroke-width="1.75"
              stroke-linecap="round"
              stroke-linejoin="round"
            >
              <rect x="3" y="5" width="18" height="16" rx="2">
              </rect>
              <path d="M7 3v4m10-4v4M3 10h18M7 14h2m4 0h2m-8 3h2">
              </path>
            </svg>
          </button>
        </div>
        <p class="muted" id="date-help">
          Choose a start date in any month or year.
        </p>
      </div>
      <div class="field">
        <label for="end-date">
          End Date
          <span class="required-marker" aria-hidden="true">
            *
          </span>
        </label>
        <div class="datepicker" data-datepicker="">
          <input
            class="input"
            type="date"
            data-range-end=""
            id="end-date"
            name="end"
            aria-describedby="end-help range-error"
            required=""
          >
          <button
            type="button"
            data-datepicker-open=""
            aria-label="Choose a date"
            hidden=""
          >
            <svg
              class="icon"
              aria-hidden="true"
              focusable="false"
              xmlns="http://www.w3.org/2000/svg"
              viewbox="0 0 24 24"
              fill="none"
              stroke="currentColor"
              stroke-width="1.75"
              stroke-linecap="round"
              stroke-linejoin="round"
            >
              <rect x="3" y="5" width="18" height="16" rx="2">
              </rect>
              <path d="M7 3v4m10-4v4M3 10h18M7 14h2m4 0h2m-8 3h2">
              </path>
            </svg>
          </button>
        </div>
        <p class="muted" id="end-help">
          Choose an end date on or after the start date.
        </p>
      </div>
    </div>
    <p id="range-error" data-range-error="" aria-live="polite">
    </p>
  </fieldset>
  <div class="field">
    <label for="brand-color">
      Brand Color
      <span class="required-marker" aria-hidden="true">
        *
      </span>
    </label>
    <div class="color-picker" data-color-picker="" data-swatch-only>
      <button
        type="button"
        data-color-swatch=""
        aria-label="Choose Brand Color"
        hidden=""
      >
      </button>
      <input
        class="input"
        dir="ltr"
        id="brand-color"
        name="brand"
        pattern="#[0-9a-fA-F]{6}"
        maxlength="7"
        spellcheck="false"
        autocomplete="off"
        required=""
        aria-describedby="brand-color-help"
      >
    </div>
    <p class="muted" id="brand-color-help">
      Choose a color or enter a six-digit hex value, such as #294cda.
    </p>
  </div>
  <div class="file-upload" data-file-upload="" data-max-size="5242880" data-max-files="5">
    <div class="upload-dropzone">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewbox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="1.75"
        stroke-linecap="round"
        stroke-linejoin="round"
        class="icon"
        aria-hidden="true"
        focusable="false"
      >
        <path d="M12 15V3m-5 5 5-5 5 5M4 16v3a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-3">
        </path>
      </svg>
      <label for="attachments">
        Choose Files or Drop Them Here
        <span class="required-marker" aria-hidden="true">
          *
        </span>
      </label>
      <p id="upload-help">
        PDF, PNG, or JPEG. Up to 5 files, 5 MB each.
      </p>
      <input
        id="attachments"
        name="attachments"
        type="file"
        accept=".pdf,image/png,image/jpeg"
        multiple=""
        aria-describedby="upload-help"
        required=""
      >
    </div>
  </div>
  <div class="cluster">
    <button class="btn" type="submit">
      Check Form
    </button>
    <button class="btn secondary" type="reset">
      Reset
    </button>
  </div>
  <p role="status" class="muted" data-validate-success hidden>
    The form is valid. Nothing was sent.
  </p>
</form>

Installation

Download these files into the matching paths in your project, or use Create to assemble a kit. Include each dependency once. Do not also load app.css.

Add to your page
<link rel="stylesheet" href="/library/assets/base.css">
<link rel="stylesheet" href="/library/assets/field.css">
<link rel="stylesheet" href="/library/assets/button.css">
<link rel="stylesheet" href="/library/assets/validate.css">
<link rel="stylesheet" href="/library/assets/icon.css">
<script defer src="/library/assets/validate.js"></script>
<script defer src="/library/assets/icon.js"></script>
<script defer src="/library/icons/validate.js"></script>

22,915 bytes CSS including base · 13,799 bytes runtime JavaScript

Usage

HTML-only setup

Load the shared assets once. Add data-validate to a form with action and method for normal submission, or data-validate="check" to validate without submitting. Add a hidden element with data-validate-success for local confirmation; the component shows it on a valid check and hides it on edits, invalid checks and reset. This confirms validity, not server delivery. No per-form script is needed.

Choose your rules

Use required, type="email", type="url", min, max, step, minlength, maxlength and pattern. Use data-match="other-field-id" for confirmation fields in the same form and data-error-match for the message. Optional empty fields remain valid unless their rules require otherwise.

Submission and custom logic

Normal data-validate forms submit natively once valid. Listen to cancelable validate:valid and preventDefault only when your app needs custom submission. Check mode never submits. A canceled validate:valid event suppresses local confirmation. Client-side validation does not replace server validation.

Native foundation

Use required, type, min, max, step, minlength, maxlength and pattern on native controls. Group each label/control/help in .field. Without JavaScript, native browser validation remains available. Client validation does not replace server validation.

Server responses

Call form.validate.setErrors({ username: "This username is already taken." }) with native field names as keys and strings as messages. Each call replaces prior server errors; an empty object clears them without clearing native rules. Errors use the existing inline feedback, radio/date-range grouping and visible focus targets. Focus moves to the first applicable field in form order; pass {focus:false} as the second argument to keep focus. Unknown names or non-string messages throw before changing errors. Disabled, readonly and ignored controls do not show errors. Editing clears the affected server error; reset clears all feedback. Your application owns requests and stale-response handling.

Custom rules

Use layoutValidate.addRule(control,(value,control,form)=>message) for synchronous rules. Return an empty string when valid. The existing layoutValidate.setError(control,message) API remains available for individual controls; layoutValidate.setErrors(form,errors,options) is the form-level alternative.

Localization

The default messages use the browser locale. Override with data-error or a constraint-specific attribute such as data-error-value-missing. Set the form data-validate-summary text for a translated summary. Hebrew RTL is demonstrated above.

Lifecycle

Use layoutValidate.refresh(root) after mounting and destroy(form) before removal. validate(form,{focus:false}) performs an explicit validation without moving focus. Native form reset clears presented errors. Programmatic value updates should dispatch input when live feedback is needed. After enhancement, form.validate exposes the same form-scoped API. Destroy restores any prior validate property.

Composite controls

Password, OTP, Number, Autocomplete, Select, Date Picker, Color Picker, Upload, Textarea, Checkbox, Radio, Switch, Segmented and Slider use their native form value source. Date ranges and compact colors provide visible validation targets. Sliders always have a native value; use a custom rule for application-specific requirements. Upload rejects invalid files and reports their rejection through native custom validity until a new selection, removal or reset.

Feedback ownership

Within enhanced forms, shared validation owns inline errors. Auxiliary picker and search inputs are excluded. data-validate-target points to a visible proxy by ID; data-validate-field marks its visible error container. These hooks also support custom composite controls.

Selective installation

Include the shared Validate assets plus the assets for controls used in your form. Selecting Validate in Create does not bundle every form component.

Building with AI? The AI Manifest has these notes and the full contract. See the AI Guide.