Browse Components

Validation

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.

Use an address where we can contact you.

View Code
HTML
<form class="stack" id="validation-basic" data-validate>
  <p data-validation-summary role="status">
  </p>
  <div class="field">
    <label for="contact-name">
      Name (required)
    </label>
    <input
      class="input"
      id="contact-name"
      name="contact-name"
      autocomplete="name"
      required
    >
  </div>
  <div class="field">
    <label for="contact-email">
      Email address (required)
    </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-result hidden>
  </p>
</form>
<script>
  addEventListener('DOMContentLoaded',()=>{const form=document.getElementById('validation-basic');const result=form.querySelector('[data-result]');form.addEventListener('submit',event=>event.preventDefault());form.addEventListener('validation:valid',event=>{event.preventDefault();result.hidden=false;result.textContent="The form is valid. Demo only; nothing was sent.";});form.addEventListener('input',()=>{result.hidden=true;result.textContent='';});form.addEventListener('validation:invalid',()=>{result.hidden=true;result.textContent='';});form.addEventListener('reset',()=>{result.hidden=true;result.textContent='';});});
</script>

Complete form

A project brief combining Input, Select, Number, Date Picker, Textarea, Switch and Checkbox. Try submitting it empty, correcting errors, and resetting the form.

Start a project

Tell us about your team and what you want to build. Required fields are marked.

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"
  id="validation-complete"
  data-validate
  style="width:100%;max-width:760px;margin-inline:auto;gap:32px"
>
  <div class="stack" style="gap:8px">
    <h3 style="margin:0">
      Start a project
    </h3>
    <p class="muted" style="margin:0">
      Tell us about your team and what you want to build. Required fields are marked.
    </p>
  </div>
  <p data-validation-summary role="status">
  </p>
  <fieldset class="stack" style="border:0;padding:0;margin:0;min-width:0;gap:24px">
    <legend style="font-weight:600;padding:0;margin-block-end:20px">
      Your details
    </legend>
    <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,240px),1fr));gap:24px;align-items:start">
      <div class="field">
        <label for="project-name">
          Full name (required)
        </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 (required)
        </label>
        <input
          class="input"
          id="project-email"
          name="project-email"
          type="email"
          autocomplete="email"
          required
        >
      </div>
    </div>
  </fieldset>
  <fieldset class="stack" style="border:0;padding:0;margin:0;min-width:0;gap:24px">
    <legend style="font-weight:600;padding:0;margin-block-end:20px">
      Project details
    </legend>
    <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,240px),1fr));gap:24px;align-items:start">
      <div class="field">
        <label for="project-type">
          Project type (required)
        </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 (required)
        </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 (required)
        </label>
        <input
          class="input"
          id="project-title"
          name="project-title"
          maxlength="120"
          required
        >
      </div>
      <div class="field">
        <label for="project-start">
          Start date (required)
        </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>
            <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="project-date-help">
          When would you like to get started?
        </p>
      </div>
    </div>
    <div class="field">
      <label for="project-description">
        Project brief (required)
      </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" style="border:0;padding:0;margin:0;min-width:0;gap:24px">
    <legend style="font-weight:600;padding:0;margin-block-end:20px">
      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 (required)
        </span>
      </label>
    </div>
  </fieldset>
  <div class="stack" style="gap:16px">
    <div class="cluster">
      <button class="btn" type="submit">
        Send Project Brief
      </button>
      <button class="btn secondary" type="reset">
        Reset
      </button>
    </div>
    <p class="muted" style="margin:0">
      Interactive demo only. Nothing is sent or saved.
    </p>
    <p role="status" data-result hidden style="margin:0;font-weight:600">
    </p>
  </div>
</form>
<script>
  addEventListener('DOMContentLoaded',()=>{const form=document.getElementById('validation-complete'),result=form.querySelector('[data-result]');form.addEventListener('submit',event=>event.preventDefault());form.addEventListener('validation:valid',event=>{event.preventDefault();result.hidden=false;result.textContent='Your project brief is ready. In a real app, it would now be sent to your server.';});for(const type of ['input','change','reset','validation:invalid'])form.addEventListener(type,()=>{result.hidden=true;result.textContent='';});});
</script>

Native rules

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

Choose 5–50 seats in increments of 5.

