Input fields

Input fields turn curiosity into structured data—names, search terms, shipping details, or dates that steer the experience forward. Well-crafted fields hint at their expected content through labels, placeholders, and validation, reducing friction so people can stay focused on the task at hand rather than deciphering the UI.

The patterns below cover single-line inputs, selects, textareas, and enhanced combinations with icons, buttons, or suggestions. Use them as building blocks when designing flows: align labels, reuse helper text styles, and lean on the provided states to keep every form clear, accessible, and predictable from start to submit.

Text fields

Text inputs handle everything from simple strings to structured data types like email, tel, and date pickers. Pair each input with a descriptive label and keep placeholders instructional rather than essential, since helper text and validation states can communicate formatting rules far more reliably.

Error messages where appropriate Help messages where appropriate. Limited support in most browsers Limited support in most browsers

Connect every label via the for/id relationship, and avoid removing native browser affordances (e.g., numeric spinners) so keyboard, touch, and assistive users all benefit from the built-in semantics.

<input type="text">
<input type="text" disabled>
<input type="text" readonly>
<input type="text" required>
<input type="password">
<input type="tel">
<input type="email">
<input type="url">
<input type="search">
<input type="date">
<input type="time">
<input type="datetime-local">
<input type="week">
<input type="month">
<input type="number">

Select fields

Select elements constrain choices to a known list, perfect for states, categories, or filters where free-form entry would introduce errors. Rely on the native single-select for concise menus and the multiple variant when users need to select several items without leaving the form context.

Help messages where appropriate.

Help messages where appropriate.

Keep option labels short and unique so they truncate gracefully, and always provide an empty first option or placeholder when a selection is optional. This ensures assistive tech can announce whether a value has been chosen, preventing accidental defaults.

<select>
  <option value="...">...</option>
  <option value="...">...</option>
  <option value="...">...</option>
</select>
<select disabled>...</select>
<select multiple>
  <option value="...">...</option>
  <option value="...">...</option>
  <option value="...">...</option>
</select>
<select disabled multiple>...</select>

Textarea

Textareas support longer narratives like feedback, descriptions, or multi-line code snippets. Keep them vertically resizable when possible and pair them with helper text describing content expectations so users can gauge the appropriate length.

Help messages where appropriate.

Preserve line breaks server-side and mirror them in previews, and ensure disabled or read-only states still contrast enough with the background so the content remains legible.

<textarea></textarea>
<textarea disabled></textarea>

Attached buttons

Attached buttons bundle actions directly with their controlling fields: think search bars with submit buttons, unit converters, or segmented filter chips. Grouping the controls keeps spatial relationships obvious and reduces the travel distance between text entry and next step.

Help messages where appropriate.

Match heights and spacing between inputs and buttons so the group reads as a single control, and ensure keyboard users can tab through the sequence without unexpected focus traps or ARIA misfires.

<div class="group">
  <input type="text">
  <button class="positive">
    <svg class="icon">...</svg>
  </button>
</div>
<div class="group">
  <input type="text">
  <button>...</button>
  <button class="secondary">...</button>
</div>
<div class="group">
  <button class="primary">...</button>
  <input type="text">
  <button class="negative">...</button>
</div>
<div class="group">
  <select>...</select>
  <button class="positive">
    <svg class="icon">...</svg>
  </button>
</div>

Attached labels

Attached labels keep inline units, suffixes, or prefixes tightly coupled to their fields. Use them for currency symbols, measurement units, or quick descriptors when a traditional block label would consume too much vertical space.

Reuse real <label> elements, even for inline adornments, so assistive technologies announce the association. When the label mirrors a unit (like “kg”), keep it short and avoid duplicating the same text inside the input value to prevent redundancy.

<div class="group">
  <input type="text">
  <label for="...">Appended label</label>
</div>
<div class="group">
  <label for="...">Prepended label</label>
  <input type="text">
</div>
<div class="group">
  <select>...</select>
  <label for="...">Appended label</label>
</div>

Icons

Icon adornments provide visual hints for expected content—mail icons for email fields, magnifiers for search, warning badges for validation. They also help users scan dense forms by reinforcing intent before they read the full label.

