Skip to content
Carl Victor Fontanos
Carl Victor Fontanos

Carl Victor Fontanos

Software Engineer

I build web applications and share what I learn along the way.

© 2026

Floating Labels with :placeholder-shown (No JavaScript, No Wrappers)

C
Carlo Fontanos
· 2 min read

:placeholder-shown matches an input exactly while its placeholder is visible - which is a proxy for "the field is empty". That one bit of state, readable from CSS, is the entire engine behind floating labels:

<div class="field">
    <input id="email" type="email" placeholder=" " required>
    <label for="email">Email address</label>
</div>
.field {
    position: relative;
}

.field label {
    position: absolute;
    inset-inline-start: 12px;
    top: 50%;
    translate: 0 -50%;
    color: #64748b;
    pointer-events: none;              /* clicks fall through to the input */
    transition: 0.15s ease;
}

/* Field has content OR focus: float the label up */
.field input:not(:placeholder-shown) + label,
.field input:focus + label {
    top: 0;
    font-size: 12px;
    background: var(--surface, #fff);
    padding-inline: 4px;
    color: #4f46e5;
}

Empty and unfocused: label sits inside like a placeholder. Focused or filled: it floats to the border. Browser autofill triggers it correctly too, because autofilled fields stop showing their placeholder - the exact bug JS implementations of this pattern historically had.

The two tricks hiding in there

placeholder=" " - a single space. :placeholder-shown only matches if a placeholder exists; you don't want visible placeholder text competing with the label, so an invisible one satisfies the selector. Ugly, load-bearing, and the part everyone forgets.

Label after input in the markup, so the sibling combinator (+) can select it from the input's state. Visually reordered by absolute positioning; the for/id association keeps semantics intact. (With :has() you can keep label-first markup - .field:has(input:focus) label - which is where I'd write this today; the sibling version works back to much older browsers.)

Other things :placeholder-shown is quietly great at

/* Show the "clear" button only when there's something to clear */
.search:has(input:not(:placeholder-shown)) .clear-btn { visibility: visible; }

/* Validation styling that waits until the user typed something */
input:not(:placeholder-shown):invalid { border-color: #dc2626; }

That second one is the poor man's :user-invalid for engines where you need broader reach - errors appear only after content exists, not on pristine forms.

Accessibility notes worth taking seriously

This pattern's virtue is that it uses a real label - screen readers announce it regardless of visual position, unlike placeholder-only forms (a known WCAG failure: placeholders vanish on input, leaving no visible field identity... which floating solves too). Keep contrast on the floated state, don't shrink below ~12px, and never encode critical instructions in a floating label - they belong in persistent hint text.

Support: :placeholder-shown is universal (it's from 2015!). The whole pattern - markup, CSS, autofill behavior - is one of those rare tricks with no JavaScript to break and no library to update.

C
Written by Carlo Fontanos

Full-stack web developer sharing practical tutorials and building tools that ship.

Got something on your mind?

My inbox is open - no forms disappearing into the void here.

  • Just say hello Found a tutorial useful? Spotted a mistake? Tell me.
  • Hire me for a project Have something custom in mind? Let's talk scope and timelines.
  • Product support Bought something here? I'll help you get it running.

I usually reply within 1-2 business days.

Message sent!

Your details are only used to reply to you.

Keep reading