16 CSS Floating Label Inputs06 / 16

Pure CSSMIT licensed

CSS Floating Label Validation States (Error + Success)

CSS-only inline validation done politely: fields stay neutral until the user has actually interacted, then :user-invalid turns the border and floated label red with a subtle shake, and :user-valid confirms with green and a checkmark — no JavaScript validators, no premature red walls on page load. Variation: the label performs a full 3D rotateX flip as it floats — a card-flip motion that visually stamps the new state.

Published

Live Demo
Try it

The code

<section class="flb-06" aria-label="Floating label validation demo">
  <form class="flb-06__form">
    <h3 class="flb-06__title">Create account</h3>
    <div class="flb-06__field">
      <input class="flb-06__input" type="email" id="flb-06-email" name="email" placeholder=" " required autocomplete="email" aria-describedby="flb-06-email-msg">
      <label class="flb-06__label" for="flb-06-email">Email address</label>
      <p class="flb-06__msg" id="flb-06-email-msg">Enter a valid email, e.g. you@domain.com</p>
    </div>
    <div class="flb-06__field">
      <input class="flb-06__input" type="text" id="flb-06-user" name="username" placeholder=" " required minlength="4" pattern="[a-zA-Z0-9_]+" autocomplete="username" aria-describedby="flb-06-user-msg">
      <label class="flb-06__label" for="flb-06-user">Username</label>
      <p class="flb-06__msg" id="flb-06-user-msg">4+ characters — letters, numbers, underscores</p>
    </div>
    <button class="flb-06__btn" type="submit">Sign up</button>
  </form>
</section>
.flb-06,
.flb-06 *,
.flb-06 *::before,
.flb-06 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.flb-06 {
  --accent: oklch(0.55 0.18 264);
  --err: oklch(0.55 0.19 25);
  --ok: oklch(0.58 0.15 155);
  font-family: 'Segoe UI',system-ui,sans-serif;
  width: 100%;
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #f4f3f0;
  padding: 40px 20px;
}

