Styling the placeholder with CSS

The placeholder attribute hints at what a field expects while it is still empty. Styling it once took four vendor prefixes; today a single ::placeholder selector is enough. Let us look at how to style the hint properly, which trick built on it makes a form easier to fill, and why it must never stand in for a field label.

The main point: a placeholder is not a label

The hint disappears at the first character typed. Someone interrupted halfway through the form can no longer tell what they are filling in; in some browsers a screen reader does not read it at all; browser autofill works worse. So every field needs a real <label>, while the placeholder carries only the format or an example.

DON'T: THE LABEL LIVES ONLY IN THE PLACEHOLDERPhone in +380 format…field empty — the hint is visible+380 67 502 45 84typing started — the hint is goneand what the field was, you can no longer tellDO: A SEPARATE LABEL, THE PLACEHOLDER SHOWS THE FORMATPhone+380 __ ___ __ __the label is always therePhone+380 67 502 45 84both a person and a screen reader read it
A label living only in the placeholder, and the correct version with a label
<!-- don't -->
<input type="tel" name="phone" placeholder="Phone in +380 format…">

<!-- do -->
<label for="phone">Phone</label>
<input type="tel" id="phone" name="phone"
       placeholder="+380 __ ___ __ __" autocomplete="tel">

Changing the colour and the font

One rule does it now — ::placeholder is supported by every current browser:

input::placeholder,
textarea::placeholder {
    color: #8A8A8A;
    font-style: italic;
    opacity: 1; /* Firefox dims the hint by default */
}

That opacity line is not padding: Firefox has always rendered the placeholder semi-transparent, and without it your chosen colour comes out paler than intended.

The set of properties that actually applies to the hint is small: colour, background, the whole font-* family, letter and word spacing, text-decoration, text-transform, line-height, opacity and text-overflow. Padding, borders and positioning do not apply — those belong to the field itself.

The old prefixes and why they are worth knowing

Articles from five years ago wrote the same effect like this:

::-webkit-input-placeholder { color: #8A8A8A; }
::-moz-placeholder          { color: #8A8A8A; }
:-moz-placeholder           { color: #8A8A8A; }
:-ms-input-placeholder      { color: #8A8A8A; }

The reason was that no standard existed and every engine implemented styling its own way: in IE and in Firefox before version 19 the placeholder counted as a pseudo-class, in WebKit and Blink as a pseudo-element. Today those lines matter only to projects supporting very old browsers. One detail worth remembering: these selectors cannot be joined with commas — a browser that does not recognise one of them discards the whole rule.

A long hint in a narrow field

On a phone fields get narrow and long text is cut off mid-word. An ellipsis looks tidier:

input::placeholder {
    text-overflow: ellipsis;
    white-space: nowrap;
    overflow: hidden;
}

The trick works, but it treats the symptom. If the hint does not fit, it is too long: the pattern “+380 __ ___ __ __” reads better than a full sentence stretched across the field.

Hiding the hint on focus

Browsers differ: some drop the placeholder as soon as the caret lands, others keep it until the first character. One line brings them in line:

input:focus::placeholder,
textarea:focus::placeholder {
    color: transparent;
}

Nicer still, with the hint sliding away:

input::placeholder {
    opacity: 1;
    transition: opacity .2s ease, transform .2s ease;
}

input:focus::placeholder {
    opacity: 0;
    transform: translateX(6px);
}

Field states and :placeholder-shown

A separate pseudo-class, :placeholder-shown, matches while the field is empty and the hint is visible. Tricks that used to need scripting now rest on it.

THREE FIELD STATES AND THEIR SELECTORSYour nameempty, hint visible::placeholder:placeholder-shownYour namecursor in the field:focus::placeholderthis is where the hint is usually hiddenMaxima value is present:not(:placeholder-shown)the label floats upThe :placeholder-shown selector fires while the field is empty — the floating-label trick rests on itand so does highlighting unfilled fields, without a line of JavaScript.
Three field states and the selectors that match them

The floating label — the label sits over an empty field and rises as soon as text appears. The whole effect in CSS:

<div class="field">
  <input type="text" id="name" placeholder=" ">
  <label for="name">Your name</label>
</div>
.field { position: relative; }

.field label {
    position: absolute;
    left: 12px;
    top: 12px;
    color: #8A8A8A;
    pointer-events: none;
    transition: transform .15s ease, font-size .15s ease;
}

/* field filled or focused — the label moves up */
.field input:not(:placeholder-shown) + label,
.field input:focus + label {
    transform: translateY(-22px);
    font-size: 12px;
}

The space in placeholder=" " is required: with no attribute at all, :placeholder-shown never matches.

The same selector highlights unfilled required fields, and only after the visitor has touched them:

input:required:not(:placeholder-shown):invalid {
    border-color: #C0392B;
}

Accessibility and dark mode

  • Contrast. Light grey hint text on white is the classic mistake. WCAG asks for at least 4.5:1 on body text; keep the hint subdued but readable.
  • Do not drop the label just to keep things clean. If the design calls for fields without a visible label, the label still has to exist in the markup — at the very least as aria-label.
  • Dark mode. Set the hint colour as a token and override it in a media query:
input::placeholder { color: #8A8A8A; }

@media (prefers-color-scheme: dark) {
    input::placeholder { color: #9B9B9B; }
}
  • Keep anything important out of the placeholder. Password requirements, date format, whether a field is required — that is text beside the field, not a hint that vanishes.

The short version

Styling the hint now takes ::placeholder, and the vendor prefixes only matter when very old browsers are on the list. For field states there is :placeholder-shown: it carries the floating label and the highlighting of empty fields, with no JavaScript. The main rule has not changed: the placeholder shows the format, the label says what the field is, and neither replaces the other.

If the form on your site works but visitors abandon it halfway, write to us — we will find where the leads are being lost.