GODRICH

CSS Input Design: 9 Form Widgets You Can Paste In

CSS input design means the small form fields — phone numbers, card numbers, quantities, tags — that fill checkout and signup forms but need a little JavaScript

Five of the nine (tags, auto-grow, drag-drop, resend timer, inline validation) rarely show up in animation libraries — you mostly meet them on real product screens instead.

Auto-plays · click a tile to jump to its section · all nine in one zip

The nine are ordered by how a form actually gets filled out, not by popularity: contact and payment details first (01–03), quantity and tags next (04–05), notes and attachments after that (06–07), and identity verification plus final validation last (08–09).

# Widget When you'd use it
01 Auto-hyphen phone number Signup contact, shipping phone
02 Card number 4-digit grouping Checkout card field
03 Currency input with auto comma Donation, quote amount
04 Quantity +/- stepper Cart quantity, booking guests
05 Tag input Keywords, multiple recipients
06 Auto-growing textarea Review, support message
07 Drag-and-drop file dropzone Profile photo, attachments
08 OTP resend timer button Phone or email re-verification
09 Inline input validation Email, password validation

01Auto-hyphen phone number

Type digits only and hyphens land in the right spot automatically, like 010-1234-5678. Pasting a full number reformats it the same way.

010 패턴자동 하이픈숫자만
function format(raw) {
  const d = raw.replace(/\D/g, "").slice(0, 11);
  if (d.length < 4) return d;
  if (d.length < 8) return d.slice(0, 3) + "-" + d.slice(3);
  return d.slice(0, 3) + "-" + d.slice(3, 7) + "-" + d.slice(7);
}
input.addEventListener("input", () => {
  input.value = format(input.value);
});

02Card number 4-digit grouping

A space is inserted every four digits as you type, so the number reads like a real card. The first digit also flags VISA, Mastercard, or AMEX.

4자리 그룹16자리 제한숫자만
function format(raw) {
  const d = raw.replace(/\D/g, "").slice(0, 16);
  const groups = d.match(/.{1,4}/g) || [];
  return groups.join(" ");
}
function brandOf(d) {
  if (d[0] === "4") return "VISA";
  if (/^5[1-5]/.test(d)) return "Mastercard";
  return "";
}

03Currency input with auto comma

Type digits into a currency-prefixed field and thousands separators appear automatically. Leading zeros are stripped along the way.

₩ 접두어천 단위 콤마우측 정렬
function format(raw) {
  const d = raw.replace(/\D/g, "").replace(/^0+(?=\d)/, "").slice(0, 12);
  return d ? Number(d).toLocaleString("en-US") : "";
}
input.addEventListener("input", () => {
  input.value = format(input.value);
});

04Quantity +/- stepper

Each tap changes the number in the middle by one, and the minus button dims and stops responding once the minimum (1) is reached.

최소/최대 제한키보드 지원숫자 직접 입력
const MIN = 1, MAX = 99;
function set(v) {
  n = Math.min(MAX, Math.max(MIN, v));
  value.value = n;
  minus.disabled = n <= MIN;
  plus.disabled = n >= MAX;
}
minus.addEventListener("click", () => set(n - 1));
plus.addEventListener("click", () => set(n + 1));

05Tag input

Type text and press Enter to turn it into a pill-shaped tag; pressing Backspace on an empty field removes the last tag.

Enter로 추가×로 삭제백스페이스 삭제
input.addEventListener("keydown", (e) => {
  if (e.key === "Enter" || e.key === ",") {
    e.preventDefault();
    if (input.value.trim()) tags.push(input.value.trim());
    input.value = "";
    render();
  } else if (e.key === "Backspace" && !input.value && tags.length) {
    tags.pop();
    render();
  }
});

06Auto-growing textarea

As lines are added, the box itself grows taller instead of scrolling, only switching to a scrollbar past a set max height (132px).

scrollHeight 기반최대 높이 제한리사이즈 불가
function resize() {
  // reset to auto first, or the height never shrinks back down
  area.style.height = "auto";
  const next = Math.min(area.scrollHeight, 132);
  area.style.height = next + "px";
}
area.addEventListener("input", resize);

