Advanced Input Types & Custom Controls

Reviewed & published by Brayan K

HTML5 input types like date, color, range, and email give the browser the right keyboard, picker, and built-in validation for each kind of data, so you write less JavaScript and users get a better experience.

Part of the free HTML & CSS course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

💡 Think of It Like This

HTML5 input types are like specialised tools. A text input is a Swiss Army knife — works for everything but nothing perfectly. A type="date" input is a calendar. A type="color" is a paint palette. Using the right input type gives users the best native controls for each data type.

Input TypeWhat It ShowsMobile Keyboard
rangeSlider controlN/A
colorColor picker popupN/A
dateCalendar date pickerDate wheel
timeTime selectorTime wheel
datetime-localDate + time combinedDate + time
searchSearchable text with clear buttonSearch keyboard
telPhone number inputNumeric pad
urlURL input with validation.com keyboard

HTML5 Input Types Gallery

Try every HTML5 input type in one interactive demo.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>HTML5 Input Types</title>
  <style>
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body { background: #0f172a; font-family: system-ui, sans-serif; color: #e5e7eb; padding: 30px; }

    .grid {
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
      gap: 15px;
    }

    .input-card {
      background: #1e293b;
      padding: 15px;
      border-radius: 8px;
    }

    label {
      display: block;
      font-size: 12px;
      color: #3b82f6;
      font-weight: 600;
      text-transform: uppercase;
      letter-spacing: 0.5px;
      margin-bottom: 8px;
    }

    .type-tag {
      font-size: 11px;
      background: #334155;
      padding: 2px 6px;
      border-radius: 4px;
      color: #94a3b8;
    }

    input, select {
      width: 100%;
      padding: 8px 10px;
      background: #334155;
      border: 1px solid #475569;
      border-radius: 6px;
      color: white;
      font-size: 14px;
    }

    input[type="color"] {
      height: 40px;
      padding: 4px;
      cursor: pointer;
    }

    input[type="range"] {
      padding: 0;
      cursor: pointer;
    }

    .output {
      font-size: 12px;
      color: #94a3b8;
      margin-top: 5px;
    }

    h1 { margin-bottom: 20px; }
  </style>
</head>
<body>
  <h1>🎛️ HTML5 Input Types</h1>
  <div class="grid">
    <div class="input-card">
      <label>Range <span class="type-tag">type="range"</span></label>
      <input type="range" min="0" max="100" value="50" oninput="this.nextElementSibling.textContent='Value: '+this.value">
      <p class="output">Value: 50</p>
    </div>

    <div class="input-card">
      <label>Color <span class="type-tag">type="color"</span></label>
      <input type="color" value="#3b82f6" oninput="this.nextElementSibling.textContent=this.value">
      <p class="output">#3b82f6</p>
    </div>

    <div class="input-card">
      <label>Date <span class="type-tag">type="date"</span></label>
      <input type="date">
    </div>

    <div class="input-card">
      <label>Time <span class="type-tag">type="time"</span></label>
      <input type="time">
    </div>

    <div class="input-card">
      <label>DateTime <span class="type-tag">type="datetime-local"</span></label>
      <input type="datetime-local">
    </div>

    <div class="input-card">
      <label>Search <span class="type-tag">type="search"</span></label>
      <input type="search" placeholder="Search...">
    </div>

    <div class="input-card">
      <label>Tel <span class="type-tag">type="tel"</span></label>
      <input type="tel" placeholder="+1 (555) 000-0000">
    </div>

    <div class="input-card">
      <label>URL <span class="type-tag">type="url"</span></label>
      <input type="url" placeholder="https://example.com">
    </div>

    <div class="input-card">
      <label>Number <span class="type-tag">type="number"</span></label>
      <input type="number" min="0" max="999" step="1" value="42">
    </div>

    <div class="input-card">
      <label>Month <span class="type-tag">type="month"</span></label>
      <input type="month">
    </div>

    <div class="input-card">
      <label>Week <span class="type-tag">type="week"</span></label>
      <input type="week">
    </div>

    <div class="input-card">
      <label>With Datalist <span class="type-tag">list="..."</span></label>
      <input list="frameworks" placeholder="Type a framework...">
      <datalist id="frameworks">
        <option value="React">
        <option value="Vue">
        <option value="Angular">
        <option value="Svelte">
        <option value="Solid">
      </datalist>
    </div>
  </div>
  <!-- ✅ Expected result, measured in a real browser:
     .grid -> display: grid
     label -> display: block
     input[type="color"] -> cursor: pointer
     input[type="range"] -> cursor: pointer
     .input-card -> count: 12
  -->
</body>
</html>
The page this code makes: Interactive gallery of every modern input type
What this code shows in a browser window 720 pixels wide.

🧠 Worked Example — a form the browser validates for you

The gallery above shows what each type looks like. This example shows what each type does — because the real payoff of picking the right type is that the browser starts checking the data for you, with no JavaScript at all. Read the comments, then run it and try to submit rubbish.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Worked Example: A form the browser validates</title>
  <style>
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body { background:#0f172a; color:#e5e7eb; font-family:system-ui,sans-serif; padding:24px; }
    form { max-width: 420px; background:#1e293b; padding:20px; border-radius:12px; display:grid; gap:14px; }
    label { display:block; font-size:.78rem; text-transform:uppercase; letter-spacing:.5px; color:#60a5fa; font-weight:700; margin-bottom:6px; }
    input, select { width:100%; padding:9px 10px; background:#334155; border:1px solid #475569; border-radius:6px; color:#fff; font-size:14px; }
    input[type="range"] { padding:0; }

    /* :user-invalid only kicks in AFTER the person has typed and left the field,
       so an untouched empty form is never painted red. (Its older cousin
       :invalid turns a required-but-empty field red the moment the page loads,
       which reads as "you got it wrong" before they have typed a thing.) */
    input:user-invalid { border-color:#ef4444; }   /* red once it is genuinely wrong */
    input:user-valid   { border-color:#22c55e; }   /* green once it is genuinely right */

    .hint { font-size:.78rem; color:#94a3b8; margin-top:4px; }
    button { padding:10px; background:#3b82f6; border:none; border-radius:8px; color:#fff; font-weight:700; font-size:15px; cursor:pointer; }
    output { font-weight:700; color:#22c55e; }
  </style>
</head>
<body>
  <form id="booking">
    <div>
      <!-- for="email" matches id="email": clicking the label focuses the input,
           and screen readers announce the label when the input is focused. -->
      <label for="email">Email</label>

      <!-- type="email" gives phones the @ keyboard AND makes the browser check
           the shape of the address. required means "cannot be left empty". -->
      <input id="email" name="email" type="email" required placeholder="[email protected]">
      <p class="hint">Try typing "nope" and pressing Tab.</p>
    </div>

    <div>
      <label for="when">Preferred date</label>
      <!-- type="date" gives a real calendar picker instead of a text box.
           min/max fence the allowed range: dates outside 2026 are rejected. -->
      <input id="when" name="when" type="date" required min="2026-01-01" max="2026-12-31">
      <p class="hint">Pick 2027 and the browser refuses it.</p>
    </div>

    <div>
      <label for="guests">Guests</label>
      <!-- step="1" means whole numbers only; min/max cap it at 1 to 8. -->
      <input id="guests" name="guests" type="number" min="1" max="8" step="1" value="2">
    </div>

    <div>
      <!-- <output> is a real HTML element for "a value the page calculated".
           for="budget" links it to the input it reports on. -->
      <label for="budget">Budget: &pound;<output id="budgetOut" for="budget">60</output></label>

      <!-- step="10" means the slider can only ever land on 20, 30, 40 ... 200.
           oninput fires on every drag; "this.value" is the slider's new number. -->
      <input id="budget" name="budget" type="range" min="20" max="200" step="10" value="60"
             oninput="document.getElementById('budgetOut').value = this.value">
    </div>

    <div>
      <label for="city">City</label>
      <!-- list="cities" points at the <datalist id="cities"> below: suggestions
           appear as you type, but anything else is still allowed. -->
      <input id="city" name="city" type="text" list="cities" placeholder="Start typing...">
      <datalist id="cities">
        <option value="Bristol"></option>
        <option value="Leeds"></option>
        <option value="Manchester"></option>
      </datalist>
    </div>

    <button type="submit">Request booking</button>
  </form>

  <!-- ✅ What you should see when you run it and try to break it:
       - Every border starts neutral grey. Type "nope" in Email, press Tab: that
         border turns red. Fix it to [email protected] and it turns green.
       - Press "Request booking" with Email empty and the form does not submit —
         Chrome and Edge pop up "Please fill out this field."; with "nope" in it
         they say "Please include an '@' in the email address." Firefox and
         Safari word these messages differently.
       - Type 20 into Guests and submit: "Value must be less than or equal to 8."
       - Choose 1 May 2027: "Value must be 12/31/2026 or earlier."
       - Drag the Budget slider: the number after the £ follows it, and because
         step is 10 it only ever shows 20, 30, 40 ... 200.
       Not one line of validation JavaScript was written to get any of that. -->
  <!-- ✅ Expected result, measured in a real browser:
     form -> display: grid
     label -> display: block
     button -> cursor: pointer
     .hint -> count: 2
  -->
</body>
</html>
The page this code makes: type, required, min/max/step, output and datalist, fully commented
What this code shows in a browser window 720 pixels wide.

Those pop-up messages are written by the browser, not by you, which is why the exact wording differs between Chrome, Firefox and Safari — and why they are already translated into the user's language.

🎯 Your Turn — pick the right type and wire the datalist

This form is built but deliberately dumb: every field is a plain text box. Fill in the four blanks marked ___ to turn it into something the browser understands.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Your Turn: Pick the right input</title>
  <style>
    /* 🎯 YOUR TURN — fill in the four blanks marked ___ (all four are in the HTML below) */

    * { box-sizing:border-box; margin:0; padding:0; }
    body { background:#0f172a; color:#e5e7eb; font-family:system-ui,sans-serif; padding:24px; }
    form { max-width:400px; background:#1e293b; padding:20px; border-radius:12px; display:grid; gap:14px; }
    label { display:block; font-size:.78rem; text-transform:uppercase; color:#60a5fa; font-weight:700; margin-bottom:6px; letter-spacing:.5px; }
    input { width:100%; padding:9px 10px; background:#334155; border:1px solid #475569; border-radius:6px; color:#fff; font-size:14px; }
    input[type="range"] { padding:0; }
    input:user-invalid { border-color:#ef4444; }
    input:user-valid { border-color:#22c55e; }
    output { color:#22c55e; font-weight:700; }
  </style>
</head>
<body>
  <form>
    <div>
      <label for="mail">Work email</label>
      <!-- 1) Make the browser check this is an email address (and give phones the @ key). -->
      <input id="mail" type="___" required placeholder="[email protected]">
      <!-- 👉 replace ___ with  email  -->
    </div>

    <div>
      <label for="vol">Volume: <output id="volOut">50</output>%</label>
      <!-- 2) Make this a slider instead of a text box. -->
      <!-- 3) Make it move in jumps of 5 (so it can only ever be 0, 5, 10 ... 100). -->
      <input id="vol" type="___" min="0" max="100" step="___" value="50"
             oninput="document.getElementById('volOut').value = this.value">
      <!-- 👉 replace the first ___ with  range , the second ___ with  5  -->
    </div>

    <div>
      <label for="lang">Favourite language</label>
      <!-- 4) Connect this input to the <datalist> below so suggestions appear.
              The value must match the datalist's id exactly. -->
      <input id="lang" type="text" list="___" placeholder="Start typing...">
      <!-- 👉 replace ___ with  langs  -->
      <datalist id="langs">
        <option value="CSS"></option>
        <option value="HTML"></option>
        <option value="JavaScript"></option>
        <option value="Python"></option>
      </datalist>
    </div>
  </form>

  <!-- ✅ Expected once all four blanks are filled:
       - Work email: type "hello", press Tab -> the border goes red and the field
         is rejected on submit. Change it to [email protected] -> border goes green.
       - Volume: a draggable slider. Drag it and the green number next to
         "Volume:" follows. Because step is 5 it never shows 97 — it snaps to 95.
       - Favourite language: a dropdown of suggestions appears as you type, but
         you can still type something that is not on the list.
       Leave a blank unfilled and that field silently stays an ordinary text box —
       an unknown type value falls back to type="text". -->
</body>
</html>

Custom Toggle Switch & Styled Checkbox

Build beautiful custom controls using only CSS — no JavaScript needed.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Custom Controls</title>
  <style>
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body { background: #0f172a; font-family: system-ui, sans-serif; color: #e5e7eb; padding: 30px; }

    h2 { color: #3b82f6; margin: 25px 0 15px; }

    /* === TOGGLE SWITCH === */
    .toggle-label {
      display: flex;
      align-items: center;
      gap: 12px;
      cursor: pointer;
      margin: 10px 0;
      font-size: 15px;
    }

    .toggle-input {
      display: none;
    }

    .toggle-slider {
      width: 50px;
      height: 26px;
      background: #475569;
      border-radius: 13px;
      position: relative;
      transition: background 0.3s;
    }

    .toggle-slider::after {
      content: '';
      position: absolute;
      top: 3px;
      left: 3px;
      width: 20px;
      height: 20px;
      background: white;
      border-radius: 50%;
      transition: transform 0.3s;
    }

    .toggle-input:checked + .toggle-slider {
      background: #3b82f6;
    }

    .toggle-input:checked + .toggle-slider::after {
      transform: translateX(24px);
    }

    /* === CUSTOM CHECKBOX === */
    .custom-check {
      display: flex;
      align-items: center;
      gap: 10px;
      cursor: pointer;
      margin: 8px 0;
      font-size: 15px;
    }

    .custom-check input {
      display: none;
    }

    .checkmark {
      width: 22px;
      height: 22px;
      border: 2px solid #475569;
      border-radius: 4px;
      display: flex;
      align-items: center;
      justify-content: center;
      transition: all 0.2s;
      flex-shrink: 0;
    }

    .checkmark::after {
      content: '✓';
      color: white;
      font-size: 14px;
      font-weight: bold;
      opacity: 0;
      transform: scale(0);
      transition: all 0.2s;
    }

    .custom-check input:checked + .checkmark {
      background: #3b82f6;
      border-color: #3b82f6;
    }

    .custom-check input:checked + .checkmark::after {
      opacity: 1;
      transform: scale(1);
    }

    /* === CUSTOM RADIO === */
    .custom-radio {
      display: flex;
      align-items: center;
      gap: 10px;
      cursor: pointer;
      margin: 8px 0;
      font-size: 15px;
    }

    .custom-radio input { display: none; }

    .radio-dot {
      width: 22px;
      height: 22px;
      border: 2px solid #475569;
      border-radius: 50%;
      display: flex;
      align-items: center;
      justify-content: center;
      transition: border-color 0.2s;
      flex-shrink: 0;
    }

    .radio-dot::after {
      content: '';
      width: 10px;
      height: 10px;
      background: #3b82f6;
      border-radius: 50%;
      opacity: 0;
      transform: scale(0);
      transition: all 0.2s;
    }

    .custom-radio input:checked + .radio-dot {
      border-color: #3b82f6;
    }

    .custom-radio input:checked + .radio-dot::after {
      opacity: 1;
      transform: scale(1);
    }

    .card {
      background: #1e293b;
      padding: 20px;
      border-radius: 8px;
      margin: 10px 0;
    }
  </style>
</head>
<body>
  <h1>🎨 Custom CSS Controls</h1>

  <h2>Toggle Switches</h2>
  <div class="card">
    <label class="toggle-label">
      <input type="checkbox" class="toggle-input" checked>
      <span class="toggle-slider"></span>
      Dark Mode
    </label>
    <label class="toggle-label">
      <input type="checkbox" class="toggle-input">
      <span class="toggle-slider"></span>
      Notifications
    </label>
    <label class="toggle-label">
      <input type="checkbox" class="toggle-input" checked>
      <span class="toggle-slider"></span>
      Auto-save
    </label>
  </div>

  <h2>Custom Checkboxes</h2>
  <div class="card">
    <label class="custom-check">
      <input type="checkbox" checked>
      <span class="checkmark"></span>
      HTML & CSS
    </label>
    <label class="custom-check">
      <input type="checkbox">
      <span class="checkmark"></span>
      JavaScript
    </label>
    <label class="custom-check">
      <input type="checkbox" checked>
      <span class="checkmark"></span>
      Python
    </label>
  </div>

  <h2>Custom Radio Buttons</h2>
  <div class="card">
    <label class="custom-radio">
      <input type="radio" name="plan" checked>
      <span class="radio-dot"></span>
      Free Plan
    </label>
    <label class="custom-radio">
      <input type="radio" name="plan">
      <span class="radio-dot"></span>
      Pro Plan — $9/mo
    </label>
    <label class="custom-radio">
      <input type="radio" name="plan">
      <span class="radio-dot"></span>
      Enterprise — $29/mo
    </label>
  </div>
  <!-- ✅ Expected result, measured in a real browser:
     .toggle-label -> display: flex
     .toggle-input -> display: none
     .toggle-slider -> position: relative
     .custom-check -> display: flex
     .card -> count: 3
  -->
</body>
</html>
The page this code makes: Toggle switches and styled checkboxes with pure CSS
What this code shows in a browser window 720 pixels wide.

🧩 Mini-Challenge — a custom checkbox that keyboard users can still use

The demo above hides the real checkbox with display: none and draws a replacement next to it. It looks great with a mouse, but a display: none input is removed from the page completely — you cannot Tab to it and a screen reader never announces it. Your challenge is to build the same look without hiding anything, by restyling the real checkbox with appearance: none. Outline only this time.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Mini-Challenge: Accessible Custom Checkbox</title>
  <style>
    /* 🧩 MINI-CHALLENGE: a custom checkbox with nothing hidden
       1. input[type="checkbox"] — set  appearance: none;  (add -webkit-appearance: none;
          for older Safari). This strips the browser's own tick box and leaves you a
          plain, still-focusable, still-checkable element to style.
       2. Give it a size (about 22px square), a 2px border, a border-radius and
          vertical-align: middle so it lines up with the label text.
       3. input[type="checkbox"]:checked — change the background and border colour.
       4. Draw the tick with  input[type="checkbox"]:checked::after  { content: '✓'; }
          and centre it (display:grid; place-items:center; on the input works).
       5. input[type="checkbox"]:focus-visible — add an outline so keyboard users can
          see where they are. This is the bit the display:none version can never have.

       ✅ How to check it: click the box — it ticks. Now click somewhere else and press
          Tab until you reach it — you should SEE a focus ring, and pressing Space
          should tick it. Do the same on the display:none demo higher up the page and
          the Tab key skips straight past it: the checkbox is unreachable.
    */

    * { box-sizing: border-box; margin: 0; padding: 0; }
    body { background:#0f172a; color:#e5e7eb; font-family:system-ui,sans-serif; padding:30px; }
    label { display:flex; align-items:center; gap:10px; margin:10px 0; font-size:15px; cursor:pointer; }

    /* your CSS here */

  </style>
</head>
<body>
  <h1>Pick your topics</h1>

  <!-- Markup done for you: real checkboxes, each inside its own label. -->
  <label><input type="checkbox" checked> HTML &amp; CSS</label>
  <label><input type="checkbox"> JavaScript</label>
  <label><input type="checkbox"> Python</label>

  <!-- ✅ Expected result, measured in a real browser:
     body -> padding-top: 30px
     label -> display: flex
  -->
</body>
</html>

⚠️ Common Mistakes

🎉 Lesson Complete

You can now use and style every HTML5 input type:

Practice quiz

Which input type shows a native calendar date picker?

  • type="text"
  • type="date"
  • type="calendar"
  • type="day"

Answer: type="date". type="date" gives the browser a built-in calendar picker, and "calendar"/"day" are not real input types.

On a mobile device, which input type brings up the numeric phone keypad?

  • type="number"
  • type="phone"
  • type="tel"
  • type="digits"

Answer: type="tel". type="tel" is for telephone numbers and triggers the numeric phone keypad on mobile.

What does the <datalist> element provide for an input?

  • A dropdown that forces one of its values
  • Autocomplete suggestions while still allowing free text
  • A multi-select list
  • Client-side form validation

Answer: Autocomplete suggestions while still allowing free text. <datalist> offers suggested values for autocomplete but the user can still type any value, and it degrades to a plain text input.

Which input type renders a slider control?

  • type="slider"
  • type="range"
  • type="number"
  • type="scroll"

Answer: type="range". type="range" produces a slider whose value is between its min and max.

Which input type opens a color picker?

  • type="color"
  • type="hex"
  • type="paint"
  • type="rgb"

Answer: type="color". type="color" shows the native color picker and its value is a hex color like #3b82f6.

Which attributes set the bounds and step of a number or range input?

  • low, high, by
  • start, end, gap
  • min, max, step
  • from, to, increment

Answer: min, max, step. min and max bound the value and step controls the increment for number and range inputs.

To make a hidden native checkbox still toggle when you click your custom control, you should keep the input inside which element?

  • A <span>
  • A <label>
  • A <button>
  • A <fieldset>

Answer: A <label>. Wrapping the input in a <label> means clicking the label (and your custom control) still toggles the real input.

In the toggle-switch pattern, which CSS combinator styles the slider when the checkbox is checked?

  • .toggle-input:checked > .toggle-slider
  • .toggle-input:checked + .toggle-slider
  • .toggle-input:checked .toggle-slider
  • .toggle-slider:checked

Answer: .toggle-input:checked + .toggle-slider. The adjacent sibling combinator (+) targets the .toggle-slider that immediately follows the checked input.

Which input type adds URL validation and a ".com"-style keyboard on mobile?

  • type="link"
  • type="web"
  • type="url"
  • type="href"

Answer: type="url". type="url" validates that the value looks like a URL and offers a URL-friendly keyboard.

Why prefer type="email" over type="text" for an email field?

  • It encrypts the value
  • It gives an email keyboard and built-in email validation
  • It hides the typed characters
  • It auto-submits the form

Answer: It gives an email keyboard and built-in email validation. type="email" surfaces the right mobile keyboard and lets the browser validate the format automatically.

Continue this course