CSS Input Text: 18 Examples With Copy-Paste CSS

Most people restyle the border first when they style a CSS input text field, and that is where accessibility rules kick in: once a stylesheet modifies the border, it needs 3:1 contrast under WCAG 2.2.

Below are 18 CSS input text examples you can try and copy, followed by a guide to selectors, sizing, focus, placeholder and autofill styling.

By Bogdan Sandu · Updated October 28, 2026

Text field styles

Five visual treatments for the same single-line field: outlined, filled, underline, floating label and a dark theme. Each one keeps a real label, a focus indicator that survives forced colors, and borders that pass 3:1 against the card behind them.

Outlined field with hint

  • :where() base
  • aria-describedby
  • transparent outline

We send the workspace invite here. No newsletters.

Click or tab into a field.

The classic outlined field. The border is #6b7385, about 4.8:1 on white, so it clears the 3:1 that WCAG 1.4.11 asks of a restyled boundary. Focus adds a box-shadow ring plus a transparent outline, and the outline is what forced colors mode paints when it drops the shadow.

View code
.it-out {
  width: min(100%, 400px);
  padding: 28px;
  border-radius: 18px;
  background: #fff;
  box-shadow: 0 12px 32px rgba(17, 24, 39, .08);
}
.it-out__label {
  display: block;
  margin-bottom: 8px;
  color: #1d2433;
  font-size: 15px;
  font-weight: 600;
}
.it-out__input {
  box-sizing: border-box;
  width: 100%;
  min-height: 48px;
  padding: 0 14px;
  border: 1.5px solid #6b7385;
  border-radius: 10px;
  background: #fff;
  color: #1d2433;
  font: inherit;
  font-size: max(1rem, 16px);
  caret-color: #2f5bea;
  transition: border-color .15s ease, box-shadow .15s ease;
}
.it-out__input::placeholder {
  color: #6b7280;
  opacity: 1;
}
.it-out__input:hover {
  border-color: #3d4456;
}
.it-out__input:focus-visible {
  border-color: #2f5bea;
  outline: 3px solid transparent;
  outline-offset: 2px;
  box-shadow: 0 0 0 3px rgba(47, 91, 234, .35);
}
.it-out__hint {
  margin: 8px 0 0;
  color: #4b5263;
  font-size: 14px;
  line-height: 1.45;
}

Filled fields with an active bar

  • implicit label
  • :focus-within
  • inset box-shadow

Where should we ship it?

Click or tab into a field.

A filled field puts the label inside a tinted box, Material style. The tint is decoration; the 1px bottom border (#55605d on the tint, above 5:1) is the boundary that carries the contrast. Focus thickens that edge with an inset shadow, so the layout never shifts.

View code
.it-fill {
  display: grid;
  gap: 14px;
  width: min(100%, 440px);
  padding: 26px;
  border-radius: 20px;
  background: #fff;
  box-shadow: 0 12px 32px rgba(17, 24, 39, .08);
}
.it-fill__title {
  margin: 0 0 2px;
  color: #12302b;
  font-family: "Fraunces", serif;
  font-size: 22px;
  line-height: 1.2;
}
.it-fill__row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: 14px;
}
.it-fill__box {
  display: grid;
  gap: 2px;
  padding: 8px 14px 6px;
  border-bottom: 1px solid #55605d;
  border-radius: 8px 8px 0 0;
  background: #e8f0ee;
  cursor: text;
  transition: background-color .15s ease, box-shadow .15s ease;
}
.it-fill__box:hover {
  background: #dde8e5;
}
.it-fill__label {
  color: #3d4a47;
  font-size: 13px;
  font-weight: 600;
}
.it-fill__input {
  box-sizing: border-box;
  width: 100%;
  min-width: 0;
  padding: 2px 0 4px;
  border: 0;
  background: transparent;
  color: #12302b;
  font: inherit;
  font-size: max(1rem, 16px);
  caret-color: #0f766e;
}
.it-fill__input::placeholder {
  color: #5c6866;
  opacity: 1;
}
.it-fill__input:focus {
  outline: none;
}
.it-fill__box:focus-within {
  outline: 2px solid transparent;
  box-shadow: inset 0 -3px 0 #0f766e;
}
.it-fill__box:focus-within .it-fill__label {
  color: #0b5e57;
}

Editorial underline field

  • border-bottom only
  • Fraunces
  • text-overflow

Only you can see this list until you share it.

Click or tab into a field.

An underline field reads well in editorial layouts where boxes feel heavy. Only border-bottom survives, so it has to carry the full 3:1 on its own; this one is near-black on cream. The placeholder gets text-overflow: ellipsis while it shows, so a long hint is trimmed cleanly on narrow screens.

View code
.it-ul {
  width: min(100%, 460px);
  padding: 34px 30px 28px;
  border-radius: 6px;
  background: #fbf6ec;
  box-shadow: 0 1px 0 #e7dcc6, 0 18px 40px rgba(59, 50, 38, .1);
}
.it-ul__label {
  display: block;
  color: #3b3226;
  font-size: 14px;
  font-weight: 600;
}
.it-ul__input {
  box-sizing: border-box;
  width: 100%;
  padding: 10px 0 8px;
  border: 0;
  border-bottom: 2px solid #3b3226;
  border-radius: 0;
  background: transparent;
  color: #241d14;
  font-family: "Fraunces", serif;
  font-size: clamp(20px, 5vw, 26px);
  caret-color: #b4441c;
  transition: border-color .15s ease, box-shadow .15s ease;
}
.it-ul__input::placeholder {
  color: #6d604c;
  opacity: 1;
}
.it-ul__input:placeholder-shown {
  text-overflow: ellipsis;
}
.it-ul__input:focus-visible {
  border-bottom-color: #b4441c;
  outline: 2px solid transparent;
  box-shadow: 0 2px 0 #b4441c;
}
.it-ul__meta {
  margin: 12px 0 0;
  color: #5f5442;
  font-size: 14px;
}

Floating label on the border

  • :placeholder-shown
  • transform
  • placeholder=" "

Billing details

Click or tab into a field.

The label sits inside the field until it gets focus or a value, then shrinks onto the border with a background patch that cuts the line. The value check is input:not(:placeholder-shown), which needs a placeholder, so each field has a single space. The label stays a real label element the whole time.

