GODRICH
#css-animation#button#loader#transition#scroll#dashboard#ui#design-trends

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

All nine below share one thing the grid above can't offer: a real, typable <input>. These css input box design snippets are ordered by how much of the state the browser already tracks on its own, not 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. Motion that runs on its own timer instead of waiting for a pointer is a different set entirely, gathered in 9 CSS keyframe animations. And for the small marks that sit beside a field — the counter on item 08, the eye on item 09 — drawing them as strokes rather than boxes is covered in 9 SVG icon animations, which pairs directly with the fields below.

01Floating label

When the label lifts

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).

The lift now carries a spring overshoot (cubic-bezier(.3, 1.6, .4, 1)) that pushes past the resting spot before settling, and a radial glow bloom washes outward from the border for one pass at the same instant.

Floating label
Open in a new tab

01 · Floating label — Measured area 7.982% · moving frames 8/23Original by GODRICH · SCSS and React ship in the zip · See the password

:placeholder-showntransform scale220ms

Where a spare line matters

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

A two-pixel span below the field

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.

The underline overshoots past scaleX(1) and winds back with a spring curve (cubic-bezier(.2, 1.5, .35, 1)), while an elliptical radial glow spreads once beneath it the moment focus lands.

Underline grow on focus
Open in a new tab

02 · Underline grow on focus — Measured area 1.592% · moving frames 7/23Original by GODRICH · SCSS and React ship in the zip · See the password

:focus-withinscaleX300ms

Why focus-within, not 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 outside the input

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.

On entry the ring first springs in from scale(.8) through 1.08 down to .92 in one glow-in pass, and the breathing loop takes over from that settled state.

Glow via :has(:focus)
Open in a new tab

03 · Glow via :has(:focus) — Measured area 6.35% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

:has()opacity scale2s 루프

What :has() makes possible

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

From 148px to 240px

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.

The width eases on a spring curve (cubic-bezier(.3, 1.4, .4, 1)) that stretches a touch past 240px before settling, and a radial glow blooms once inside the pill as it widens.

Search box expand
Open in a new tab

04 · Search box expand — Measured area 7.744% · moving frames 7/23Original by GODRICH · SCSS and React ship in the zip · See the password

:focus-withinwidth300ms

Why a css search bar animation needs a width cap

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

A blank space as the placeholder

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.

The hint now travels 13px out with scale(.94) on a spring overshoot (cubic-bezier(.3, 1.6, .4, 1)) that overshoots the rest point, and a radial glow washes through the field once as it leaves.

Placeholder slide
Open in a new tab

05 · Placeholder slide — Measured area 7.516% · moving frames 9/23Original by GODRICH · SCSS and React ship in the zip · See the password

:placeholder-showntranslate opacity220ms

Where a long hint fits

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

The browser's own verdict

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, while a valid value swaps in a checkmark instead at the same spot. The shake decays cycle by cycle like a damped oscillator (1 → .75 → .55 → .34 amplitude), and a red radial glow flares once around the frame at the moment the error appears.

Error shake
Open in a new tab

06 · Error shake — Measured area 2.305% · moving frames 8/23Original by GODRICH · SCSS and React ship in the zip · See the password

:invalidtranslateX600ms

Where live format feedback fits

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

Why a native select had to go

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, with a five-line click handler toggling an is-open class that scales the list open on the Y axis while the chevron spins 180 degrees. The list now springs past scaleY(1) up to 1.06 before winding back, and a radial glow spreads downward from the button's bottom edge to confirm the open in one pass.

Custom select open
Open in a new tab

07 · Custom select open — Measured area 18.322% · moving frames 16/23Original by GODRICH · SCSS and React ship in the zip · See the password

aria-expandedscaleY220ms

A theme picker or a sort dropdown

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

Nine lines to count characters

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.

The bounce is now a damped spring rebound (1.35 → .853 → 1.062 → 1, decay factor .42) that crosses the rest size twice in opposite directions before settling.

Character count
Open in a new tab

08 · Character count — Measured area 1.504% · moving frames 10/23Original by GODRICH · SCSS and React ship in the zip · See the password

maxlengthscale bump140ms

Where the bounce lands

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

Flipping type, not appearance

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.

The swap rides a spring overshoot that stretches the icon to 1.12 before returning, and a radial glow blooms once from the button's center to confirm the flip.

Password eye toggle
Open in a new tab

09 · Password eye toggle — Measured area 0.572% · moving frames 6/23Original by GODRICH · SCSS and React ship in the zip · See the password

type toggleopacity scale220ms

Confirming what you typed

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?

The first thing most people change after pasting one of these in is the focus color, and a bright, high-contrast set to pull those values from sits in the nine citrus fruit-color landing sections. What breaks before any color choice does is the selector holding the state.

Why required is not the switch

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

Labels, alerts, and live regions

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;
}

What reduced motion keeps

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.

Where the rest lives

The rest of the input field animation css 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 css input field design demos need JavaScript?

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. The same script-free approach applied outside a form field, on hover and selected states instead, is what the nine CSS pricing table animations are built from.

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.

Enter the archive password

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