View Code
HTML
<form class="stack" id="validation-rules" data-validate>
  <p data-validation-summary role="status">
  </p>
  <div class="field">
    <label for="seats">
      Seats (required)
    </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 (required)
    </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 (required)
      </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-result hidden>
  </p>
</form>
<script>
  addEventListener('DOMContentLoaded',()=>{const form=document.getElementById('validation-rules');const result=form.querySelector('[data-result]');form.addEventListener('submit',event=>event.preventDefault());form.addEventListener('validation:valid',event=>{event.preventDefault();result.hidden=false;result.textContent="The form is valid. Demo only; nothing was sent.";});form.addEventListener('input',()=>{result.hidden=true;result.textContent='';});form.addEventListener('validation:invalid',()=>{result.hidden=true;result.textContent='';});form.addEventListener('reset',()=>{result.hidden=true;result.textContent='';});});
</script>

Custom rule

A synchronous rule can compare two fields. Check the form after changing either value.

View Code
HTML
<form class="stack" id="validation-custom" data-validate>
  <p data-validation-summary role="status">
  </p>
  <div class="field">
    <label for="new-email">
      Email address (required)
    </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 (required)
    </label>
    <input
      class="input"
      id="confirm-email"
      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-result hidden>
  </p>
</form>
<script>
  addEventListener('DOMContentLoaded',()=>{const form=document.getElementById('validation-custom');const result=form.querySelector('[data-result]');form.addEventListener('submit',event=>event.preventDefault());form.addEventListener('validation:valid',event=>{event.preventDefault();result.hidden=false;result.textContent="The form is valid. Demo only; nothing was sent.";});form.addEventListener('input',()=>{result.hidden=true;result.textContent='';});form.addEventListener('validation:invalid',()=>{result.hidden=true;result.textContent='';});form.addEventListener('reset',()=>{result.hidden=true;result.textContent='';});const email=form.querySelector('input[type=email]');const confirm=document.getElementById('confirm-email');layoutValidation.addRule(confirm,value=>value===email.value?'':'Enter the same email address in both fields.');});
</script>

Server errors

Present an application-supplied error with setError. This button simulates a response without a request.

Use 3–20 letters, numbers or underscores.

View Code
HTML
<form class="stack" id="validation-server" data-validate>
  <p data-validation-summary role="status">
  </p>
  <div class="field">
    <label for="username">
      Username (required)
    </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-result hidden>
  </p>
</form>
<script>
  addEventListener('DOMContentLoaded',()=>{const form=document.getElementById('validation-server');const result=form.querySelector('[data-result]');form.addEventListener('submit',event=>event.preventDefault());form.addEventListener('validation:valid',event=>{event.preventDefault();result.hidden=false;result.textContent="The form is valid. Demo only; nothing was sent.";});form.addEventListener('input',()=>{result.hidden=true;result.textContent='';});form.addEventListener('validation:invalid',()=>{result.hidden=true;result.textContent='';});form.addEventListener('reset',()=>{result.hidden=true;result.textContent='';});form.querySelector('[data-server]').addEventListener('click',()=>layoutValidation.setError(document.getElementById('username'),'This username is unavailable. Choose another name.'));});
</script>

Right to left

Translate error copy and summary messages while keeping native constraints.

View Code
HTML
<form
  class="stack"
  id="validation-hebrew"
  data-validate
  lang="he"
  dir="rtl"
  data-validation-summary="בדקו את השדות המסומנים ונסו שוב."
>
  <p data-validation-summary role="status">
  </p>
  <div class="field">
    <label for="he-name">
      שם (חובה)
    </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">
      כתובת אימייל (חובה)
    </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-result hidden>
  </p>
</form>
<script>
  addEventListener('DOMContentLoaded',()=>{const form=document.getElementById('validation-hebrew');const result=form.querySelector('[data-result]');form.addEventListener('submit',event=>event.preventDefault());form.addEventListener('validation:valid',event=>{event.preventDefault();result.hidden=false;result.textContent="הטופס תקין. זוהי הדגמה בלבד; לא נשלח מידע.";});form.addEventListener('input',()=>{result.hidden=true;result.textContent='';});form.addEventListener('validation:invalid',()=>{result.hidden=true;result.textContent='';});form.addEventListener('reset',()=>{result.hidden=true;result.textContent='';});});
</script>

Enhanced inputs