View code
.it-flt {
  display: grid;
  gap: 18px;
  width: min(100%, 400px);
  padding: 26px;
  border-radius: 16px;
  background: #fff;
  box-shadow: 0 12px 32px rgba(17, 24, 39, .08);
}
.it-flt__title {
  margin: 0;
  color: #241b3a;
  font-size: 18px;
  font-weight: 700;
}
.it-flt__field {
  position: relative;
}
.it-flt__input {
  box-sizing: border-box;
  width: 100%;
  height: 54px;
  padding: 16px 14px 0;
  border: 1.5px solid #6e6880;
  border-radius: 10px;
  background: #fff;
  color: #241b3a;
  font: inherit;
  font-size: max(1rem, 16px);
  caret-color: #6d28d9;
}
.it-flt__label {
  position: absolute;
  top: 50%;
  left: 10px;
  padding: 0 4px;
  background: #fff;
  color: #57516a;
  font-size: 16px;
  line-height: 1.2;
  pointer-events: none;
  transform: translateY(-50%);
  transform-origin: left center;
}
.it-flt__input:focus,
.it-flt__input:not(:placeholder-shown) {
  padding-top: 0;
}
.it-flt__input:focus + .it-flt__label,
.it-flt__input:not(:placeholder-shown) + .it-flt__label {
  top: 0;
  transform: translateY(-50%) scale(.8);
}
.it-flt__input:focus + .it-flt__label {
  color: #6d28d9;
}
.it-flt__input:focus-visible {
  border-color: #6d28d9;
  outline: 3px solid transparent;
  box-shadow: 0 0 0 3px rgba(109, 40, 217, .3);
}
@media (prefers-reduced-motion: no-preference) {
  .it-flt__label {
    transition: transform .18s ease, top .18s ease, color .18s ease;
  }
}

Dark theme config fields

  • color-scheme: dark
  • monospace
  • custom properties

Environment

Click or tab into a field.

color-scheme: dark on the wrapper tells the browser to draw its own parts (caret, selection, autofill, scrollbars) for a dark surface. Colors live in custom properties, so a light theme only swaps the variables. The border is rechecked on the dark card: #6b7686 on #10151d lands near 3.9:1.

View code
.it-dk {
  --it-dk-bg: #10151d;
  --it-dk-field: #0a0e14;
  --it-dk-line: #6b7686;
  --it-dk-text: #e8edf4;
  --it-dk-muted: #9aa5b5;
  --it-dk-accent: #fbbf24;
  color-scheme: dark;
  display: grid;
  width: min(100%, 400px);
  padding: 26px;
  border: 1px solid #252d39;
  border-radius: 16px;
  background: var(--it-dk-bg);
}
.it-dk__title {
  margin: 0 0 14px;
  color: var(--it-dk-text);
  font-size: 18px;
  font-weight: 700;
}
.it-dk__label {
  margin: 0 0 6px;
  color: var(--it-dk-muted);
  font-size: 14px;
  font-weight: 600;
}
.it-dk__input {
  box-sizing: border-box;
  width: 100%;
  min-height: 46px;
  margin-bottom: 16px;
  padding: 0 12px;
  border: 1px solid var(--it-dk-line);
  border-radius: 8px;
  background: var(--it-dk-field);
  color: var(--it-dk-text);
  font: inherit;
  font-size: max(1rem, 16px);
  caret-color: var(--it-dk-accent);
}
.it-dk__input:last-child {
  margin-bottom: 0;
}
.it-dk__input--mono {
  font-family: ui-monospace, Consolas, monospace;
  font-size: 15px;
  letter-spacing: .02em;
}
.it-dk__input::placeholder {
  color: #8d98a8;
  opacity: 1;
}
.it-dk__input:hover {
  border-color: #8b95a5;
}
.it-dk__input:focus-visible {
  border-color: var(--it-dk-accent);
  outline: 2px solid transparent;
  box-shadow: 0 0 0 2px var(--it-dk-accent);
}
.it-dk__input::selection {
  background: #7c5a10;
  color: #fff;
}

Icons, addons and toggles

Fields that carry something extra inside the box: an icon and a clear button, currency and domain addons, a unit picker, a search control and a password reveal. The wrapper draws the border, so focus rings move to :focus-within.

Leading icon and clear button

  • position: absolute
  • :placeholder-shown
  • focus return

Type a name, then press the clear button.

The icon is absolutely positioned and the input gets matching left padding, so text never runs under it. The clear button only exists while the field has a value (input:placeholder-shown hides it), it is a real button with an accessible name, and it sends focus back to the field after clearing.

View code
.it-ic {
  width: min(100%, 380px);
  padding: 24px;
  border-radius: 16px;
  background: #fff;
  box-shadow: 0 12px 32px rgba(17, 24, 39, .08);
}
.it-ic__label {
  display: block;
  margin-bottom: 8px;
  color: #1f2430;
  font-size: 15px;
  font-weight: 600;
}
.it-ic__wrap {
  position: relative;
}
.it-ic__icon {
  position: absolute;
  top: 50%;
  left: 14px;
  fill: none;
  stroke: #5b6170;
  stroke-width: 2;
  stroke-linecap: round;
  transform: translateY(-50%);
  pointer-events: none;
}
.it-ic__input {
  box-sizing: border-box;
  width: 100%;
  height: 48px;
  padding: 0 48px 0 44px;
  border: 1.5px solid #747a88;
  border-radius: 999px;
  background: #f7f8fa;
  color: #1f2430;
  font: inherit;
  font-size: max(1rem, 16px);
  transition: border-color .15s ease, background-color .15s ease, box-shadow .15s ease;
}
.it-ic__input::placeholder {
  color: #646a78;
  opacity: 1;
}
.it-ic__input:focus-visible {
  border-color: #e0531f;
  outline: 3px solid transparent;
  background: #fff;
  box-shadow: 0 0 0 3px rgba(224, 83, 31, .3);
}
.it-ic__clear {
  position: absolute;
  top: 50%;
  right: 6px;
  display: grid;
  place-items: center;
  width: 36px;
  height: 36px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: #3e4352;
  cursor: pointer;
  transform: translateY(-50%);
}
.it-ic__clear svg {
  fill: none;
  stroke: currentColor;
  stroke-width: 2.4;
  stroke-linecap: round;
}
.it-ic__clear:hover {
  background: #eceef2;
}
.it-ic__clear:focus-visible {
  outline: 2px solid #e0531f;
  outline-offset: 0;
}
.it-ic__input:placeholder-shown + .it-ic__clear {
  display: none;
}

Currency and domain addons

  • flex group
  • :focus-within
  • inputmode="decimal"
https:// .shopkit.io

Prefix and suffix addons sit next to the input inside one flex wrapper that owns the border, and :focus-within draws the ring around the whole group. The currency is in the label text, so the dollar sign can be aria-hidden; the domain parts are linked with aria-describedby so a screen reader hears the full address. In a narrow container the https:// part is dropped to leave room for typing.