Ensure icons are decorative by default (hide them from screen readers) unless they convey critical meaning, and keep padding symmetrical so focus rings never clip against the SVG container.

<div class="group">
  <input type="text" class="append-icon">
  <svg class="icon">...</svg>
</div>

<div class="group">
  <svg class="icon">...</svg>
  <input type="text" class="prepend-icon">
</div>

<div class="group">
  <svg class="icon">...</svg>
  <input type="text" class="prepend-icon append-icon">
  <svg class="icon">...</svg>
</div>

<div class="group">
  <select class="append-icon">...</select>
  <svg class="icon">...</svg>
</div>

Mixed

Mixed groups combine labels, icons, multiple inputs, and buttons to build compact flows like newsletter subscriptions or billing forms. Treat each grouping as a mini-layout: align baselines, reuse spacing tokens, and keep the reading order logical so both visual and assistive users can parse the sequence.

<div class="group">
  <label for="..."><svg class="icon">...</svg></label>
  <svg class="icon">...</svg>
  <input type="email" class="prepend-icon">
  <button class="primary">...</button>
  <button class="secondary"><svg class="icon">...</svg></button>
</div>

<div class="group">
  <input type="text">
  <input type="text">
  <button>...</button>
</div>

<div class="group">
  <label for="..."><svg class="icon">...</svg></label>
  <svg class="icon">...</svg>
  <select class="prepend-icon">...</select>
  <button class="primary">...</button>
  <button class="secondary"><svg class="icon">...</svg></button>
</div>

<div class="group">
  <select>...</select>
  <select>...</select>
  <button>...</button>
</div>

File upload

The browser’s own file input is hard to style, so Nausikaä hides the <input type="file"> visually and lets a <label class="button"> act as the control. The input must come directly before its label: the button then shows the input’s focus and disabled states, and clicking the label opens the file dialog. The input stays in the page, so it still works with the keyboard and screen readers and is submitted with the form as usual.

No files chosen.

<input type="file" id="photos" name="photos" accept="image/*" multiple>
<label for="photos" class="button">
  <span class="label">
    <svg class="icon"><use xlink:href="/images/icons.svg#upload"></use></svg>
  </span>
  Choose images
</label>

Because the native input is hidden, the names of the chosen files are no longer visible. Show them next to the button with a few lines of JavaScript, as the example above does:

const input = document.getElementById("photos");
const output = document.getElementById("photo-names");

input.addEventListener("change", () => {
  const names = Array.from(input.files, (file) => file.name);
  output.textContent = names.length ? names.join(", ") : "No files chosen.";
});