Password, OTP, Number, Autocomplete and Textarea share error timing, correction and reset. All fields 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="validation-controls" data-validate>
  <p data-validation-summary role="status">
  </p>
  <div class="field">
    <label for="account-password">
      Password
   (required)
    </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
   (required)
    </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
   (required)
    </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
   (required)
    </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
   (required)
    </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-result hidden>
  </p>
</form>
<script>
  addEventListener('DOMContentLoaded',()=>{const form=document.getElementById('validation-controls'),result=form.querySelector('[data-result]');form.addEventListener('submit',e=>e.preventDefault());form.addEventListener('validation:valid',e=>{e.preventDefault();result.hidden=false;result.textContent='Valid. Demo only; nothing was sent.';});for(const event of ['input','reset','validation:invalid'])form.addEventListener(event,()=>{result.hidden=true;result.textContent='';});});
</script>

Choices and slider

Related radio options share one error. Checkbox, Switch and Segmented retain their native semantics. The slider demonstrates a custom minimum of 25%.

Delivery frequency (required)
Billing period (required)
50%
View Code
HTML
<form class="stack" id="validation-choices" data-validate="">
  <p data-validation-summary="" role="status">
  </p>
  <fieldset class="radio-group">
    <legend>
      Delivery frequency
   (required)
    </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
   (required)
    </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 (required)
    </span>
  </label>
  <label class="switch">
    <input type="checkbox" role="switch" name="notifications" value="enabled" required="">
    <span>
      Enable account protection (required)
    </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="0" 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-result="" hidden="">
  </p>
</form>
<script>
  addEventListener('DOMContentLoaded',()=>{const form=document.getElementById('validation-choices'),result=form.querySelector('[data-result]');form.addEventListener('submit',e=>e.preventDefault());form.addEventListener('validation:valid',e=>{e.preventDefault();result.hidden=false;result.textContent='Valid. Demo only; nothing was sent.';});for(const event of ['input','reset','validation:invalid'])form.addEventListener(event,()=>{result.hidden=true;result.textContent='';});layoutValidation.addRule(form.querySelector('input[type=range]'),value=>Number(value)>=25?'':'Choose at least 25%.');});
</script>

Pickers and upload

Date range and compact Color Picker focus their visible controls. Upload rejection messages participate in validation. All fields 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="validation-pickers" data-validate>
  <p data-validation-summary role="status">
  </p>
  <fieldset class="stack" data-date-range="">
    <legend>
      Date range
    </legend>
    <div class="datepicker-fields">
      <div class="field">
        <label for="start-date">
          Start date
        </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
        </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
   (required)
    </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
     (required)
      </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-result hidden>
  </p>
</form>
<script>
  addEventListener('DOMContentLoaded',()=>{const form=document.getElementById('validation-pickers'),result=form.querySelector('[data-result]');form.addEventListener('submit',e=>e.preventDefault());form.addEventListener('validation:valid',e=>{e.preventDefault();result.hidden=false;result.textContent='Valid. Demo only; nothing was sent.';});for(const event of ['input','reset','validation:invalid'])form.addEventListener(event,()=>{result.hidden=true;result.textContent='';});});
</script>

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/validation.css">
<script defer src="/library/assets/validation.js"></script>

14,608 bytes CSS including base · 7,818 bytes runtime JavaScript

Usage

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.

Timing and submission

After editing and leaving a field, show its error. Submit validates all eligible controls and focuses the first error. Listen to cancelable validation:valid and preventDefault to handle submission yourself. A valid form otherwise submits normally. Demo listeners explicitly prevent submission.

Custom and server errors

Use layoutValidation.addRule(control,(value,control,form)=>message) for synchronous rules. Return an empty string when valid. Call layoutValidation.setError(control,message) for a server response. Clear it with an empty string; editing clears that server error automatically. Applications own asynchronous request cancellation and stale responses.

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-validation-summary text for a translated summary. Hebrew RTL is demonstrated above.

Lifecycle

Use layoutValidation.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.

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-validation-target points to a visible proxy by ID; data-validation-field marks its visible error container. These hooks also support custom composite controls.

Selective installation

Validation itself needs only Field, Button and Validation assets for its basic example. The compatibility examples load additional form components on this documentation page only. Include the assets for the controls you actually use; selecting Validation 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.