View code
.it-af {
  container-type: inline-size;
  display: grid;
  width: min(100%, 420px);
  padding: 24px;
  border-radius: 14px;
  background: #fff;
  box-shadow: 0 12px 32px rgba(17, 24, 39, .08);
}
.it-af__label {
  margin: 0 0 8px;
  color: #172033;
  font-size: 15px;
  font-weight: 600;
}
.it-af__group {
  display: flex;
  align-items: stretch;
  min-height: 46px;
  margin-bottom: 18px;
  overflow: hidden;
  border: 1.5px solid #6c7486;
  border-radius: 8px;
  background: #fff;
  transition: border-color .15s ease, box-shadow .15s ease;
}
.it-af__group:last-child {
  margin-bottom: 0;
}
.it-af__group:focus-within {
  border-color: #0d6efd;
  outline: 3px solid transparent;
  box-shadow: 0 0 0 3px rgba(13, 110, 253, .3);
}
.it-af__add {
  display: flex;
  flex: none;
  align-items: center;
  padding: 0 12px;
  border-right: 1px solid #d3d7df;
  background: #f1f3f7;
  color: #3f4759;
  font-size: 15px;
  white-space: nowrap;
}
.it-af__add--end {
  border-right: 0;
  border-left: 1px solid #d3d7df;
}
.it-af__input {
  box-sizing: border-box;
  flex: 1;
  width: 100%;
  min-width: 0;
  padding: 0 12px;
  border: 0;
  background: transparent;
  color: #172033;
  font: inherit;
  font-size: max(1rem, 16px);
  font-variant-numeric: tabular-nums;
}
.it-af__input::placeholder {
  color: #646c7d;
  opacity: 1;
}
.it-af__input:focus {
  outline: none;
}
@container (max-width: 360px) {
  .it-af__add {
    padding: 0 8px;
    font-size: 14px;
  }
  .it-af__input {
    padding: 0 8px;
  }
  .it-af__add--scheme {
    display: none;
  }
}

Number field with a unit select

  • appearance: none
  • select inside group
  • unit conversion

Parcel details

Up to 30 kg per parcel. We round to one decimal.

Change the unit to pounds.

A value and its unit belong together, so the select lives inside the field border instead of floating next to it. Both controls keep their own labels (the unit label is visually hidden). Switching the unit converts the value, which saves the user from doing kilogram math in their head.

