15 / 16
All 16 in 16 Tailwind CSS Floating Label Inputs
- 01Tailwind Staggered Letter-Cascade Floating Label
- 02Tailwind Gradient-Ink Floating Label with Animated Underline Draw
- 03Tailwind Conic Border-Beam Floating Label (@property Animation)
- 04Pure Tailwind Floating Label Input (No JavaScript)
- 05Tailwind Floating Label with Chrome Autofill Fix
- 06Tailwind Floating Label Validation States (Error + Success)
- 07Accessible Tailwind Floating Label Input (ARIA + WCAG)
- 08Tailwind Floating Label for Select Dropdowns
- 09Tailwind Material Design Outlined Text Field
- 10Tailwind Filled & Underline Floating Label Input
- 11Tailwind Dark Mode Floating Label Input with light-dark()
- 12Tailwind Input with Icon Inside and Floating Label
- 13Responsive Tailwind Floating Label for Mobile (No iOS Zoom)
- 14Tailwind Auto-Growing Textarea with Floating Label
- 15Tailwind Password Input with Show/Hide Toggle and Floating Label
- 16Tailwind Multi-Column Form Grid with Floating Labels
Tailwind Password Input with Show/Hide Toggle and Floating Label
The login-form workhorse, finished: a floating-label password field with a 44px eye <button> whose icons swap via group-aria-pressed/eye:, plus the strength meter the CSS original only described — four segments lit by group-data-[strength=N]/pw: variants. The script flips attributes; every visual lives in utilities. The label springs with an overshoot bezier.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="tfl-15 w-full min-h-svh grid place-items-center bg-tfl15-bg px-5 py-12 font-tfl15-sans" aria-label="Password floating label with toggle demo">
<form class="flex w-full max-w-[380px] flex-col gap-6 rounded-3xl bg-white p-8 shadow-[0_24px_60px_-32px_oklch(0.3_0.06_265/0.4)] ring-1 ring-tfl15-ink/5" novalidate>
<div class="grid gap-1">
<h3 class="text-2xl font-extrabold tracking-[-0.02em] text-tfl15-ink">Choose a password</h3>
<p class="text-sm text-tfl15-mut">Use a phrase you'll remember.</p>
</div>
<div class="group/pw relative" data-tfl15-field data-strength="0">
<input class="peer h-14 w-full rounded-xl border-[1.5px] border-tfl15-line bg-white pt-[22px] pr-[58px] pb-1.5 pl-4 text-base text-tfl15-ink outline-hidden transition-[border-color,box-shadow] duration-200 hover:border-tfl15-line-hover focus:border-tfl15-acc focus:ring-4 focus:ring-tfl15-acc/15" type="password" id="tfl-15-pass" name="new-password" placeholder=" " autocomplete="new-password" minlength="8" aria-describedby="tfl-15-hint">
<label class="pointer-events-none absolute left-4 top-7 origin-left -translate-y-1/2 text-base text-tfl15-mut transition-[translate,scale,color] duration-500 ease-[cubic-bezier(.3,1.8,.4,.9)] peer-focus:translate-y-[calc(-50%_-_13px)] peer-focus:scale-72 peer-focus:text-tfl15-acc peer-[:not(:placeholder-shown)]:translate-y-[calc(-50%_-_13px)] peer-[:not(:placeholder-shown)]:scale-72 motion-reduce:transition-none" for="tfl-15-pass">Password</label>
<button class="group/eye absolute top-1.5 right-1.5 grid size-11 cursor-pointer place-items-center rounded-[10px] text-tfl15-mut transition-colors duration-150 hover:bg-tfl15-bg hover:text-tfl15-ink focus-visible:outline-3 focus-visible:outline-offset-1 focus-visible:outline-tfl15-acc/50" type="button" data-toggle aria-pressed="false" aria-label="Show password" aria-controls="tfl-15-pass">
<svg class="col-start-1 row-start-1 transition-opacity duration-150 group-aria-pressed/eye:opacity-0" aria-hidden="true" viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3.5-6.5 10-6.5S22 12 22 12s-3.5 6.5-10 6.5S2 12 2 12Z"></path><circle cx="12" cy="12" r="3"></circle></svg>
<svg class="col-start-1 row-start-1 opacity-0 transition-opacity duration-150 group-aria-pressed/eye:opacity-100" aria-hidden="true" viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3.5-6.5 10-6.5S22 12 22 12s-3.5 6.5-10 6.5S2 12 2 12Z"></path><circle cx="12" cy="12" r="3"></circle><path d="m4 4 16 16"></path></svg>
</button>
<div class="mt-3 grid grid-cols-4 gap-1.5" aria-hidden="true">
<span class="h-1 rounded-full bg-tfl15-line transition-colors duration-300 group-data-[strength=1]/pw:bg-tfl15-err group-data-[strength=2]/pw:bg-tfl15-warn group-data-[strength=3]/pw:bg-tfl15-ok group-data-[strength=4]/pw:bg-tfl15-ok"></span>
<span class="h-1 rounded-full bg-tfl15-line transition-colors duration-300 group-data-[strength=2]/pw:bg-tfl15-warn group-data-[strength=3]/pw:bg-tfl15-ok group-data-[strength=4]/pw:bg-tfl15-ok"></span>
<span class="h-1 rounded-full bg-tfl15-line transition-colors duration-300 group-data-[strength=3]/pw:bg-tfl15-ok group-data-[strength=4]/pw:bg-tfl15-ok"></span>
<span class="h-1 rounded-full bg-tfl15-line transition-colors duration-300 group-data-[strength=4]/pw:bg-tfl15-ok"></span>
</div>
<p class="mt-2 flex justify-between gap-3 text-[13px] text-tfl15-mut"><span id="tfl-15-hint">At least 8 characters</span><span class="font-semibold group-data-[strength=1]/pw:text-tfl15-err group-data-[strength=2]/pw:text-tfl15-warn-ink group-data-[strength=3]/pw:text-tfl15-ok group-data-[strength=4]/pw:text-tfl15-ok" role="status" aria-live="polite" data-status></span></p>
</div>
<button class="min-h-12 cursor-pointer rounded-xl bg-tfl15-acc text-[15px] font-bold text-white shadow-[0_10px_24px_-12px_var(--color-tfl15-acc)] transition-[filter,scale] duration-150 hover:brightness-110 active:scale-98 focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tfl15-acc/50" type="button">Create account</button>
</form>
</section><section class="tfl-15 w-full min-h-svh grid place-items-center bg-tfl15-bg px-5 py-12 font-tfl15-sans" aria-label="Password floating label with toggle demo">
<form class="flex w-full max-w-[380px] flex-col gap-6 rounded-3xl bg-white p-8 shadow-[0_24px_60px_-32px_oklch(0.3_0.06_265/0.4)] ring-1 ring-tfl15-ink/5" novalidate>
<div class="grid gap-1">
<h3 class="text-2xl font-extrabold tracking-[-0.02em] text-tfl15-ink">Choose a password</h3>
<p class="text-sm text-tfl15-mut">Use a phrase you'll remember.</p>
</div>
<div class="group/pw relative" data-tfl15-field data-strength="0">
<input class="peer h-14 w-full rounded-xl border-[1.5px] border-tfl15-line bg-white pt-[22px] pr-[58px] pb-1.5 pl-4 text-base text-tfl15-ink outline-hidden transition-[border-color,box-shadow] duration-200 hover:border-tfl15-line-hover focus:border-tfl15-acc focus:ring-4 focus:ring-tfl15-acc/15" type="password" id="tfl-15-pass" name="new-password" placeholder=" " autocomplete="new-password" minlength="8" aria-describedby="tfl-15-hint">
<label class="pointer-events-none absolute left-4 top-7 origin-left -translate-y-1/2 text-base text-tfl15-mut transition-[translate,scale,color] duration-500 ease-[cubic-bezier(.3,1.8,.4,.9)] peer-focus:translate-y-[calc(-50%_-_13px)] peer-focus:scale-72 peer-focus:text-tfl15-acc peer-[:not(:placeholder-shown)]:translate-y-[calc(-50%_-_13px)] peer-[:not(:placeholder-shown)]:scale-72 motion-reduce:transition-none" for="tfl-15-pass">Password</label>
<button class="group/eye absolute top-1.5 right-1.5 grid size-11 cursor-pointer place-items-center rounded-[10px] text-tfl15-mut transition-colors duration-150 hover:bg-tfl15-bg hover:text-tfl15-ink focus-visible:outline-3 focus-visible:outline-offset-1 focus-visible:outline-tfl15-acc/50" type="button" data-toggle aria-pressed="false" aria-label="Show password" aria-controls="tfl-15-pass">
<svg class="col-start-1 row-start-1 transition-opacity duration-150 group-aria-pressed/eye:opacity-0" aria-hidden="true" viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3.5-6.5 10-6.5S22 12 22 12s-3.5 6.5-10 6.5S2 12 2 12Z"></path><circle cx="12" cy="12" r="3"></circle></svg>
<svg class="col-start-1 row-start-1 opacity-0 transition-opacity duration-150 group-aria-pressed/eye:opacity-100" aria-hidden="true" viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3.5-6.5 10-6.5S22 12 22 12s-3.5 6.5-10 6.5S2 12 2 12Z"></path><circle cx="12" cy="12" r="3"></circle><path d="m4 4 16 16"></path></svg>
</button>
<div class="mt-3 grid grid-cols-4 gap-1.5" aria-hidden="true">
<span class="h-1 rounded-full bg-tfl15-line transition-colors duration-300 group-data-[strength=1]/pw:bg-tfl15-err group-data-[strength=2]/pw:bg-tfl15-warn group-data-[strength=3]/pw:bg-tfl15-ok group-data-[strength=4]/pw:bg-tfl15-ok"></span>
<span class="h-1 rounded-full bg-tfl15-line transition-colors duration-300 group-data-[strength=2]/pw:bg-tfl15-warn group-data-[strength=3]/pw:bg-tfl15-ok group-data-[strength=4]/pw:bg-tfl15-ok"></span>
<span class="h-1 rounded-full bg-tfl15-line transition-colors duration-300 group-data-[strength=3]/pw:bg-tfl15-ok group-data-[strength=4]/pw:bg-tfl15-ok"></span>
<span class="h-1 rounded-full bg-tfl15-line transition-colors duration-300 group-data-[strength=4]/pw:bg-tfl15-ok"></span>
</div>
<p class="mt-2 flex justify-between gap-3 text-[13px] text-tfl15-mut"><span id="tfl-15-hint">At least 8 characters</span><span class="font-semibold group-data-[strength=1]/pw:text-tfl15-err group-data-[strength=2]/pw:text-tfl15-warn-ink group-data-[strength=3]/pw:text-tfl15-ok group-data-[strength=4]/pw:text-tfl15-ok" role="status" aria-live="polite" data-status></span></p>
</div>
<button class="min-h-12 cursor-pointer rounded-xl bg-tfl15-acc text-[15px] font-bold text-white shadow-[0_10px_24px_-12px_var(--color-tfl15-acc)] transition-[filter,scale] duration-150 hover:brightness-110 active:scale-98 focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tfl15-acc/50" type="button">Create account</button>
</form>
</section>@import "tailwindcss";
@theme {
--color-tfl15-bg: #f1f2f7;
--color-tfl15-bg: oklch(0.96 0.007 270);
--color-tfl15-ink: #191c2b;
--color-tfl15-ink: oklch(0.22 0.03 270);
--color-tfl15-mut: #62677c;
--color-tfl15-mut: oklch(0.5 0.03 270);
--color-tfl15-line: #d6d9e4;
--color-tfl15-line: oklch(0.88 0.015 270);
--color-tfl15-line-hover: #b4b9cc;
--color-tfl15-line-hover: oklch(0.78 0.025 270);
--color-tfl15-acc: #4a50d4;
--color-tfl15-acc: oklch(0.5 0.2 272);
--color-tfl15-err: #c8372e;
--color-tfl15-err: oklch(0.55 0.19 27);
--color-tfl15-warn: #e0a21a;
--color-tfl15-warn: oklch(0.76 0.15 78);
--color-tfl15-warn-ink: #8a5a00;
--color-tfl15-warn-ink: oklch(0.5 0.11 70);
--color-tfl15-ok: #1f8a4c;
--color-tfl15-ok: oklch(0.55 0.14 152);
--font-tfl15-sans: 'Geist', system-ui, sans-serif;
}
.tfl-15 {
width: 100%;
min-height: 100svh;
}@import "tailwindcss";
@theme {
--color-tfl15-bg: #f1f2f7;
--color-tfl15-bg: oklch(0.96 0.007 270);
--color-tfl15-ink: #191c2b;
--color-tfl15-ink: oklch(0.22 0.03 270);
--color-tfl15-mut: #62677c;
--color-tfl15-mut: oklch(0.5 0.03 270);
--color-tfl15-line: #d6d9e4;
--color-tfl15-line: oklch(0.88 0.015 270);
--color-tfl15-line-hover: #b4b9cc;
--color-tfl15-line-hover: oklch(0.78 0.025 270);
--color-tfl15-acc: #4a50d4;
--color-tfl15-acc: oklch(0.5 0.2 272);
--color-tfl15-err: #c8372e;
--color-tfl15-err: oklch(0.55 0.19 27);
--color-tfl15-warn: #e0a21a;
--color-tfl15-warn: oklch(0.76 0.15 78);
--color-tfl15-warn-ink: #8a5a00;
--color-tfl15-warn-ink: oklch(0.5 0.11 70);
--color-tfl15-ok: #1f8a4c;
--color-tfl15-ok: oklch(0.55 0.14 152);
--font-tfl15-sans: 'Geist', system-ui, sans-serif;
}
.tfl-15 {
width: 100%;
min-height: 100svh;
}(() => {
const LABELS = ['', 'Weak', 'Fair', 'Good', 'Strong'];
const score = (v) => !v ? 0 : Math.max(1, [v.length >= 8, /[a-z]/.test(v) && /[A-Z]/.test(v), /\d/.test(v), /[^A-Za-z0-9]/.test(v)].filter(Boolean).length);
document.querySelectorAll('[data-tfl15-field]').forEach((field) => {
if (field.dataset.wired) return;
field.dataset.wired = '1';
const input = field.querySelector('input');
const btn = field.querySelector('[data-toggle]');
const status = field.querySelector('[data-status]');
btn.addEventListener('click', () => {
const show = input.type === 'password';
input.type = show ? 'text' : 'password';
btn.setAttribute('aria-pressed', String(show));
btn.setAttribute('aria-label', show ? 'Hide password' : 'Show password');
});
input.addEventListener('input', () => {
const s = score(input.value);
field.dataset.strength = String(s);
status.textContent = LABELS[s];
});
});
})();(() => {
const LABELS = ['', 'Weak', 'Fair', 'Good', 'Strong'];
const score = (v) => !v ? 0 : Math.max(1, [v.length >= 8, /[a-z]/.test(v) && /[A-Z]/.test(v), /\d/.test(v), /[^A-Za-z0-9]/.test(v)].filter(Boolean).length);
document.querySelectorAll('[data-tfl15-field]').forEach((field) => {
if (field.dataset.wired) return;
field.dataset.wired = '1';
const input = field.querySelector('input');
const btn = field.querySelector('[data-toggle]');
const status = field.querySelector('[data-status]');
btn.addEventListener('click', () => {
const show = input.type === 'password';
input.type = show ? 'text' : 'password';
btn.setAttribute('aria-pressed', String(show));
btn.setAttribute('aria-label', show ? 'Hide password' : 'Show password');
});
input.addEventListener('input', () => {
const s = score(input.value);
field.dataset.strength = String(s);
status.textContent = LABELS[s];
});
});
})();Setup — do this once per project
Skip this if your project already runs Tailwind v4.
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>Compiles in the browser — perfect for a CodePen or a quick test, not for production.
npm install tailwindcss @tailwindcss/vite
npm install tailwindcss @tailwindcss/vite// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})Any v4 setup works — Vite, PostCSS or the CLI. Nothing in this demo is bundler-specific.
@import "tailwindcss";
@theme {
--color-tfl15-bg: #f1f2f7;
--color-tfl15-bg: oklch(0.96 0.007 270);
--color-tfl15-ink: #191c2b;
--color-tfl15-ink: oklch(0.22 0.03 270);
--color-tfl15-mut: #62677c;
--color-tfl15-mut: oklch(0.5 0.03 270);
--color-tfl15-line: #d6d9e4;
--color-tfl15-line: oklch(0.88 0.015 270);
--color-tfl15-line-hover: #b4b9cc;
--color-tfl15-line-hover: oklch(0.78 0.025 270);
--color-tfl15-acc: #4a50d4;
--color-tfl15-acc: oklch(0.5 0.2 272);
--color-tfl15-err: #c8372e;
--color-tfl15-err: oklch(0.55 0.19 27);
--color-tfl15-warn: #e0a21a;
--color-tfl15-warn: oklch(0.76 0.15 78);
--color-tfl15-warn-ink: #8a5a00;
--color-tfl15-warn-ink: oklch(0.5 0.11 70);
--color-tfl15-ok: #1f8a4c;
--color-tfl15-ok: oklch(0.55 0.14 152);
--font-tfl15-sans: 'Geist', system-ui, sans-serif;
}
.tfl-15 {
width: 100%;
min-height: 100svh;
}@import "tailwindcss";
@theme {
--color-tfl15-bg: #f1f2f7;
--color-tfl15-bg: oklch(0.96 0.007 270);
--color-tfl15-ink: #191c2b;
--color-tfl15-ink: oklch(0.22 0.03 270);
--color-tfl15-mut: #62677c;
--color-tfl15-mut: oklch(0.5 0.03 270);
--color-tfl15-line: #d6d9e4;
--color-tfl15-line: oklch(0.88 0.015 270);
--color-tfl15-line-hover: #b4b9cc;
--color-tfl15-line-hover: oklch(0.78 0.025 270);
--color-tfl15-acc: #4a50d4;
--color-tfl15-acc: oklch(0.5 0.2 272);
--color-tfl15-err: #c8372e;
--color-tfl15-err: oklch(0.55 0.19 27);
--color-tfl15-warn: #e0a21a;
--color-tfl15-warn: oklch(0.76 0.15 78);
--color-tfl15-warn-ink: #8a5a00;
--color-tfl15-warn-ink: oklch(0.5 0.11 70);
--color-tfl15-ok: #1f8a4c;
--color-tfl15-ok: oklch(0.55 0.14 152);
--font-tfl15-sans: 'Geist', system-ui, sans-serif;
}
.tfl-15 {
width: 100%;
min-height: 100svh;
}<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Geist:wght@400..800&display=swap">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Geist:wght@400..800&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
.eye[aria-pressed="true"] .ic--showbecamegroup-aria-pressed/eye:opacity-0— the named group scopes the ARIA read to this button only.- The two overlaid icons share a grid cell with
col-start-1 row-start-1, replacinggrid-area:1/1. - UI upgrade from the CSS original: the strength meter the original documented (
data-strength) but never shipped now exists — four segments, a coloured Weak/Fair/Good/Strong status, and a polite live region so the rating is announced. - UI upgrade: the controller is scoped per field (
[data-tfl15-field]) instead of hard-coded ids, so any number of password fields work on one page; hint text rose from#7a7f93to 5.9:1.
Here's a working CSS demo 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: Tailwind Password Input with Show/Hide Toggle and Floating Label
Source: https://codefronts.com/components/tailwind-css-floating-label-inputs/tailwind-password-show-hide-toggle-floating-label/
The login-form workhorse, finished: a floating-label password field with a 44px eye <button> whose icons swap via group-aria-pressed/eye:, plus the strength meter the CSS original only described — four segments lit by group-data-[strength=N]/pw: variants. The script flips attributes; every visual lives in utilities. The label springs with an overshoot bezier.
## HTML
```html
<section class="tfl-15 w-full min-h-svh grid place-items-center bg-tfl15-bg px-5 py-12 font-tfl15-sans" aria-label="Password floating label with toggle demo">
<form class="flex w-full max-w-[380px] flex-col gap-6 rounded-3xl bg-white p-8 shadow-[0_24px_60px_-32px_oklch(0.3_0.06_265/0.4)] ring-1 ring-tfl15-ink/5" novalidate>
<div class="grid gap-1">
<h3 class="text-2xl font-extrabold tracking-[-0.02em] text-tfl15-ink">Choose a password</h3>
<p class="text-sm text-tfl15-mut">Use a phrase you'll remember.</p>
</div>
<div class="group/pw relative" data-tfl15-field data-strength="0">
<input class="peer h-14 w-full rounded-xl border-[1.5px] border-tfl15-line bg-white pt-[22px] pr-[58px] pb-1.5 pl-4 text-base text-tfl15-ink outline-hidden transition-[border-color,box-shadow] duration-200 hover:border-tfl15-line-hover focus:border-tfl15-acc focus:ring-4 focus:ring-tfl15-acc/15" type="password" id="tfl-15-pass" name="new-password" placeholder=" " autocomplete="new-password" minlength="8" aria-describedby="tfl-15-hint">
<label class="pointer-events-none absolute left-4 top-7 origin-left -translate-y-1/2 text-base text-tfl15-mut transition-[translate,scale,color] duration-500 ease-[cubic-bezier(.3,1.8,.4,.9)] peer-focus:translate-y-[calc(-50%_-_13px)] peer-focus:scale-72 peer-focus:text-tfl15-acc peer-[:not(:placeholder-shown)]:translate-y-[calc(-50%_-_13px)] peer-[:not(:placeholder-shown)]:scale-72 motion-reduce:transition-none" for="tfl-15-pass">Password</label>
<button class="group/eye absolute top-1.5 right-1.5 grid size-11 cursor-pointer place-items-center rounded-[10px] text-tfl15-mut transition-colors duration-150 hover:bg-tfl15-bg hover:text-tfl15-ink focus-visible:outline-3 focus-visible:outline-offset-1 focus-visible:outline-tfl15-acc/50" type="button" data-toggle aria-pressed="false" aria-label="Show password" aria-controls="tfl-15-pass">
<svg class="col-start-1 row-start-1 transition-opacity duration-150 group-aria-pressed/eye:opacity-0" aria-hidden="true" viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3.5-6.5 10-6.5S22 12 22 12s-3.5 6.5-10 6.5S2 12 2 12Z"></path><circle cx="12" cy="12" r="3"></circle></svg>
<svg class="col-start-1 row-start-1 opacity-0 transition-opacity duration-150 group-aria-pressed/eye:opacity-100" aria-hidden="true" viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3.5-6.5 10-6.5S22 12 22 12s-3.5 6.5-10 6.5S2 12 2 12Z"></path><circle cx="12" cy="12" r="3"></circle><path d="m4 4 16 16"></path></svg>
</button>
<div class="mt-3 grid grid-cols-4 gap-1.5" aria-hidden="true">
<span class="h-1 rounded-full bg-tfl15-line transition-colors duration-300 group-data-[strength=1]/pw:bg-tfl15-err group-data-[strength=2]/pw:bg-tfl15-warn group-data-[strength=3]/pw:bg-tfl15-ok group-data-[strength=4]/pw:bg-tfl15-ok"></span>
<span class="h-1 rounded-full bg-tfl15-line transition-colors duration-300 group-data-[strength=2]/pw:bg-tfl15-warn group-data-[strength=3]/pw:bg-tfl15-ok group-data-[strength=4]/pw:bg-tfl15-ok"></span>
<span class="h-1 rounded-full bg-tfl15-line transition-colors duration-300 group-data-[strength=3]/pw:bg-tfl15-ok group-data-[strength=4]/pw:bg-tfl15-ok"></span>
<span class="h-1 rounded-full bg-tfl15-line transition-colors duration-300 group-data-[strength=4]/pw:bg-tfl15-ok"></span>
</div>
<p class="mt-2 flex justify-between gap-3 text-[13px] text-tfl15-mut"><span id="tfl-15-hint">At least 8 characters</span><span class="font-semibold group-data-[strength=1]/pw:text-tfl15-err group-data-[strength=2]/pw:text-tfl15-warn-ink group-data-[strength=3]/pw:text-tfl15-ok group-data-[strength=4]/pw:text-tfl15-ok" role="status" aria-live="polite" data-status></span></p>
</div>
<button class="min-h-12 cursor-pointer rounded-xl bg-tfl15-acc text-[15px] font-bold text-white shadow-[0_10px_24px_-12px_var(--color-tfl15-acc)] transition-[filter,scale] duration-150 hover:brightness-110 active:scale-98 focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tfl15-acc/50" type="button">Create account</button>
</form>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tfl15-bg: #f1f2f7;
--color-tfl15-bg: oklch(0.96 0.007 270);
--color-tfl15-ink: #191c2b;
--color-tfl15-ink: oklch(0.22 0.03 270);
--color-tfl15-mut: #62677c;
--color-tfl15-mut: oklch(0.5 0.03 270);
--color-tfl15-line: #d6d9e4;
--color-tfl15-line: oklch(0.88 0.015 270);
--color-tfl15-line-hover: #b4b9cc;
--color-tfl15-line-hover: oklch(0.78 0.025 270);
--color-tfl15-acc: #4a50d4;
--color-tfl15-acc: oklch(0.5 0.2 272);
--color-tfl15-err: #c8372e;
--color-tfl15-err: oklch(0.55 0.19 27);
--color-tfl15-warn: #e0a21a;
--color-tfl15-warn: oklch(0.76 0.15 78);
--color-tfl15-warn-ink: #8a5a00;
--color-tfl15-warn-ink: oklch(0.5 0.11 70);
--color-tfl15-ok: #1f8a4c;
--color-tfl15-ok: oklch(0.55 0.14 152);
--font-tfl15-sans: 'Geist', system-ui, sans-serif;
}
.tfl-15 {
width: 100%;
min-height: 100svh;
}
```
## JavaScript
```js
(() => {
const LABELS = ['', 'Weak', 'Fair', 'Good', 'Strong'];
const score = (v) => !v ? 0 : Math.max(1, [v.length >= 8, /[a-z]/.test(v) && /[A-Z]/.test(v), /\d/.test(v), /[^A-Za-z0-9]/.test(v)].filter(Boolean).length);
document.querySelectorAll('[data-tfl15-field]').forEach((field) => {
if (field.dataset.wired) return;
field.dataset.wired = '1';
const input = field.querySelector('input');
const btn = field.querySelector('[data-toggle]');
const status = field.querySelector('[data-status]');
btn.addEventListener('click', () => {
const show = input.type === 'password';
input.type = show ? 'text' : 'password';
btn.setAttribute('aria-pressed', String(show));
btn.setAttribute('aria-label', show ? 'Hide password' : 'Show password');
});
input.addEventListener('input', () => {
const s = score(input.value);
field.dataset.strength = String(s);
status.textContent = LABELS[s];
});
});
})();
```Here's a working CSS demo 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: Tailwind Password Input with Show/Hide Toggle and Floating Label
Source: https://codefronts.com/components/tailwind-css-floating-label-inputs/tailwind-password-show-hide-toggle-floating-label/
The login-form workhorse, finished: a floating-label password field with a 44px eye <button> whose icons swap via group-aria-pressed/eye:, plus the strength meter the CSS original only described — four segments lit by group-data-[strength=N]/pw: variants. The script flips attributes; every visual lives in utilities. The label springs with an overshoot bezier.
## HTML
```html
<section class="tfl-15 w-full min-h-svh grid place-items-center bg-tfl15-bg px-5 py-12 font-tfl15-sans" aria-label="Password floating label with toggle demo">
<form class="flex w-full max-w-[380px] flex-col gap-6 rounded-3xl bg-white p-8 shadow-[0_24px_60px_-32px_oklch(0.3_0.06_265/0.4)] ring-1 ring-tfl15-ink/5" novalidate>
<div class="grid gap-1">
<h3 class="text-2xl font-extrabold tracking-[-0.02em] text-tfl15-ink">Choose a password</h3>
<p class="text-sm text-tfl15-mut">Use a phrase you'll remember.</p>
</div>
<div class="group/pw relative" data-tfl15-field data-strength="0">
<input class="peer h-14 w-full rounded-xl border-[1.5px] border-tfl15-line bg-white pt-[22px] pr-[58px] pb-1.5 pl-4 text-base text-tfl15-ink outline-hidden transition-[border-color,box-shadow] duration-200 hover:border-tfl15-line-hover focus:border-tfl15-acc focus:ring-4 focus:ring-tfl15-acc/15" type="password" id="tfl-15-pass" name="new-password" placeholder=" " autocomplete="new-password" minlength="8" aria-describedby="tfl-15-hint">
<label class="pointer-events-none absolute left-4 top-7 origin-left -translate-y-1/2 text-base text-tfl15-mut transition-[translate,scale,color] duration-500 ease-[cubic-bezier(.3,1.8,.4,.9)] peer-focus:translate-y-[calc(-50%_-_13px)] peer-focus:scale-72 peer-focus:text-tfl15-acc peer-[:not(:placeholder-shown)]:translate-y-[calc(-50%_-_13px)] peer-[:not(:placeholder-shown)]:scale-72 motion-reduce:transition-none" for="tfl-15-pass">Password</label>
<button class="group/eye absolute top-1.5 right-1.5 grid size-11 cursor-pointer place-items-center rounded-[10px] text-tfl15-mut transition-colors duration-150 hover:bg-tfl15-bg hover:text-tfl15-ink focus-visible:outline-3 focus-visible:outline-offset-1 focus-visible:outline-tfl15-acc/50" type="button" data-toggle aria-pressed="false" aria-label="Show password" aria-controls="tfl-15-pass">
<svg class="col-start-1 row-start-1 transition-opacity duration-150 group-aria-pressed/eye:opacity-0" aria-hidden="true" viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3.5-6.5 10-6.5S22 12 22 12s-3.5 6.5-10 6.5S2 12 2 12Z"></path><circle cx="12" cy="12" r="3"></circle></svg>
<svg class="col-start-1 row-start-1 opacity-0 transition-opacity duration-150 group-aria-pressed/eye:opacity-100" aria-hidden="true" viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3.5-6.5 10-6.5S22 12 22 12s-3.5 6.5-10 6.5S2 12 2 12Z"></path><circle cx="12" cy="12" r="3"></circle><path d="m4 4 16 16"></path></svg>
</button>
<div class="mt-3 grid grid-cols-4 gap-1.5" aria-hidden="true">
<span class="h-1 rounded-full bg-tfl15-line transition-colors duration-300 group-data-[strength=1]/pw:bg-tfl15-err group-data-[strength=2]/pw:bg-tfl15-warn group-data-[strength=3]/pw:bg-tfl15-ok group-data-[strength=4]/pw:bg-tfl15-ok"></span>
<span class="h-1 rounded-full bg-tfl15-line transition-colors duration-300 group-data-[strength=2]/pw:bg-tfl15-warn group-data-[strength=3]/pw:bg-tfl15-ok group-data-[strength=4]/pw:bg-tfl15-ok"></span>
<span class="h-1 rounded-full bg-tfl15-line transition-colors duration-300 group-data-[strength=3]/pw:bg-tfl15-ok group-data-[strength=4]/pw:bg-tfl15-ok"></span>
<span class="h-1 rounded-full bg-tfl15-line transition-colors duration-300 group-data-[strength=4]/pw:bg-tfl15-ok"></span>
</div>
<p class="mt-2 flex justify-between gap-3 text-[13px] text-tfl15-mut"><span id="tfl-15-hint">At least 8 characters</span><span class="font-semibold group-data-[strength=1]/pw:text-tfl15-err group-data-[strength=2]/pw:text-tfl15-warn-ink group-data-[strength=3]/pw:text-tfl15-ok group-data-[strength=4]/pw:text-tfl15-ok" role="status" aria-live="polite" data-status></span></p>
</div>
<button class="min-h-12 cursor-pointer rounded-xl bg-tfl15-acc text-[15px] font-bold text-white shadow-[0_10px_24px_-12px_var(--color-tfl15-acc)] transition-[filter,scale] duration-150 hover:brightness-110 active:scale-98 focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tfl15-acc/50" type="button">Create account</button>
</form>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tfl15-bg: #f1f2f7;
--color-tfl15-bg: oklch(0.96 0.007 270);
--color-tfl15-ink: #191c2b;
--color-tfl15-ink: oklch(0.22 0.03 270);
--color-tfl15-mut: #62677c;
--color-tfl15-mut: oklch(0.5 0.03 270);
--color-tfl15-line: #d6d9e4;
--color-tfl15-line: oklch(0.88 0.015 270);
--color-tfl15-line-hover: #b4b9cc;
--color-tfl15-line-hover: oklch(0.78 0.025 270);
--color-tfl15-acc: #4a50d4;
--color-tfl15-acc: oklch(0.5 0.2 272);
--color-tfl15-err: #c8372e;
--color-tfl15-err: oklch(0.55 0.19 27);
--color-tfl15-warn: #e0a21a;
--color-tfl15-warn: oklch(0.76 0.15 78);
--color-tfl15-warn-ink: #8a5a00;
--color-tfl15-warn-ink: oklch(0.5 0.11 70);
--color-tfl15-ok: #1f8a4c;
--color-tfl15-ok: oklch(0.55 0.14 152);
--font-tfl15-sans: 'Geist', system-ui, sans-serif;
}
.tfl-15 {
width: 100%;
min-height: 100svh;
}
```
## JavaScript
```js
(() => {
const LABELS = ['', 'Weak', 'Fair', 'Good', 'Strong'];
const score = (v) => !v ? 0 : Math.max(1, [v.length >= 8, /[a-z]/.test(v) && /[A-Z]/.test(v), /\d/.test(v), /[^A-Za-z0-9]/.test(v)].filter(Boolean).length);
document.querySelectorAll('[data-tfl15-field]').forEach((field) => {
if (field.dataset.wired) return;
field.dataset.wired = '1';
const input = field.querySelector('input');
const btn = field.querySelector('[data-toggle]');
const status = field.querySelector('[data-status]');
btn.addEventListener('click', () => {
const show = input.type === 'password';
input.type = show ? 'text' : 'password';
btn.setAttribute('aria-pressed', String(show));
btn.setAttribute('aria-label', show ? 'Hide password' : 'Show password');
});
input.addEventListener('input', () => {
const s = score(input.value);
field.dataset.strength = String(s);
status.textContent = LABELS[s];
});
});
})();
```How this works
The script never touches a class. It sets type, aria-pressed, aria-label and data-strength; utilities do the rest:
<button class="group/eye …" aria-pressed="false">
<svg class="group-aria-pressed/eye:opacity-0"> <!-- eye -->
<svg class="opacity-0 group-aria-pressed/eye:opacity-100"> <!-- eye-off -->Named groups keep the two states apart: /pw is the field (strength), /eye is the button (visibility), so neither variant can match the other's attribute.
Scoring is one point each for length ≥ 8, mixed case, a digit and a symbol — a teaching meter, not a security audit.
Make it yours
- Keep the button 44×44 (
size-11); shrink only the icon. - Swap the SVG pair — both use
stroke="currentColor". - Meter colours: the
group-data-[strength=N]/pw:classes on each segment. - Scoring rules: the
score()function in the script.
Gotchas — read before shipping
- The toggle must be a real
<button type="button">— a div is invisible to keyboards, and a missing type submits the form. - Don't move focus on toggle; users are mid-typing.
- Keep
autocomplete="new-password"so password managers still fill after type swaps; never block paste.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
ARIA and data-attribute variants are plain attribute selectors; the script is ES2017.