9 CSS Input Focus Animations: One Line, Copy-Paste
A css input focus animation is what a text field does in one line of CSS the moment :focus, :focus-within, or a validity state kicks in — a label lifting, a
Auto-plays · click a tile to jump to its section · all nine in one zip
- 01 Floating label
- 02 Underline grow on focus
- 03 Glow via :has(:focus)
- 04 Search box expand
- 05 Placeholder slide
- 06 Error shake
- 07 Custom select open
- 08 Character count
- 09 Password eye toggle
All nine below share one thing the grid above can't offer: a real, typable <input>. They're ordered by how much of the state the browser already tracks on its own rather than by visual flash. Items 01 through 05 run on nothing but :focus, :focus-within, and :placeholder-shown, and item 06 hands the verdict to the browser's own HTML5 validity check. Items 07 through 09 are the three that need a small script, since opening a list, counting characters, and swapping an input's type are jobs no CSS selector can finish by itself. The grid above loops through all nine by itself too, because its frames sit inside iframes with no pointer able to reach them; everything further down, by contrast, is a genuine field that takes real typing and real focus.
01Floating label
The label sitting inside an empty field lifts to the top-left and shrinks the moment the input gets :focus or already holds a value, judged by :not(:placeholder-shown). It works well on a signup form's nickname or name field, where a form can't spare an extra line for a permanent label.
.fl__label {
position: absolute;
transform: translateY(-50%) scale(1);
transform-origin: left center;
transition: transform $duration $easing, color $duration $easing;
}
.fl__input:focus ~ .fl__label,
.fl__input:not(:placeholder-shown) ~ .fl__label {
transform: translateY(-24px) scale(.78);
color: $color;
}
02Underline grow on focus
Below a borderless input, a two-pixel span grows outward from the center via scaleX the moment focus lands anywhere inside the wrapper, tracked by :focus-within rather than the input's own :focus. Minimal settings forms and a single website or handle field both suit it, since there's no border left to redraw.
.ug__underline {
height: 2px;
background: $color;
transform: scaleX(0);
transform-origin: center;
transition: transform $duration $easing;
}
.ug:focus-within .ug__underline {
transform: scaleX(1);
}
03Glow via :has(:focus)
A ring that sits outside the input, not even its sibling, breathes larger and smaller on an infinite loop for as long as the input stays focused. Chrome and Edge have supported :has() since version 105, Safari since 15.4, and Firefox since 121, which is what lets the parent .fg detect a child's focus at all.
.fg__glow {
opacity: 0;
transform: scale(.92);
}
.fg:has(.fg__input:focus) .fg__glow {
animation: glow-pulse $duration $easing infinite;
}
@keyframes glow-pulse {
0%, 100% { opacity: .35; transform: scale(.92); }
50% { opacity: .9; transform: scale(1.04); }
}
04Search box expand
A pill-shaped search box sits narrow at 148px until :focus-within fires on the whole label, then widens to 240px and pushes whatever sits beside it. Unlike most of these nine, this one animates width itself rather than a transform, so a header search box needs that 240px cap to avoid overflowing a tight nav bar.
.se {
width: 148px;
transition: width $duration $easing, border-color $duration $easing;
}
.se:focus-within {
width: 240px;
border-color: $color;
}
05Placeholder slide
The real placeholder attribute stays a single blank space, while an overlaid span carries the visible hint text and slides sideways as it fades out on focus or once a value exists. It suits an email or address field where the hint sentence runs long enough that a floating label alone would crowd the input.
.ps__ghost {
transform: translate(0, -50%);
transition: transform $duration $easing, opacity $duration $easing;
}
.ps__input:focus ~ .ps__ghost,
.ps__input:not(:placeholder-shown) ~ .ps__ghost {
transform: translate(10px, -50%);
opacity: 0;
}
06Error shake
Once a value fills an email field marked required, the browser's own :invalid verdict — no JavaScript at all — shakes the border and swaps in an X icon. A valid value swaps in a checkmark instead at the same spot. Signup email fields and any input that needs live format feedback both lean on this, since nobody had to write the validation by hand.
.ie__input:invalid:not(:placeholder-shown) {
border-color: $error;
animation: shake $duration $easing;
}
@keyframes shake {
10%, 90% { transform: translateX($amount * -.5); }
20%, 80% { transform: translateX($amount); }
30%, 70% { transform: translateX($amount * -1); }
40%, 60% { transform: translateX($amount * .7); }
50% { transform: translateX($amount * -.7); }
}
07Custom select open
A native select can't have its option list restyled, so this one swaps in a button plus a hand-built ul[role=listbox] instead. A five-line click handler toggles an is-open class that scales the list open on the Y axis while the chevron spins 180 degrees. That's a fit for a theme picker or a sort dropdown that has to match the rest of a custom design.
.cs__chevron {
transform: rotate(0deg);
transition: transform $duration $easing;
}
.cs.is-open .cs__chevron { transform: rotate(180deg); }
.cs__list {
transform: scaleY(0);
transform-origin: top;
opacity: 0;
transition: transform $duration $easing, opacity $duration $easing;
}
.cs.is-open .cs__list {
transform: scaleY(1);
opacity: 1;
}
08Character count
CSS alone can't count characters, so a nine-line script rewrites the remaining-count number on every input event and retriggers a bounce by toggling an is-bump class off and back on. A bio textarea or any tweet-length field benefits most, since the bounce lands right on the number rather than the whole box.
.cc__num {
color: $color;
display: inline-block;
transform: scale(1);
}
.cc__count.is-bump .cc__num {
animation: bump $duration $easing;
}
@keyframes bump {
0% { transform: scale(1); }
50% { transform: scale(1.35); }
100% { transform: scale(1); }
}
09Password eye toggle
Clicking the eye button actually flips the input's type between password and text, not just its appearance, while two stacked eye icons cross-fade between each other with opacity and scale. Login and checkout password fields both use it so a person can confirm what they actually typed before submitting.
.pw__icon {
transform: scale(1);
opacity: 1;
transition: transform $duration $easing, opacity $duration $easing;
}
.pw__icon--off { transform: scale(.5); opacity: 0; }
.pw__eye.is-shown .pw__icon--eye { transform: scale(.5); opacity: 0; }
.pw__eye.is-shown .pw__icon--off { transform: scale(1); opacity: 1; }
Where does it break?
Item 06's shake looks like it needs a JavaScript event listener until the very first test run. It fails in a different way instead: removing required doesn't turn the validity check off, it just leaves an empty field marked permanently invalid, so the border turns red and the X icon sits there from the instant the page loads, before a single character gets typed. :not(:placeholder-shown) is the actual switch holding the shake and both icons back until a value exists — required only decides whether the browser blocks form submission, and says nothing about what :invalid matches. A newer selector, :user-invalid, would skip that whole combination by matching only after a real interaction. But MDN notes its browser support still trails plain :invalid, which is why this set kept the older pairing instead. Every one of the nine already ships with that fix in place inside the archive, unlocked with 4qrx8ve2 typed exactly as it sits on this line, no trailing space added anywhere.
Accessibility
Every visible label pairs for with a matching id rather than floating free. Item 01's label for="fl-nick" next to input id="fl-nick" autocomplete="nickname" is a real line copied from the zip, not a paraphrase. Item 06's role="alert" reads its error message the instant :invalid fires, and item 08's aria-live="polite" follows the shrinking character count. Items 07 and 09 update aria-expanded and aria-pressed on every click rather than leaving them fixed at whatever value they loaded with. None of the nine style :focus-visible on their own — they key off plain :focus. But adding it costs one extra selector and keeps a visible ring for keyboard users once a project restyles or removes the browser's default outline:
.fl__input:focus-visible ~ .fl__label {
outline: 2px solid #4a9eff;
outline-offset: 2px;
}
Under prefers-reduced-motion: reduce, all nine drop their transition or animation to none while keeping the end state a person already reached — a lifted label, a widened search box, an open dropdown. So no information disappears, only the trip getting there. MDN's guide to prefers-reduced-motion explains which operating-system toggle drives that media feature.
The rest of the input-and-focus collection sits in the CSS category hub; what's free to read here versus what's inside the zip is spelled out on the about page.
FAQ
Do all nine need JavaScript, or is most of this plain CSS?
Six of the nine — 01 through 06 — run entirely on state the browser already tracks: :focus, :focus-within, :placeholder-shown, and the built-in :valid/:invalid verdict, with no script written for any of them. Only items 07, 08, and 09 carry JavaScript, and each uses it for exactly the one job no CSS selector can finish alone — opening a list, counting characters, and swapping an input's type.
Does :has() work in every browser these demos might run in?
Not everywhere yet. Item 03's glow and item 06's icon pairing both depend on :has() to let a parent react to a child input's state. caniuse lists that selector as supported from Chrome/Edge 105, Safari 15.4, and Firefox 121 onward. Outside those versions the rule is simply ignored — the input itself keeps taking focus and typing normally, only the glow or the icon swap stops appearing.
Is a hand-built dropdown like item 07 as accessible as a native <select>?
Close, but only once the ARIA gets added by hand, which is why item 07 carries aria-haspopup="listbox", a live aria-expanded, and role="listbox"/role="option" on the list itself. A native select gets keyboard support — arrow keys, type-ahead, Escape to close — for free from the browser, while a rebuilt version like this one only wires up what the demo actually needs. The click handling and the ARIA state are real, but Escape and the arrow keys are left for whoever adapts it into a production form.