View code
.it-un {
  width: min(100%, 360px);
  padding: 24px;
  border-radius: 4px;
  background: #fffdf7;
  box-shadow: 0 0 0 1px #e6e0cf, 0 14px 30px rgba(55, 48, 30, .1);
}
.it-un__title {
  margin: 0 0 16px;
  color: #2a2618;
  font-size: 18px;
  font-weight: 700;
}
.it-un__label {
  display: block;
  margin-bottom: 8px;
  color: #2a2618;
  font-size: 15px;
  font-weight: 600;
}
.it-un__group {
  display: flex;
  height: 50px;
  border: 2px solid #2a2618;
  border-radius: 4px;
  background: #fff;
}
.it-un__group:focus-within {
  outline: 3px solid #e2a400;
  outline-offset: 2px;
}
.it-un__input {
  box-sizing: border-box;
  flex: 1;
  min-width: 0;
  padding: 0 14px;
  border: 0;
  background: transparent;
  color: #2a2618;
  font: inherit;
  font-size: 20px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.it-un__input:focus,
.it-un__select:focus {
  outline: none;
}
.it-un__select {
  -webkit-appearance: none;
  appearance: none;
  flex: none;
  width: 72px;
  padding: 0 28px 0 14px;
  border: 0;
  border-left: 2px solid #2a2618;
  border-radius: 0;
  background: #f4efe0 url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' fill='none' stroke='%232a2618' stroke-width='2'/%3E%3C/svg%3E") no-repeat right 10px center;
  color: #2a2618;
  font: inherit;
  font-size: 16px;
  font-weight: 700;
  cursor: pointer;
}
.it-un__select:focus-visible {
  background-color: #ffe9a8;
}
.it-un__help {
  margin: 8px 0 0;
  color: #57503b;
  font-size: 14px;
}
.it-un__sr {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

Password field with a show toggle

  • aria-pressed
  • type swap
  • autocomplete="new-password"

Use 12 or more characters. A short phrase works well.

Type a password, then press the eye button.

The toggle is a button with a fixed accessible name, "Show password", and aria-pressed carries the state, so the announcement stays consistent. Clicking swaps the input type between password and text and keeps the caret where it was. autocomplete="new-password" lets password managers offer a generated one.

View code
.it-pw {
  width: min(100%, 380px);
  padding: 26px;
  border-radius: 20px;
  background: #fff;
  box-shadow: 0 12px 32px rgba(17, 24, 39, .08);
}
.it-pw__label {
  display: block;
  margin-bottom: 8px;
  color: #13261b;
  font-size: 15px;
  font-weight: 600;
}
.it-pw__wrap {
  position: relative;
}
.it-pw__input {
  box-sizing: border-box;
  width: 100%;
  height: 50px;
  padding: 0 56px 0 16px;
  border: 1.5px solid #66736b;
  border-radius: 12px;
  background: #fff;
  color: #13261b;
  font: inherit;
  font-size: max(1rem, 16px);
  letter-spacing: .04em;
}
.it-pw__input:focus-visible {
  border-color: #15803d;
  outline: 3px solid transparent;
  box-shadow: 0 0 0 3px rgba(21, 128, 61, .3);
}
.it-pw__toggle {
  position: absolute;
  top: 50%;
  right: 4px;
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 0;
  border-radius: 10px;
  background: transparent;
  color: #3d4a42;
  cursor: pointer;
  transform: translateY(-50%);
}
.it-pw__toggle:hover {
  background: #eef3ef;
}
.it-pw__toggle:focus-visible {
  outline: 2px solid #15803d;
  outline-offset: -2px;
}
.it-pw__eye {
  fill: none;
  stroke: currentColor;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.it-pw__toggle[aria-pressed="true"] .it-pw__slash {
  display: none;
}
.it-pw__toggle[aria-pressed="true"] {
  color: #15803d;
}
.it-pw__rule {
  margin: 8px 0 0;
  color: #47544c;
  font-size: 14px;
}

Validation, counters and states

How a text input talks back: a character counter, inline errors that wait for the user, a textarea that grows with its content, disabled and read-only fields, and autofill styling that keeps the cue visible.

Character counter with maxlength

  • maxlength
  • aria-describedby
  • polite live region
0 / 60

Type a headline.

maxlength does the hard limit, the counter just makes it visible. The visible count is tied to the field with aria-describedby, and a separate polite live region speaks only from ten characters left, so a screen reader is not interrupted on every keystroke. The bar is a functional fill, not decoration.

View code
.it-ct {
  position: relative;
  width: min(100%, 420px);
  padding: 24px;
  border-radius: 14px;
  background: #fff;
  box-shadow: 0 12px 32px rgba(17, 24, 39, .08);
}
.it-ct__top {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 4px 12px;
  margin-bottom: 8px;
}
.it-ct__label {
  color: #1b1f2a;
  font-size: 15px;
  font-weight: 600;
}
.it-ct__count {
  color: #50586a;
  font-size: 14px;
  font-variant-numeric: tabular-nums;
}
.it-ct__input {
  box-sizing: border-box;
  width: 100%;
  height: 48px;
  padding: 0 14px;
  border: 1.5px solid #6b7385;
  border-radius: 10px 10px 0 0;
  background: #fff;
  color: #1b1f2a;
  font: inherit;
  font-size: max(1rem, 16px);
}
.it-ct__input::placeholder {
  color: #646c7d;
  opacity: 1;
}
.it-ct__input:focus-visible {
  border-color: #0e7490;
  outline: 3px solid transparent;
  box-shadow: 0 0 0 3px rgba(14, 116, 144, .28);
}
.it-ct__bar {
  display: block;
  height: 4px;
  overflow: hidden;
  border-radius: 0 0 4px 4px;
  background: #e3e6ec;
}
.it-ct__bar i {
  display: block;
  width: 0;
  height: 100%;
  background: #0e7490;
}
.it-ct--warn .it-ct__count {
  color: #a3330f;
  font-weight: 700;
}
.it-ct--warn .it-ct__bar i {
  background: #c2410c;
}
.it-ct__sr {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
@media (prefers-reduced-motion: no-preference) {
  .it-ct__bar i {
    transition: width .15s linear;
  }
}

Inline validation with :user-invalid

  • :user-invalid
  • aria-invalid
  • pattern

3 to 20 characters: lowercase letters, numbers and hyphens.

Type "Ana B" and press Continue.

The error border and message key off :user-invalid, so nothing turns red before the user has typed and left the field. A short script mirrors the state into aria-invalid and writes a specific message into the element named by aria-describedby. The form uses novalidate, and a submit attempt adds a class that shows errors on untouched fields too.

View code
.it-va {
  width: min(100%, 400px);
  margin: 0;
  padding: 26px;
  border-radius: 16px;
  background: #fff;
  box-shadow: 0 12px 32px rgba(17, 24, 39, .08);
}
.it-va__label {
  display: block;
  margin-bottom: 8px;
  color: #1c1f26;
  font-size: 15px;
  font-weight: 600;
}
.it-va__wrap {
  position: relative;
}
.it-va__input {
  box-sizing: border-box;
  width: 100%;
  height: 48px;
  padding: 0 44px 0 14px;
  border: 1.5px solid #6b7180;
  border-radius: 10px;
  background: #fff;
  color: #1c1f26;
  font: inherit;
  font-size: max(1rem, 16px);
  transition: border-color .15s ease, box-shadow .15s ease;
}
.it-va__input:focus-visible {
  border-color: #2563eb;
  outline: 3px solid transparent;
  box-shadow: 0 0 0 3px rgba(37, 99, 235, .3);
}
.it-va__input:user-invalid,
.it-va--tried .it-va__input:invalid {
  border-color: #c0262d;
  box-shadow: none;
}
.it-va__input:user-invalid:focus-visible,
.it-va--tried .it-va__input:invalid:focus-visible {
  box-shadow: 0 0 0 3px rgba(192, 38, 45, .28);
}
.it-va__input:user-valid {
  border-color: #157a3c;
}
.it-va__ok {
  position: absolute;
  top: 50%;
  right: 14px;
  display: none;
  fill: none;
  stroke: #157a3c;
  stroke-width: 2.6;
  stroke-linecap: round;
  stroke-linejoin: round;
  transform: translateY(-50%);
}
.it-va__input:user-valid + .it-va__ok {
  display: block;
}
.it-va__hint {
  margin: 8px 0 0;
  color: #4e5462;
  font-size: 14px;
}
.it-va__error {
  margin: 6px 0 0;
  color: #b0222a;
  font-size: 14px;
  font-weight: 600;
}
.it-va__error:empty {
  display: none;
}
.it-va__error::before {
  content: "";
  display: inline-block;
  width: 7px;
  height: 7px;
  margin-right: 8px;
  border-radius: 50%;
  background: currentColor;
  vertical-align: middle;
}
.it-va__btn {
  min-height: 46px;
  margin-top: 18px;
  padding: 0 22px;
  border: 0;
  border-radius: 10px;
  background: #1c1f26;
  color: #fff;
  font: inherit;
  font-size: 15px;
  font-weight: 600;
  cursor: pointer;
}
.it-va__btn:focus-visible {
  outline: 3px solid #2563eb;
  outline-offset: 2px;
}

Auto-growing textarea

  • field-sizing: content
  • @supports
  • max-block-size

4 out of 5

Loved the room, but breakfast ended at nine and nobody told us at check-in.

Guest review, Marlow Inn

Type a few lines of reply.

field-sizing: content lets the textarea grow with each new line, with no script. min-block-size keeps it from collapsing to one line and max-block-size hands over to scrolling for long replies. Browsers without the property read the @supports fallback: three rows plus a vertical resize handle.

View code
.it-ta {
  width: min(100%, 440px);
  padding: 24px;
  border-radius: 16px;
  background: #fff;
  box-shadow: 0 12px 32px rgba(17, 24, 39, .08);
}
.it-ta__review {
  margin-bottom: 18px;
  padding: 14px 16px;
  border-left: 3px solid #b45309;
  border-radius: 0 10px 10px 0;
  background: #fbf5ec;
}
.it-ta__stars {
  margin: 0 0 4px;
  color: #8a3f06;
  font-size: 13px;
  font-weight: 700;
}
.it-ta__quote {
  margin: 0;
  color: #2b2118;
  font-family: "Fraunces", serif;
  font-size: 17px;
  line-height: 1.4;
}
.it-ta__by {
  margin: 6px 0 0;
  color: #5c4d3f;
  font-size: 13px;
}
.it-ta__label {
  display: block;
  margin-bottom: 8px;
  color: #2b2118;
  font-size: 15px;
  font-weight: 600;
}
.it-ta__input {
  display: block;
  box-sizing: border-box;
  width: 100%;
  padding: 12px 14px;
  border: 1.5px solid #77695b;
  border-radius: 10px;
  background: #fff;
  color: #2b2118;
  font: inherit;
  font-size: max(1rem, 16px);
  line-height: 1.5;
  resize: vertical;
}
.it-ta__input::placeholder {
  color: #6c5e51;
  opacity: 1;
}
.it-ta__input:focus-visible {
  border-color: #b45309;
  outline: 3px solid transparent;
  box-shadow: 0 0 0 3px rgba(180, 83, 9, .28);
}
@supports (field-sizing: content) {
  .it-ta__input {
    field-sizing: content;
    min-block-size: calc(3 * 1.5em + 27px);
    max-block-size: 14em;
    resize: none;
  }
}

Disabled and read-only fields

  • :read-only:not(:disabled)
  • :disabled
  • readonly

Account

Verified. Contact support to change it.

Upgrade the plan to add seats.

Read-only and disabled look alike in most UIs and should not. The read-only email stays focusable, selectable and submitted, so it keeps full text contrast and gets a dashed edge plus a lock. The disabled plan field is skipped by the keyboard and not submitted; WCAG exempts inactive controls from contrast, so it can be muted.

View code
.it-st {
  display: grid;
  width: min(100%, 400px);
  padding: 24px;
  border-radius: 14px;
  background: #fff;
  box-shadow: 0 12px 32px rgba(17, 24, 39, .08);
}
.it-st__title {
  margin: 0 0 14px;
  color: #161a22;
  font-size: 18px;
  font-weight: 700;
}
.it-st__label {
  margin: 0 0 6px;
  color: #161a22;
  font-size: 14px;
  font-weight: 600;
}
.it-st__wrap {
  position: relative;
}
.it-st__input {
  box-sizing: border-box;
  width: 100%;
  height: 44px;
  margin-bottom: 14px;
  padding: 0 12px;
  border: 1.5px solid #6b7180;
  border-radius: 8px;
  background: #fff;
  color: #161a22;
  font: inherit;
  font-size: max(1rem, 16px);
}
.it-st__wrap .it-st__input,
.it-st__input[aria-describedby] {
  margin-bottom: 0;
}
.it-st__input:focus-visible {
  border-color: #7c3aed;
  outline: 3px solid transparent;
  box-shadow: 0 0 0 3px rgba(124, 58, 237, .28);
}
.it-st__input:read-only:not(:disabled) {
  padding-right: 40px;
  border-style: dashed;
  border-color: #6b7180;
  background: #f5f6f8;
  color: #161a22;
  cursor: default;
}
.it-st__input:disabled {
  border-color: #d5d8de;
  background: #eef0f3;
  color: #7d828e;
  cursor: not-allowed;
  opacity: 1;
  -webkit-text-fill-color: #7d828e;
}
.it-st__lock {
  position: absolute;
  top: 50%;
  right: 12px;
  fill: none;
  stroke: #4f5563;
  stroke-width: 1.8;
  transform: translateY(-50%);
}
.it-st__note {
  margin: 6px 0 16px;
  color: #4f5563;
  font-size: 13px;
}

Autofill styling that keeps the cue

  • :is(:-webkit-autofill, :autofill)
  • inset box-shadow
  • autocomplete tokens

Delivery address

Use the toggle, or let your browser fill the fields.

Chrome locks the autofill background and text color with !important, so the rule paints over them with an oversized inset box-shadow and -webkit-text-fill-color. Instead of hiding the cue, it swaps the stock blue for a tinted fill and an edge bar that match the brand. The toggle applies the same look, because a script cannot trigger real autofill. Firefox currently matches :autofill only on username and password fields.

View code
.it-auto {
  display: grid;
  width: min(100%, 400px);
  margin: 0;
  padding: 24px;
  border-radius: 16px;
  background: #fff;
  box-shadow: 0 12px 32px rgba(17, 24, 39, .08);
}
.it-auto__title {
  margin: 0 0 14px;
  color: #1d1b2e;
  font-size: 18px;
  font-weight: 700;
}
.it-auto__label {
  margin: 0 0 6px;
  color: #1d1b2e;
  font-size: 14px;
  font-weight: 600;
}
.it-auto__input {
  box-sizing: border-box;
  width: 100%;
  height: 46px;
  margin-bottom: 14px;
  padding: 0 12px;
  border: 1.5px solid #6e6b80;
  border-radius: 8px;
  background: #fff;
  color: #1d1b2e;
  font: inherit;
  font-size: max(1rem, 16px);
}
.it-auto__input--short {
  width: min(100%, 160px);
}
.it-auto__input:focus-visible {
  border-color: #be185d;
  outline: 3px solid transparent;
  box-shadow: 0 0 0 3px rgba(190, 24, 93, .28);
}
.it-auto__input:is(:-webkit-autofill, :autofill),
.it-auto--preview .it-auto__input {
  border-color: #be185d;
  -webkit-text-fill-color: #1d1b2e;
  box-shadow: inset 4px 0 0 #be185d, inset 0 0 0 1000px #fdf0f6;
}
.it-auto__input:is(:-webkit-autofill, :autofill):focus-visible,
.it-auto--preview .it-auto__input:focus-visible {
  box-shadow: inset 4px 0 0 #be185d, inset 0 0 0 1000px #fdf0f6, 0 0 0 3px rgba(190, 24, 93, .28);
}
.it-auto__btn {
  justify-self: start;
  min-height: 44px;
  margin-top: 4px;
  padding: 0 16px;
  border: 1.5px solid #1d1b2e;
  border-radius: 8px;
  background: #fff;
  color: #1d1b2e;
  font: inherit;
  font-size: 15px;
  font-weight: 600;
  cursor: pointer;
}
.it-auto__btn[aria-pressed="true"] {
  background: #1d1b2e;
  color: #fff;
}
.it-auto__btn:focus-visible {
  outline: 3px solid #be185d;
  outline-offset: 2px;
}

Special-purpose inputs

Three patterns built from plain text inputs plus a little script: a one-time code, a tag input and a large touch form sized so iOS Safari does not zoom on focus.

One-time code boxes

  • autocomplete="one-time-code"
  • paste distribution
  • fieldset
Enter the 6-digit code we sent to your phone ending in 21

Type or paste a six-digit code.

Six one-character inputs inside a fieldset whose legend says what the code is for. Each box gets inputmode="numeric" and a label like "Digit 2 of 6"; typing moves forward, Backspace on an empty box moves back, and pasting a full code fills every box. The first box carries autocomplete="one-time-code" so iOS and Android can offer the code from the SMS.

View code
.it-otp {
  width: min(100%, 420px);
  margin: 0;
  padding: 26px 22px;
  border-radius: 18px;
  background: #fff;
  box-shadow: 0 12px 32px rgba(17, 24, 39, .08);
}
.it-otp__set {
  margin: 0;
  padding: 0;
  border: 0;
  min-width: 0;
}
.it-otp__legend {
  margin-bottom: 16px;
  padding: 0;
  color: #14213d;
  font-size: 16px;
  font-weight: 600;
  line-height: 1.4;
}
.it-otp__row {
  display: flex;
  align-items: center;
  gap: clamp(4px, 1.6vw, 8px);
}
.it-otp__box {
  box-sizing: border-box;
  flex: 1 1 0;
  width: 0;
  min-width: 0;
  max-width: 54px;
  height: 60px;
  padding: 0;
  border: 1.5px solid #687188;
  border-radius: 10px;
  background: #f7f8fb;
  color: #14213d;
  font-family: ui-monospace, Consolas, monospace;
  font-size: 26px;
  font-weight: 700;
  text-align: center;
  caret-color: #2563eb;
}
.it-otp__box:focus-visible {
  border-color: #2563eb;
  outline: 3px solid transparent;
  background: #fff;
  box-shadow: 0 0 0 3px rgba(37, 99, 235, .3);
}
.it-otp__dash {
  flex: none;
  width: 10px;
  height: 2px;
  background: #687188;
}
.it-otp--done .it-otp__box {
  border-color: #157a3c;
  background: #effaf3;
}
.it-otp__status {
  min-height: 1.5em;
  margin: 14px 0 0;
  color: #126433;
  font-size: 14px;
  font-weight: 600;
}

Tag input with removable chips

  • textContent only
  • Enter and comma
  • live region
  • Figma
  • Copywriting

Press Enter or comma to add. Up to 8 skills.

Type a skill and press Enter.

The wrapper looks like one field, but it holds a list of chips plus a plain text input. Enter or a comma turns the text into a chip, Backspace in an empty field removes the last one, and each chip has its own remove button named after the tag. Chips are built with createElement and textContent, so typed text is never parsed as HTML.

View code
.it-tag {
  position: relative;
  width: min(100%, 440px);
  padding: 24px;
  border-radius: 14px;
  background: #fff;
  box-shadow: 0 12px 32px rgba(17, 24, 39, .08);
}
.it-tag__label {
  display: block;
  margin-bottom: 8px;
  color: #1a1d24;
  font-size: 15px;
  font-weight: 600;
}
.it-tag__box {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  min-height: 48px;
  padding: 6px;
  border: 1.5px solid #6b7180;
  border-radius: 10px;
  background: #fff;
  cursor: text;
}
.it-tag__box:focus-within {
  border-color: #0f766e;
  outline: 3px solid transparent;
  box-shadow: 0 0 0 3px rgba(15, 118, 110, .28);
}
.it-tag__list {
  display: contents;
  margin: 0;
  padding: 0;
  list-style: none;
}
.it-tag__chip {
  display: inline-flex;
  align-items: center;
  max-width: 100%;
  margin: 0;
  padding: 0 0 0 10px;
  border-radius: 6px;
  background: #ddf1ee;
  color: #0b4b45;
  font-size: 14px;
  font-weight: 600;
  line-height: 32px;
}
.it-tag__chip span {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.it-tag__x {
  display: grid;
  place-items: center;
  flex: none;
  width: 30px;
  height: 32px;
  padding: 0;
  border: 0;
  border-radius: 0 6px 6px 0;
  background: transparent;
  color: #0b4b45;
  font: inherit;
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
}
.it-tag__x:hover {
  background: #c3e6e1;
}
.it-tag__x:focus-visible {
  outline: 2px solid #0f766e;
  outline-offset: -2px;
}
.it-tag__input {
  box-sizing: border-box;
  flex: 1 1 120px;
  min-width: 0;
  height: 32px;
  padding: 0 6px;
  border: 0;
  background: transparent;
  color: #1a1d24;
  font: inherit;
  font-size: max(1rem, 16px);
}
.it-tag__input:focus {
  outline: none;
}
.it-tag__input::placeholder {
  color: #646a78;
  opacity: 1;
}
.it-tag__help {
  margin: 8px 0 0;
  color: #4f5563;
  font-size: 14px;
}
.it-tag__sr {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

Large touch fields on mobile

  • font-size: max(1rem, 16px)
  • 56px targets
  • enterkeyhint

Osteria Lume

Join tonight’s waitlist

Current wait: about 25 minutes

We text you when the table is ready.

Fill in the form and join the waitlist.

A waitlist form sized for thumbs. Field text never drops below 16px, the size under which iOS Safari zooms the page when an input gets focus, and every field and the button are at least 56px tall. type="tel", autocomplete and enterkeyhint pick the right keyboard and the right return key label.

View code
.it-tch {
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  height: 100%;
  margin: 0;
  padding: 20px 20px 24px;
  overflow: auto;
  background: #f6f1ea;
}
.it-tch__head {
  margin-bottom: 22px;
}
.it-tch__brand {
  margin: 0;
  color: #7a2e1d;
  font-family: "Fraunces", serif;
  font-size: 17px;
  font-weight: 600;
}
.it-tch__title {
  margin: 6px 0 4px;
  color: #231a14;
  font-family: "Fraunces", serif;
  font-size: 28px;
  line-height: 1.15;
}
.it-tch__wait {
  margin: 0;
  color: #5a4b3f;
  font-size: 15px;
}
.it-tch__label {
  margin-bottom: 8px;
  color: #231a14;
  font-size: 16px;
  font-weight: 600;
}
.it-tch__input {
  box-sizing: border-box;
  width: 100%;
  min-height: 56px;
  margin-bottom: 18px;
  padding: 0 16px;
  border: 2px solid #6f5f52;
  border-radius: 14px;
  background: #fff;
  color: #231a14;
  font: inherit;
  font-size: max(1rem, 17px);
}
.it-tch__input::placeholder {
  color: #6f6157;
  opacity: 1;
}
.it-tch__input:focus-visible {
  border-color: #7a2e1d;
  outline: 3px solid transparent;
  box-shadow: 0 0 0 3px rgba(122, 46, 29, .3);
}
.it-tch__note {
  margin: -10px 0 22px;
  color: #5a4b3f;
  font-size: 14px;
}
.it-tch__btn {
  min-height: 56px;
  margin-top: auto;
  border: 0;
  border-radius: 14px;
  background: #7a2e1d;
  color: #fff;
  font: inherit;
  font-size: 17px;
  font-weight: 700;
  cursor: pointer;
}
.it-tch__btn:active {
  background: #5f2215;
}
.it-tch__btn:focus-visible {
  outline: 3px solid #231a14;
  outline-offset: 3px;
}
.it-tch__status {
  min-height: 1.4em;
  margin: 12px 0 0;
  color: #3b2f26;
  font-size: 14px;
  text-align: center;
}

How do you select a text input in CSS?

Write input[type="text"], or add a class to the field and select that. The attribute version catches every single-line text field on the page. A class only touches the ones you tag.

The WHATWG HTML Living Standard describes the Text state as a one-line plain text edit control that strips line feeds from its value. Email, search, tel and url fields share the same box and font properties, so one grouped selector covers them.

input:where(:not([type]), [type="text"], [type="email"], [type="search"], [type="tel"], [type="url"]) {
  font: inherit;
}

The :not([type]) part matters. An input with no type attribute falls back to the Text state but never matches input[type="text"].

Author rules always beat the user agent stylesheet, which gives every input its default border, padding and font. Even a bare type selector overrides those defaults.

Counted as ID-CLASS-TYPE weights (MDN), a bare input is 0-0-1 and .field is 0-1-0. input[type="text"] comes out at 0-1-1.

That means the attribute selector outranks the class, so a .field override of the same property loses. It also ties with input:focus at 0-1-1, and the later rule wins a tie. This is how a base rule written below the focus rule erases the focus style, and it’s an annoying bug to track down.

A specificity calculator settles these disputes in seconds. Wrapping the base selector in :where() drops its weight to zero, so any later class overrides it cleanly. I’d do that on any shared stylesheet.

Box model settings that size a text input

A text input uses content-box by default, so width covers the content area only and padding and border are added outside it (MDN). Set width: 100% with 12px of side padding and a 1px border, and the field overflows its parent by 26px.

.field {
  box-sizing: border-box;
  width: 100%;
  padding: 0.75rem 1rem;
  border: 1px solid #767676;
  border-radius: 6px;
}

Under border-box the width is the total, padding and border included, which is why I set it on every field. Padding is the gap between the border and the typed text. The border is the field boundary, and on most designs it’s the only visible edge. border-radius rounds the corners.

Corner values are easy to judge by eye, but a border radius generator gives a live preview when you want a pill shape or a single rounded corner.

For an underline-only field, set border: 0 and then add border-bottom on its own.

A restyled border carries a contrast requirement. WCAG 2.2 (W3C, 2023) asks for 3:1 against adjacent colors on visual information needed to identify a control, and the exemption for browser-default appearance ends once the author modifies it.

By the WCAG formula, #ccc on white lands near 1.6:1, #949494 near 3:1 and #767676 near 4.5:1. That’s why the border in the example above is #767676. Check color contrast against the real page background, because a gray card behind the field changes the result.

Font inheritance and the 16px rule on iOS

A text input ignores the page font until you set font: inherit, and Safari on iOS zooms the page when a focused field renders below 16px.

MDN’s form styling guide notes that some widgets do not inherit font-family and font-size from their parents. Firefox’s user agent stylesheet (forms.css) gives inputs the operating system’s textfield font through font: -moz-field.

input,
textarea,
select {
  font: inherit;
  font-size: max(1rem, 16px);
}

Setting the field to 16px or larger stops the zoom everywhere, and it stays flexible if you write it in rem. Capping zoom in the viewport meta tag also stops the jump, but it changes zoom behavior for the whole page.

Telerik’s documentation calls the zoom intended behavior, and the West Wind Web Log (2023) ties it to a rendered text size under 16px. Neither source describes it as a bug.

Rem units can hide the problem. Rem units resolve against the root size, so a 62.5% root reset makes 1rem equal 10px and pushes a 1rem input far under the threshold (a root below 16px is one of the causes West Wind lists).

Skip the viewport cap. West Wind lists it only as a selective workaround for Safari on iOS, and a page-wide viewport setting is a heavy tool for a one-control problem.

Which pseudo-classes style the states of a text input?

A text input mostly needs :hover, :focus, :disabled, :read-only and :invalid. :user-invalid (CSS Selectors Level 4) is the better pick over :invalid when errors should appear only after the user acts.

Pseudo-classMatches whenTypical style
:hoverPointer sits over the fieldDarker border color
:disabledThe disabled attribute is setMuted background, not-allowed cursor
:read-onlyThe readonly attribute is setFlat background, normal text color
:invalidValue fails its constraints, empty required fields includedError border color
:user-invalidValue is invalid after the user interactedError border plus message

Store state colors in custom properties such as --field-border and --field-border-error, then swap the variable per state instead of rewriting declarations. A short transition: border-color 150ms ease softens each change.

Disabled and read-only fields

Disabled fields get a pass on contrast. WCAG 1.4.11 exempts inactive components (W3C, 2023), so a muted gray is acceptable there.

Readonly fields don’t get that treatment, since they stay focusable and readable. Their text and boundary keep full contrast. Readonly values still submit with the form, which disabled ones never do.

Older compatibility write-ups from CSS-Tricks and Codrops report disabled inputs matching :read-write in major browsers, against the specification. Write input:read-only:not(:disabled) so the rule does not depend on how a browser classifies disabled fields.

Validation states

:invalid matches an empty required field the moment the page loads. Error borders can appear before anyone types a character.

:user-invalid waits for interaction or a submit attempt, which is the behavior most forms want. The web-features dataset lists support in Chrome 119, Firefox 88 and Safari 16.5, with Baseline status since November 2023 and Widely available status since May 2026.

Older Firefox versions added a red glow through box-shadow on invalid fields, but Firefox 88 removed that default style (Mozilla bug 1693969). Current browsers draw no error ring of their own, so the error style is entirely yours.

Pair the red border with visible error text, because color alone does not tell every user what went wrong. Accessible form patterns cover label association and message placement in more depth.

How to style the focus indicator without failing WCAG

Replace the default focus outline with a custom indicator that holds at least 3:1 contrast against its surroundings, and never remove it without a replacement.

SC 1.4.11 in WCAG 2.2 (W3C, 2023) sets that bar at 3:1 for a control’s boundary and states against adjacent colors, at Level AA. SC 2.4.13 goes further at Level AAA and asks for a minimum indicator area equal to a perimeter 2 CSS pixels thick, with a 3:1 contrast change between focused and unfocused pixels. SC 2.4.11, also Level AA, says the focused field can’t be entirely hidden by sticky headers or pop-ups.

On the browser side, :focus-visible became available in all major browsers in March 2022, and MDN lists it as Baseline Widely available (as of October 2026).

For text inputs, :focus and :focus-visible behave alike, because browsers show focus on a field after a mouse click too (web.dev). Use :focus-visible anyway, since it matches how the browser’s own heuristics work.

.field:focus-visible {
  outline: 3px solid transparent;
  outline-offset: 2px;
  border-color: #1a56db;
  box-shadow: 0 0 0 3px #1a56db;
}

The transparent outline looks invisible in normal rendering. In forced colors mode MDN notes that box-shadow is forced to none, and the outline is what stays visible in a system color.

GOV.UK Frontend added the same transparent-outline technique to the focus states of its radio and checkbox controls for Windows High Contrast Mode (pull request 2264). GOV.UK also pairs yellow with black and a thick bottom border so its focus states meet SC 1.4.11 on any background, and the NHS service manual describes its text input version as a yellow outline with a thick black border.

A box-shadow generator helps tune spread and color of the ring. The shadow handles the look, and outline-offset keeps the invisible outline from touching the border.

This setup still breaks in a few places. A pale brand-blue ring on white falls under 3:1, and a shadow-only ring vanishes in forced colors mode. A sticky header that fully covers the focused field fails SC 2.4.11 as well, though partial overlap does not (Vispero).

MDN’s :focus reference states the rule plainly: removing the outline is acceptable only when the replacement passes SC 1.4.11.

How to style placeholder text

Target it with input::placeholder, set color and opacity explicitly, and use :placeholder-shown when the field itself needs a different look while the hint is visible.

The pseudo-element accepts only the property subset that applies to ::first-line, according to MDN. Color, font properties and opacity work. Padding and border do not.

input::placeholder {
  color: #595959;
  opacity: 1;
}

Browsers disagree on the default. Chrome paints placeholder text #757575 (Chromium html.css), while Firefox uses the input’s own color at 54% opacity (MDN), so the same stylesheet looks lighter in Firefox.

That default has a contrast cost. By the WCAG formula, #333 text at 54% opacity over white lands near #919191, about 3.2:1, under the 4.5:1 that SC 1.4.3 asks for normal-size text.

MDN also warns about the opposite problem. Placeholder text with strong contrast can be mistaken for an entered value, and the hint vanishes the moment someone types.

My rule is to keep the placeholder to a short example, put instructions in a visible hint or label, and never let it replace the <label> element.

::placeholder styles the hint text itself, while :placeholder-shown styles the input for as long as the hint displays. Both have been available in every major browser since January 2020 and are Baseline Widely available (MDN, as of October 2026). Older tutorials still teach ::-webkit-input-placeholder and :-moz-placeholder, which the unprefixed selector made unnecessary.

The pseudo-class earns its place on narrow fields. text-overflow: ellipsis on the shown state trims a long hint that would otherwise be cropped mid-word.

In forced colors mode (Windows High Contrast Mode), placeholder text renders with the same styling as typed text, per MDN. Any gray you chose is dropped there.

Caret color and autofill styling

caret-color recolors the blinking insertion pointer, and :autofill targets fields the browser filled in, though Chrome locks the autofill background and text color behind !important.

The caret property affects only the insertion caret, not the mouse cursor, and its default auto resolves to the text color (MDN). It has been Baseline Widely available since July 2022, with Chrome 57, Firefox 53 and Safari 11.1 as the first supporting versions (web-features dataset).

.field {
  caret-color: #1a56db;
}

The value auto is not animatable, so a transition from auto to a color snaps instead of fading. Set the caret color on the base rule if a state change touches it.

For autofill, match both :-webkit-autofill (the prefix from WebKit) and :autofill inside :is(), so one unknown selector cannot void the rule. Chrome 110 (February 2023) and Safari 15 support the standard name, but it is not Baseline: Firefox matches only autofilled username and password fields (MDN browser compatibility data). Either selector stops matching once the user edits the field (MDN).

Borders, outlines and radii on autofilled fields behave normally. Background and text color do not, at least on the selected autofill state, because Chrome’s internal stylesheet sets background-color (rgb(232 240 254) in light mode), background-image and color with !important, which MDN documents on its :autofill page.

The workaround is an oversized inset box-shadow that paints over the locked background, plus -webkit-text-fill-color to replace the locked text color, a technique CSS-Tricks documents. It exploits paint order, not a specification, so a Chrome stylesheet update can break it.

input:is(:-webkit-autofill, :autofill) {
  -webkit-text-fill-color: #111;
  box-shadow: 0 0 0 1000px #fff inset;
}

input:is(:-webkit-autofill, :autofill):focus-visible {
  box-shadow: 0 0 0 1000px #fff inset, 0 0 0 3px #1a56db;
}

The trick spends the same property that a shadow-based focus ring uses. The second rule repeats the fill next to the ring because an element gets only one box-shadow value. On focus, .field:focus-visible (0-2-0) outranks the first autofill rule (0-1-1) and would replace the fill, bringing Chrome’s background back. The second rule (0-2-1) outranks both and carries the fill and the ring together.

The workaround has costs. A fixed fill color hides the cue that autofill happened, a trade-off raised in a BS Base theme issue on Drupal.org (3256665) when it reset the color. Chrome also changes font size during autofill selection with !important rules, and that cannot be overridden (same issue). WebKit bug 66032, which asks for author control of autofill colors, is still the reference MDN points to.

How Chrome, Firefox and Safari render a default text input

Each browser engine ships its own user agent stylesheet, so an unstyled text input differs across Chrome, Firefox and Safari in padding, font, placeholder color and autofill selector.

PropertyChromeFirefox
Padding and border1px block and 2px inline padding, 2px inset border (Chromium html.css)1px block and 2px inline padding (forms.css)
FontPlatform default font (macOS, per MDN)OS textfield font via font: -moz-field
Placeholder color#757575Input color at 54% opacity
Autofill selector:-webkit-autofill:autofill

The values live in separate files, Chromium’s html.css and Firefox’s forms.css (Searchfox), which is why no single reset value fits every engine. Safari follows WebKit’s own stylesheet, and that lineage is why the -webkit-autofill prefix still works in Chrome and Safari.

Stylesheets change between releases, so treat the table as a starting point for cross-browser compatibility checks. In Chrome DevTools, the Styles pane labels inherited rules as the “user agent stylesheet” and the Computed pane shows the final resolved value (Chrome for Developers).

appearance: none strips native styling and keeps the control’s behavior (MDN). The unprefixed property reached Chrome 84, Firefox 80 and Safari 15.4, and the web-features dataset lists Baseline Widely available since September 2024.

Normalize.css version 8.0.1 keeps useful defaults instead of wiping them, and its changelog lists form inputs inheriting font-family and placeholders normalized in Chrome, Edge and Safari.

Its README still lists Internet Explorer 10+ and Safari 8+ as supported browsers, so much of it solves problems a current project does not have. For one text input, a short reset beats the import: font: inherit, box-sizing: border-box and an explicit placeholder color.

Can I use and MDN’s compatibility tables show per-version support for each selector covered here. Check them before depending on anything newer than the Baseline dates above.

Plain CSS or a framework for text inputs?

Write plain CSS for one or two forms, and use Bootstrap or Tailwind CSS when the project already runs on one.

Plain CSS means every rule is yours and there’s no stock focus ring to fight. You also own every cross-browser reset, which gets tedious past a couple of forms.

Bootstrap’s .form-control brings padding of .375rem .75rem and a .15s border and shadow transition, with focus styling driven by variables (Bootstrap 5.3 documentation). The catch is the default focus ring, a 0.25rem shadow drawn at 0.25 opacity in the 5.3 build, so test it against your contrast target.

The Bootstrap input styling classes cover sizes through .form-control-sm and .form-control-lg, and overriding them follows the same specificity rules as any other selector.

With Tailwind CSS, @tailwindcss/forms resets inputs with :where() selectors, so utilities override it without a specificity fight. Version 4 targets Safari 16.4+, Chrome 111+ and Firefox 128+ and will not work in older browsers (Tailwind CSS v4 upgrade guide).

Tailwind’s own defaults changed in early 2025 and hit text inputs directly. Version 4 moved the default border color from gray-200 to currentColor and the default ring width from 3px to 1px, so a migrated input that relied on the old defaults now draws a border in its text color and a thinner ring.

Pairing styling inputs with Tailwind utility classes with explicit border and ring colors avoids that surprise. The forms plugin also offers a class strategy, which skips global styling and applies form-input only where you add it.

CSS Input Text FAQ

Do password and number inputs take the same CSS as a text input?

Yes for the shared properties. Padding, border, border-radius, font and the state pseudo-classes all apply.

Number fields add spin buttons and search fields add a clear button, so both need pseudo-element rules of their own.

How do you build a floating label with CSS?

Position the <label> over the field, then move and shrink it when the input has focus or holds text. The holds-text check uses input:not(:placeholder-shown), which needs a placeholder attribute, even a single space, because the pseudo-class matches only while a placeholder displays.

Keep the label a real <label> element. The placeholder is a trigger, not a replacement.

How do you put an icon inside a text input?

Wrap the input in a container set to position: relative and place the icon absolutely inside it. Then add padding on the icon’s side of the input so typed text never runs underneath.

A background-image on the input works for a static icon, with the same padding fix.

How do you style a text input for dark mode?

Set color-scheme: light dark on the root so the browser swaps its own field colors, then override with custom properties inside @media (prefers-color-scheme: dark). Chromium’s default stylesheet paints input background and text with the Field and FieldText system colors, and that property drives them.

Recheck the border and focus indicator on the dark surface. A ring that passes on white can fail on dark gray.

Can a CSS generator write the whole text input style?

No. A generator such as CSS Portal’s text input generator returns CSS plus a preview of one styled field.

Test the focus, disabled and invalid states yourself, because a preview shows only what you configured.

Setting Up a Shared Base Stylesheet for Text Inputs

Sizing and font go into a shared base stylesheet first, the focus indicator second, and browser-specific overrides last.

Sizing leads because every later rule paints inside the field’s final dimensions. The focus indicator precedes decoration because it is the one rule that decides accessibility conformance.

Browser-specific overrides go last since they depend on paint behavior no specification defines. An isolated layer limits the damage when a browser update breaks one, and the trade-off is that early builds look plain until that layer lands.

As verified in October 2026, the autofill workaround exists only because Chrome locks those colors, so delete it the day that lock disappears. Teams ready for the rest of the form can continue with styling the rest of the form.