07Drag-and-drop file dropzone

Dragging a file over the zone thickens its border, and dropping it turns the filename into a chip. Clicking still opens the normal file picker.

dragover 표시클릭해서 찾아보기키보드 접근
zone.addEventListener("dragover", (e) => {
  e.preventDefault();
  zone.classList.add("is-over");
});
zone.addEventListener("drop", (e) => {
  e.preventDefault();
  zone.classList.remove("is-over");
  const file = e.dataTransfer.files[0];
  if (file) filename.textContent = "Selected: " + file.name;
});

08OTP resend timer button

Tapping resend disables the button for 60 seconds while the remaining count ticks down on the button label itself, then re-enables at zero.

60초 카운트다운중복 클릭 방지aria-live 안내
function cooldown(seconds) {
  let left = seconds;
  btn.disabled = true;
  btn.textContent = left + "s to resend";
  const t = setInterval(() => {
    left--;
    if (left <= 0) {
      clearInterval(t);
      btn.disabled = false;
      btn.textContent = "Resend";
    } else {
      btn.textContent = left + "s to resend";
    }
  }, 1000);
}

09Inline input validation

While typing, a green check appears the moment the email format is valid, or a red message if it isn't. An empty field shows nothing at all.

실시간 검증role="status" 알림성공/실패 아이콘
const RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
input.addEventListener("input", () => {
  const v = input.value;
  frame.classList.remove("is-ok", "is-bad");
  if (!v) { icon.textContent = ""; msg.textContent = ""; return; }
  if (RE.test(v)) { frame.classList.add("is-ok"); icon.textContent = "✓"; }
  else { frame.classList.add("is-bad"); icon.textContent = "!"; msg.textContent = "Check the email format"; }
});

Where this breaks — one real trap

01, 02, and 03 all share the same trap. Whenever you rewrite input.value wholesale, like input.value = format(value), the browser resets the caret to the very end of the string. Click into the middle of an already-formatted number to fix a digit, and the next keystroke sends the caret flying back to the end anyway. For short phone or card numbers this barely matters, but for anything you expect people to edit at length, MDN's HTMLInputElement.setSelectionRange docs show how to save the caret position before reformatting and restore it after. The password for the zip holding all nine files — working vanilla JS and a React version each — is puc8udvv — grab it with the button below.

07's drag-and-drop hides a similar gotcha. Skip calling preventDefault() on the dragover event and the browser falls back to its default behavior (opening the file in a new tab), which means drop never fires at all. The snippet above handles that on purpose.

Accessibility

Every field pairs a label with its input through for, and 04's stepper announces the new count ("Quantity 3") through a visually hidden aria-live region so screen reader users hear the change without seeing the number. 08's resend button reports completion through an aria-live="polite" status line, and 09's validation message uses role="status" so it updates quietly only when the value actually changes. 07's file input stays reachable by Tab and opens the native picker with Enter or Space, so the whole flow works without a mouse. Every color and border transition also honors prefers-reduced-motion, per MDN's docs.

@media (prefers-reduced-motion: reduce) {
  .field__input, .drop__zone, .valid__frame { transition: none; }
}

More form-input pieces live in OTP Input UI: 9 States, and the about page explains what this site is for.

FAQ

Can I drop these nine straight into a React project?

Yes. The zip's react/ folder has a .tsx and a .module.scss for every item, ported from the same logic as the vanilla JS in vanilla/. You mostly just pass in a color or label string as props.

Does auto-formatting the card number affect payment security at all?

No. What's shown here only groups digits into a readable 4-digit pattern on screen — actually handling or storing card data is the payment processor's job, and this formatting never sends anything to a server.

Does the numeric keyboard pop up automatically on mobile for the phone and card fields?

Yes, inputmode="numeric" tells most mobile browsers to show the numeric keypad instead of the full keyboard. It's only a hint though, so behavior can vary slightly between browsers.

Enter the archive password

The password is inside this article. You will find it as you read.