Useful attributes: accept limits the file dialog to certain types (image/*, .pdf), multiple allows more than one file, and capture opens the camera directly on phones. Remember to give the surrounding <form> the attribute enctype="multipart/form-data".

Drop zone

A drop zone is a large target to drop files on. Wrap the file input and its label in an element with the class upload. The input covers the whole zone, invisibly, so the browser does the work: dropping files on it and clicking it both work without JavaScript, and the files are sent with the form like those of any other file input.

<div class="upload">
  <input type="file" name="attachments" id="attachments" multiple>
  <label for="attachments">
    <svg class="icon"><use xlink:href="/images/icons.svg#upload"></use></svg>
    <strong>Drop files here</strong> or click to choose
    <small>PDF or image, up to 25 MB each</small>
  </label>
</div>

Add small for a drop zone of one line, invalid when the chosen files can't be accepted, and disabled together with the disabled attribute on the input when uploading isn't possible.

<div class="upload small">...</div>
<div class="upload small invalid">...</div>
<div class="upload small disabled">
  <input type="file" id="..." disabled>
  ...
</div>

A browser doesn't show that files are being dragged over an element. To highlight the drop zone at that moment, add the class dragover with a few lines of JavaScript. Try it by dragging a file over the first drop zone above.

const zone = document.querySelector(".upload");

["dragenter", "dragover"].forEach((type) =>
  zone.addEventListener(type, () => zone.classList.add("dragover")));

["dragleave", "drop"].forEach((type) =>
  zone.addEventListener(type, () => zone.classList.remove("dragover")));

Chosen files

A list with the class files shows the files that were chosen or uploaded. Each item has an icon, the name, and optionally the size in a <small> and a control to remove the file. A long name is cut off. A <progress> in the item gets a line of its own.

The state of a file is a class on the item: done colors the icon green, error colors it red and makes the remark behind the name stand out. While a file is on its way, give its icon the class uploading to make it turn.

  • invoice-2024-0318.pdf 184 kB
  • nausicaa-and-her-maidens-high-resolution-scan.tiff 18.2 MB 62%
  • catalogue-complete.zip Too large: 25 MB at most
<ul class="files">
  <li class="done">
    <svg class="icon"><use xlink:href="/images/icons.svg#check"></use></svg>
    <a href="..." class="name">invoice-2024-0318.pdf</a>
    <small>184 kB</small>
    <a class="control" tabindex="0" role="button" aria-label="Remove invoice-2024-0318.pdf">
      <svg class="icon"><use xlink:href="/images/icons.svg#clear"></use></svg>
    </a>
  </li>
  <li>
    <svg class="icon uploading"><use xlink:href="/images/icons.svg#refresh"></use></svg>
    <span class="name">nausicaa-and-her-maidens.tiff</span>
    <small>18.2 MB</small>
    <progress value="62" max="100">62%</progress>
  </li>
  <li class="error">
    <svg class="icon"><use xlink:href="/images/icons.svg#warning"></use></svg>
    <span class="name">catalogue-complete.zip</span>
    <small>Too large: 25 MB at most</small>
  </li>
</ul>

The turning icon also works inside an input with an icon, for a field that shows the file being uploaded:

<div class="group">
  <input type="text" class="append-icon" value="odyssey-book-6.pdf" readonly>
  <svg class="icon uploading"><use xlink:href="/images/icons.svg#refresh"></use></svg>
</div>

Accessibility: the label of the drop zone is the label of the file input, so keep the for and id pair. Don't rely on dropping alone: the zone must also work with a click and with the keyboard, which it does as long as the input stays in place. Tell what went wrong in words, not only with the red color.

Suggestions

Suggestion panels pair an input with contextual results, ideal for autocomplete, search-as-you-type, or quick-pick lists. Panels can appear below or above the control, and the group variants show how icons, buttons, and stacked inputs keep the affordance cohesive even as the interaction grows richer.

Link the panel and input with aria-controls and aria-expanded so assistive tech understands when results appear, and keep keyboard navigation consistent (arrow keys to traverse, Enter to confirm) to build muscle memory.

<div class="suggestions">
  <input type="...">
  <div class="panel">...</div>
</div>

<div class="suggestions top">
  <input type="...">
  <div class="panel">...</div>
</div>

<div class="suggestions">
  <div class="group"></div>
  <div class="panel">...</div>
</div>

Range sliders

A range slider lets people pick a number by dragging, when the exact value matters less than its position within a range: volume, zoom level or a price filter. Set min, max, step and value. The slider can also be moved with the arrow keys, Page Up and Page Down, and Home and End.

<label for="volume">Volume: <output for="volume" id="volume-value">40</output>%</label>
<input type="range" id="volume" min="0" max="100" step="5" value="40">

A slider doesn’t show its value by itself. When the number matters, display it in an <output> element and update it on the input event:

const volume = document.getElementById("volume");
volume.addEventListener("input", () => {
  document.getElementById("volume-value").textContent = volume.value;
});

When to use a number field instead: sliders are imprecise with a mouse and slow with a keyboard over large ranges. If people need to enter an exact value, such as an age or an amount, use <input type="number">, or offer both side by side.

Color pickers

An <input type="color"> shows the chosen color as a swatch and opens the operating system’s color picker when clicked. Its value is always a lowercase six-digit hex code such as #1e66f5; shorthand codes and color names are not accepted.

#1e66f5
<label for="accent">Accent color</label>
<input type="color" id="accent" name="accent" value="#1e66f5">

Offer suggested colors with a <datalist>. Browsers that support it show these as swatches in the picker, while still allowing any other color:

<input type="color" id="label-color" value="#d20f39" list="label-colors">
<datalist id="label-colors">
  <option value="#d20f39"></option>
  <option value="#40a02b"></option>
  ...
</datalist>