.flb-06__form {
  width: min(400px,100%);
  background: #fff;
  border-radius: 20px;
  padding: 32px 30px;
  box-shadow: 0 24px 60px -32px rgba(40,36,20,.35);
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.flb-06__title {
  font-size: 23px;
  font-weight: 800;
  color: #221f18;
  letter-spacing: -.02em;
  margin-bottom: 12px;
}

.flb-06__field {
  position: relative;
  padding-bottom: 26px;
}

.flb-06__input {
  width: 100%;
  height: 56px;
  padding: 22px 44px 6px 16px;
  font-size: 16px;
  color: #221f18;
  background: #fff;
  border: 1.5px solid #d9d6cd;
  border-radius: 12px;
  outline: none;
  transition: border-color .2s,box-shadow .2s;
}

.flb-06__input:focus {
  border-color: var(--accent);
  box-shadow: 0 0 0 4px oklch(0.55 0.18 264 / .13);
}

.flb-06__label {
  position: absolute;
  left: 16px;
  top: 28px;
  translate: 0 -50%;
  font-size: 16px;
  color: #8a8578;
  pointer-events: none;
  transform-origin: left center;
  transition: transform .4s cubic-bezier(.35,.7,.3,1),color .2s;
}

.flb-06__input:focus ~ .flb-06__label,
.flb-06__input:not(:placeholder-shown) ~ .flb-06__label {
  transform: perspective(320px) translateY(-13px) rotateX(360deg) scale(.72);
}

.flb-06__input:focus ~ .flb-06__label {
  color: var(--accent);
}

.flb-06__msg {
  position: absolute;
  left: 2px;
  bottom: 4px;
  font-size: 12px;
  color: #9a9588;
  transition: color .2s;
  line-height: 1.3;
}

.flb-06__field:has(.flb-06__input:user-invalid) {
  animation: flb-06-shake .3s ease;
}

.flb-06__input:user-invalid {
  border-color: var(--err);
  box-shadow: 0 0 0 4px oklch(0.55 0.19 25 / .12);
}

.flb-06__input:user-invalid ~ .flb-06__label {
  color: var(--err);
}

.flb-06__input:user-invalid ~ .flb-06__msg {
  color: var(--err);
  font-weight: 600;
}

.flb-06__input:user-valid {
  border-color: var(--ok);
}

.flb-06__input:user-valid ~ .flb-06__label {
  color: var(--ok);
}

.flb-06__field::after {
  content: "✓";
  position: absolute;
  right: 16px;
  top: 28px;
  translate: 0 -50%;
  font-size: 17px;
  font-weight: 800;
  color: var(--ok);
  opacity: 0;
  transition: opacity .2s;
}

.flb-06__field:has(.flb-06__input:user-valid)::after {
  opacity: 1;
}

@keyframes flb-06-shake {
  20% {
    transform: translateX(-5px);
  }

  40% {
    transform: translateX(5px);
  }

  60% {
    transform: translateX(-3px);
  }

  80% {
    transform: translateX(3px);
  }
}

.flb-06__btn {
  height: 50px;
  border: none;
  border-radius: 12px;
  background: #221f18;
  color: #fff;
  font-size: 15px;
  font-weight: 700;
  cursor: pointer;
  transition: filter .15s;
}

.flb-06__btn:hover {
  filter: brightness(1.25);
}

.flb-06__btn:focus-visible,
.flb-06__input:focus-visible {
  outline: 3px solid oklch(0.55 0.18 264 / .5);
  outline-offset: 2px;
}

@media (prefers-reduced-motion: reduce) {
  .flb-06__field:has(.flb-06__input:user-invalid) {
    animation: none;
  }

  .flb-06__label,
    .flb-06__input {
    transition: none;
  }
}
/* No JavaScript — :user-invalid / :user-valid apply error and success palettes only AFTER the user interacts; native constraint validation (required, type=email, minlength, pattern) does the checking. */
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Floating Label Input from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: CSS Floating Label Validation States (Error + Success)
Source: https://codefronts.com/components/css-floating-label-inputs/css-floating-label-validation-states-error-success/

CSS-only inline validation done politely: fields stay neutral until the user has actually interacted, then :user-invalid turns the border and floated label red with a subtle shake, and :user-valid confirms with green and a checkmark — no JavaScript validators, no premature red walls on page load. Variation: the label performs a full 3D rotateX flip as it floats — a card-flip motion that visually stamps the new state.
## HTML
```html
<section class="flb-06" aria-label="Floating label validation demo">
  <form class="flb-06__form">
    <h3 class="flb-06__title">Create account</h3>
    <div class="flb-06__field">
      <input class="flb-06__input" type="email" id="flb-06-email" name="email" placeholder=" " required autocomplete="email" aria-describedby="flb-06-email-msg">
      <label class="flb-06__label" for="flb-06-email">Email address</label>
      <p class="flb-06__msg" id="flb-06-email-msg">Enter a valid email, e.g. you@domain.com</p>
    </div>
    <div class="flb-06__field">
      <input class="flb-06__input" type="text" id="flb-06-user" name="username" placeholder=" " required minlength="4" pattern="[a-zA-Z0-9_]+" autocomplete="username" aria-describedby="flb-06-user-msg">
      <label class="flb-06__label" for="flb-06-user">Username</label>
      <p class="flb-06__msg" id="flb-06-user-msg">4+ characters — letters, numbers, underscores</p>
    </div>
    <button class="flb-06__btn" type="submit">Sign up</button>
  </form>
</section>
```
## CSS
```css
.flb-06,
.flb-06 *,
.flb-06 *::before,
.flb-06 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.flb-06 {
  --accent: oklch(0.55 0.18 264);
  --err: oklch(0.55 0.19 25);
  --ok: oklch(0.58 0.15 155);
  font-family: 'Segoe UI',system-ui,sans-serif;
  width: 100%;
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #f4f3f0;
  padding: 40px 20px;
}

.flb-06__form {
  width: min(400px,100%);
  background: #fff;
  border-radius: 20px;
  padding: 32px 30px;
  box-shadow: 0 24px 60px -32px rgba(40,36,20,.35);
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.flb-06__title {
  font-size: 23px;
  font-weight: 800;
  color: #221f18;
  letter-spacing: -.02em;
  margin-bottom: 12px;
}

.flb-06__field {
  position: relative;
  padding-bottom: 26px;
}

.flb-06__input {
  width: 100%;
  height: 56px;
  padding: 22px 44px 6px 16px;
  font-size: 16px;
  color: #221f18;
  background: #fff;
  border: 1.5px solid #d9d6cd;
  border-radius: 12px;
  outline: none;
  transition: border-color .2s,box-shadow .2s;
}

.flb-06__input:focus {
  border-color: var(--accent);
  box-shadow: 0 0 0 4px oklch(0.55 0.18 264 / .13);
}

.flb-06__label {
  position: absolute;
  left: 16px;
  top: 28px;
  translate: 0 -50%;
  font-size: 16px;
  color: #8a8578;
  pointer-events: none;
  transform-origin: left center;
  transition: transform .4s cubic-bezier(.35,.7,.3,1),color .2s;
}

.flb-06__input:focus ~ .flb-06__label,
.flb-06__input:not(:placeholder-shown) ~ .flb-06__label {
  transform: perspective(320px) translateY(-13px) rotateX(360deg) scale(.72);
}

.flb-06__input:focus ~ .flb-06__label {
  color: var(--accent);
}

.flb-06__msg {
  position: absolute;
  left: 2px;
  bottom: 4px;
  font-size: 12px;
  color: #9a9588;
  transition: color .2s;
  line-height: 1.3;
}

.flb-06__field:has(.flb-06__input:user-invalid) {
  animation: flb-06-shake .3s ease;
}

.flb-06__input:user-invalid {
  border-color: var(--err);
  box-shadow: 0 0 0 4px oklch(0.55 0.19 25 / .12);
}

.flb-06__input:user-invalid ~ .flb-06__label {
  color: var(--err);
}

.flb-06__input:user-invalid ~ .flb-06__msg {
  color: var(--err);
  font-weight: 600;
}

.flb-06__input:user-valid {
  border-color: var(--ok);
}

.flb-06__input:user-valid ~ .flb-06__label {
  color: var(--ok);
}

.flb-06__field::after {
  content: "✓";
  position: absolute;
  right: 16px;
  top: 28px;
  translate: 0 -50%;
  font-size: 17px;
  font-weight: 800;
  color: var(--ok);
  opacity: 0;
  transition: opacity .2s;
}

.flb-06__field:has(.flb-06__input:user-valid)::after {
  opacity: 1;
}

@keyframes flb-06-shake {
  20% {
    transform: translateX(-5px);
  }

  40% {
    transform: translateX(5px);
  }

  60% {
    transform: translateX(-3px);
  }

  80% {
    transform: translateX(3px);
  }
}

.flb-06__btn {
  height: 50px;
  border: none;
  border-radius: 12px;
  background: #221f18;
  color: #fff;
  font-size: 15px;
  font-weight: 700;
  cursor: pointer;
  transition: filter .15s;
}

.flb-06__btn:hover {
  filter: brightness(1.25);
}

.flb-06__btn:focus-visible,
.flb-06__input:focus-visible {
  outline: 3px solid oklch(0.55 0.18 264 / .5);
  outline-offset: 2px;
}

@media (prefers-reduced-motion: reduce) {
  .flb-06__field:has(.flb-06__input:user-invalid) {
    animation: none;
  }

  .flb-06__label,
    .flb-06__input {
    transition: none;
  }
}
```

## JavaScript
```js
/* No JavaScript — :user-invalid / :user-valid apply error and success palettes only AFTER the user interacts; native constraint validation (required, type=email, minlength, pattern) does the checking. */
```

How this works

The key is :user-invalid / :user-valid instead of raw :invalid / :valid: they only match after the user has interacted with the field (typed and left it), so a required empty form doesn't scream red the moment it renders — the #1 UX complaint about CSS-only validation.

When :user-invalid matches, the field border, floated label and helper message flip to the error palette, an @keyframes shake nudges the field via translateX, and the error text (pre-wired with aria-describedby) becomes visible. :user-valid shows a green state with a ✓ drawn by a pseudo-element. Constraint attributes (required, type="email", minlength, pattern) do all the validating natively.

Techniques used: :user-invalid / :user-valid (interaction-gated validation) · native constraint attributes (required, type=email, minlength, pattern) · :has() on the wrapper for the shake · @keyframes translateX shake · perspective() + rotateX() 3D flip · ::after checkmark reveal · aria-describedby error wiring · prefers-reduced-motion kill-switch

Make it yours

  • Tune error/success palettes via --err and --ok.
  • Edit the shake distance and repeat count in @keyframes flb-06-shake.
  • Swap pattern/minlength attributes to change the rules — no code changes needed.
  • Remove the shake entirely for enterprise-calm forms (keep the colour change).

Gotchas — read before shipping

  • Prefer :user-invalid over :invalid — raw :invalid marks required fields red on page load, before the user has done anything.
  • Keep error text in the DOM (visibility toggled) and referenced by aria-describedby so screen readers can find it.
  • The shake must animate transform only, and be disabled under prefers-reduced-motion (done here).

Browser support

ChromeSafariFirefoxEdge
119+ (:user-invalid)16.5+88+119+ (:user-invalid)

Where :user-invalid is unsupported the fields simply stay neutral — validation still blocks submit natively. No JS fallback needed.

Techniques used in this demo

Search CodeFronts

Loading…