24 / 30
All 30 in 30 Tailwind CSS Login Forms
- 01Tailwind Split-Screen Brand Hero Login Form
- 02Tailwind Multi-Tenant Workspace Account Switcher
- 03Tailwind Floating Label Login Form with peer Variants
- 04Tailwind Web3 Wallet Connect Login Grid
- 05Tailwind Minimalist Fintech Login Card with Layered Shadow
- 06Tailwind OAuth Social Login Form with Brand SVG Buttons
- 07Tailwind Passwordless Magic Link Login with data-state Machine
- 08Tailwind Aurora Glow Login Card with Drifting Blur Blobs
- 09Tailwind Neon Synthwave Arcade Login Form
- 10Tailwind Sliding Login and Sign Up Panel with peer-checked
- 11Tailwind 3D Flip Login Card with Password Reset Back Face
- 12Tailwind Terminal CLI Prompt Login Form
- 13Tailwind Floating Pill Login Widget Docked Over App Content
- 14Tailwind Gradient Border Login Card with background-clip
- 15Tailwind Slide-Over Side Panel Login without JavaScript
- 16Tailwind Social-First Sign Up with Brand-Compliant Buttons
- 17Tailwind Animated Mascot Login that Covers Its Eyes
- 18Tailwind Accessible Compact Login Modal with Scale-In
- 19Tailwind Glassmorphism Login Card with backdrop-filter
- 20Tailwind Multi-Step Login Wizard with Radio-Driven Track
- 21Tailwind Animated Conic Gradient Border Login Card
- 22Tailwind Secure Vault Login with user-invalid Error State
- 23Tailwind Neo-Brutalist Login Form with Hard Offset Shadow
- 24Tailwind Show Hide Password Toggle with aria-pressed
- 25Tailwind OTP Code Input Grid with Auto-Advance
- 26Tailwind Password Strength Meter with data-score Variants
- 27Tailwind Numeric PIN Pad Login for Kiosks and POS
- 28Tailwind Inline Form Validation with user-valid and user-invalid
- 29Tailwind Particle Constellation Login over Canvas
- 30Tailwind Biometric Face ID Prompt with Pulse Ripples
Tailwind Show Hide Password Toggle with aria-pressed
The everyday accessibility win: an eye button inside the password field flips it between hidden and visible so users can catch typos. The port's move is styling from the accessibility attribute — aria-[pressed=true]: draws the strike-through on the eye, so the visual state and the announced state literally cannot diverge.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="tlf-24 w-full min-h-svh grid place-items-center bg-tlf24-stage font-tlf24-sans p-6">
<form novalidate class="w-[min(380px,100%)] flex flex-col gap-3.5 p-8 bg-white border border-tlf24-edge rounded-2xl shadow-[0_22px_50px_-38px_oklch(0.25_0.06_265/0.5)]">
<h1 class="text-[23px] font-bold text-tlf24-ink">Log in</h1>
<label class="flex flex-col gap-1.5"><span class="text-[13px] font-semibold text-tlf24-label">Email</span>
<input type="email" autocomplete="email" placeholder="you@company.com" required class="h-[50px] px-3.5 rounded-xl text-[15px] text-tlf24-ink bg-tlf24-field border-[1.5px] border-tlf24-fieldedge transition-[border-color,box-shadow] duration-150 placeholder:text-tlf24-ph focus-visible:outline-none focus-visible:border-tlf24-acc focus-visible:ring-4 focus-visible:ring-tlf24-halo"></label>
<div class="flex flex-col gap-1.5">
<label for="tlf-24-pw" class="text-[13px] font-semibold text-tlf24-label">Password</label>
<div class="relative">
<input id="tlf-24-pw" class="tlf-24-pw w-full h-[50px] pl-3.5 pr-14 rounded-xl text-[15px] text-tlf24-ink bg-tlf24-field border-[1.5px] border-tlf24-fieldedge transition-[border-color,box-shadow] duration-150 placeholder:text-tlf24-ph focus-visible:outline-none focus-visible:border-tlf24-acc focus-visible:ring-4 focus-visible:ring-tlf24-halo" type="password" autocomplete="current-password" placeholder="••••••••" required>
<button type="button" aria-pressed="false" aria-label="Show password" class="tlf-24-eye group absolute right-1.5 top-1/2 -translate-y-1/2 grid place-items-center size-11 rounded-[10px] border-0 cursor-pointer bg-transparent text-tlf24-mut transition-colors duration-150 hover:bg-tlf24-stage hover:text-tlf24-acc focus-visible:outline-3 focus-visible:outline-offset-1 focus-visible:outline-tlf24-ring aria-[pressed=true]:text-tlf24-acc">
<span aria-hidden="true" class="relative block w-[22px] h-3.5 border-2 border-current rounded-[50%/60%] after:content-[''] after:absolute after:left-1/2 after:top-1/2 after:size-1.5 after:-translate-x-1/2 after:-translate-y-1/2 after:rounded-full after:bg-current before:content-[''] before:absolute before:-left-[3px] before:top-[5px] before:w-7 before:h-0.5 before:bg-current before:origin-center before:rotate-[-30deg] before:opacity-0 before:transition-opacity before:duration-150 group-aria-[pressed=true]:before:opacity-100"></span>
</button>
</div>
</div>
<button type="submit" class="mt-1 h-[50px] rounded-xl border-0 cursor-pointer bg-tlf24-acc text-white text-[15px] font-semibold transition-[filter,scale] duration-150 hover:brightness-110 active:scale-[0.985] focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tlf24-ring motion-reduce:transition-none motion-reduce:active:scale-100">Continue</button>
</form>
</section><section class="tlf-24 w-full min-h-svh grid place-items-center bg-tlf24-stage font-tlf24-sans p-6">
<form novalidate class="w-[min(380px,100%)] flex flex-col gap-3.5 p-8 bg-white border border-tlf24-edge rounded-2xl shadow-[0_22px_50px_-38px_oklch(0.25_0.06_265/0.5)]">
<h1 class="text-[23px] font-bold text-tlf24-ink">Log in</h1>
<label class="flex flex-col gap-1.5"><span class="text-[13px] font-semibold text-tlf24-label">Email</span>
<input type="email" autocomplete="email" placeholder="you@company.com" required class="h-[50px] px-3.5 rounded-xl text-[15px] text-tlf24-ink bg-tlf24-field border-[1.5px] border-tlf24-fieldedge transition-[border-color,box-shadow] duration-150 placeholder:text-tlf24-ph focus-visible:outline-none focus-visible:border-tlf24-acc focus-visible:ring-4 focus-visible:ring-tlf24-halo"></label>
<div class="flex flex-col gap-1.5">
<label for="tlf-24-pw" class="text-[13px] font-semibold text-tlf24-label">Password</label>
<div class="relative">
<input id="tlf-24-pw" class="tlf-24-pw w-full h-[50px] pl-3.5 pr-14 rounded-xl text-[15px] text-tlf24-ink bg-tlf24-field border-[1.5px] border-tlf24-fieldedge transition-[border-color,box-shadow] duration-150 placeholder:text-tlf24-ph focus-visible:outline-none focus-visible:border-tlf24-acc focus-visible:ring-4 focus-visible:ring-tlf24-halo" type="password" autocomplete="current-password" placeholder="••••••••" required>
<button type="button" aria-pressed="false" aria-label="Show password" class="tlf-24-eye group absolute right-1.5 top-1/2 -translate-y-1/2 grid place-items-center size-11 rounded-[10px] border-0 cursor-pointer bg-transparent text-tlf24-mut transition-colors duration-150 hover:bg-tlf24-stage hover:text-tlf24-acc focus-visible:outline-3 focus-visible:outline-offset-1 focus-visible:outline-tlf24-ring aria-[pressed=true]:text-tlf24-acc">
<span aria-hidden="true" class="relative block w-[22px] h-3.5 border-2 border-current rounded-[50%/60%] after:content-[''] after:absolute after:left-1/2 after:top-1/2 after:size-1.5 after:-translate-x-1/2 after:-translate-y-1/2 after:rounded-full after:bg-current before:content-[''] before:absolute before:-left-[3px] before:top-[5px] before:w-7 before:h-0.5 before:bg-current before:origin-center before:rotate-[-30deg] before:opacity-0 before:transition-opacity before:duration-150 group-aria-[pressed=true]:before:opacity-100"></span>
</button>
</div>
</div>
<button type="submit" class="mt-1 h-[50px] rounded-xl border-0 cursor-pointer bg-tlf24-acc text-white text-[15px] font-semibold transition-[filter,scale] duration-150 hover:brightness-110 active:scale-[0.985] focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tlf24-ring motion-reduce:transition-none motion-reduce:active:scale-100">Continue</button>
</form>
</section>@import "tailwindcss";
@theme {
--color-tlf24-stage: #eef1f7;
--color-tlf24-stage: oklch(0.95 0.008 260);
--color-tlf24-edge: #e5e9f2;
--color-tlf24-edge: oklch(0.92 0.008 260);
--color-tlf24-fieldedge: #d8deeb;
--color-tlf24-fieldedge: oklch(0.88 0.012 260);
--color-tlf24-field: #fafbfe;
--color-tlf24-field: oklch(0.99 0.003 260);
--color-tlf24-ink: #1a2237;
--color-tlf24-ink: oklch(0.25 0.04 260);
--color-tlf24-label: #3d465f;
--color-tlf24-label: oklch(0.37 0.035 260);
--color-tlf24-mut: #5f6884;
--color-tlf24-mut: oklch(0.5 0.03 260);
--color-tlf24-ph: #949db5;
--color-tlf24-ph: oklch(0.67 0.025 260);
--color-tlf24-acc: #3b63c4;
--color-tlf24-acc: oklch(0.5 0.16 258);
--color-tlf24-halo: #bfcdee;
--color-tlf24-halo: oklch(0.5 0.16 258 / 0.16);
--color-tlf24-ring: #94a8e6;
--color-tlf24-ring: oklch(0.5 0.16 258 / 0.5);
--font-tlf24-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
.tlf-24 {
width: 100%;
min-height: 100svh;
}@import "tailwindcss";
@theme {
--color-tlf24-stage: #eef1f7;
--color-tlf24-stage: oklch(0.95 0.008 260);
--color-tlf24-edge: #e5e9f2;
--color-tlf24-edge: oklch(0.92 0.008 260);
--color-tlf24-fieldedge: #d8deeb;
--color-tlf24-fieldedge: oklch(0.88 0.012 260);
--color-tlf24-field: #fafbfe;
--color-tlf24-field: oklch(0.99 0.003 260);
--color-tlf24-ink: #1a2237;
--color-tlf24-ink: oklch(0.25 0.04 260);
--color-tlf24-label: #3d465f;
--color-tlf24-label: oklch(0.37 0.035 260);
--color-tlf24-mut: #5f6884;
--color-tlf24-mut: oklch(0.5 0.03 260);
--color-tlf24-ph: #949db5;
--color-tlf24-ph: oklch(0.67 0.025 260);
--color-tlf24-acc: #3b63c4;
--color-tlf24-acc: oklch(0.5 0.16 258);
--color-tlf24-halo: #bfcdee;
--color-tlf24-halo: oklch(0.5 0.16 258 / 0.16);
--color-tlf24-ring: #94a8e6;
--color-tlf24-ring: oklch(0.5 0.16 258 / 0.5);
--font-tlf24-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
.tlf-24 {
width: 100%;
min-height: 100svh;
}const section = document.querySelector('.tlf-24');
const btn = section.querySelector('.tlf-24-eye');
const pw = section.querySelector('.tlf-24-pw');
btn.addEventListener('click', () => {
const show = pw.type === 'password';
pw.type = show ? 'text' : 'password';
btn.setAttribute('aria-pressed', String(show));
btn.setAttribute('aria-label', show ? 'Hide password' : 'Show password');
pw.focus();
});const section = document.querySelector('.tlf-24');
const btn = section.querySelector('.tlf-24-eye');
const pw = section.querySelector('.tlf-24-pw');
btn.addEventListener('click', () => {
const show = pw.type === 'password';
pw.type = show ? 'text' : 'password';
btn.setAttribute('aria-pressed', String(show));
btn.setAttribute('aria-label', show ? 'Hide password' : 'Show password');
pw.focus();
});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-tlf24-stage: #eef1f7;
--color-tlf24-stage: oklch(0.95 0.008 260);
--color-tlf24-edge: #e5e9f2;
--color-tlf24-edge: oklch(0.92 0.008 260);
--color-tlf24-fieldedge: #d8deeb;
--color-tlf24-fieldedge: oklch(0.88 0.012 260);
--color-tlf24-field: #fafbfe;
--color-tlf24-field: oklch(0.99 0.003 260);
--color-tlf24-ink: #1a2237;
--color-tlf24-ink: oklch(0.25 0.04 260);
--color-tlf24-label: #3d465f;
--color-tlf24-label: oklch(0.37 0.035 260);
--color-tlf24-mut: #5f6884;
--color-tlf24-mut: oklch(0.5 0.03 260);
--color-tlf24-ph: #949db5;
--color-tlf24-ph: oklch(0.67 0.025 260);
--color-tlf24-acc: #3b63c4;
--color-tlf24-acc: oklch(0.5 0.16 258);
--color-tlf24-halo: #bfcdee;
--color-tlf24-halo: oklch(0.5 0.16 258 / 0.16);
--color-tlf24-ring: #94a8e6;
--color-tlf24-ring: oklch(0.5 0.16 258 / 0.5);
--font-tlf24-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
.tlf-24 {
width: 100%;
min-height: 100svh;
}@import "tailwindcss";
@theme {
--color-tlf24-stage: #eef1f7;
--color-tlf24-stage: oklch(0.95 0.008 260);
--color-tlf24-edge: #e5e9f2;
--color-tlf24-edge: oklch(0.92 0.008 260);
--color-tlf24-fieldedge: #d8deeb;
--color-tlf24-fieldedge: oklch(0.88 0.012 260);
--color-tlf24-field: #fafbfe;
--color-tlf24-field: oklch(0.99 0.003 260);
--color-tlf24-ink: #1a2237;
--color-tlf24-ink: oklch(0.25 0.04 260);
--color-tlf24-label: #3d465f;
--color-tlf24-label: oklch(0.37 0.035 260);
--color-tlf24-mut: #5f6884;
--color-tlf24-mut: oklch(0.5 0.03 260);
--color-tlf24-ph: #949db5;
--color-tlf24-ph: oklch(0.67 0.025 260);
--color-tlf24-acc: #3b63c4;
--color-tlf24-acc: oklch(0.5 0.16 258);
--color-tlf24-halo: #bfcdee;
--color-tlf24-halo: oklch(0.5 0.16 258 / 0.16);
--color-tlf24-ring: #94a8e6;
--color-tlf24-ring: oklch(0.5 0.16 258 / 0.5);
--font-tlf24-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
.tlf-24 {
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=Plus+Jakarta+Sans:wght@400;500;600;700&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=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
group-aria-[pressed=true]:before:opacity-100is the whole port: the slash on the eye is drawn by the same attribute a screen reader reads, so a future refactor cannot leave the icon lying about the state.- The eye is CSS-drawn with
rounded-[50%/60%]— a two-radius value that Tailwind passes through intact, and a good reminder that arbitrary values accept the full CSS grammar. pr-14andsize-11are a matched pair; noting that incustomizationrather than leaving it implicit is the difference between a snippet and a component.- UI change from the CSS original: Plus Jakarta Sans replaces the system stack, the toggle grew from 36px to
size-11(44px) with the field padding adjusted to match, the muted icon colour darkened from#8a91a8tooklch(0.5 0.03 260), and the accent deepened for white-text contrast.
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 Show Hide Password Toggle with aria-pressed
Source: https://codefronts.com/components/tailwind-css-login-forms/tailwind-show-hide-password-toggle-aria-pressed/
The everyday accessibility win: an eye button inside the password field flips it between hidden and visible so users can catch typos. The port's move is styling from the accessibility attribute — aria-[pressed=true]: draws the strike-through on the eye, so the visual state and the announced state literally cannot diverge.
## HTML
```html
<section class="tlf-24 w-full min-h-svh grid place-items-center bg-tlf24-stage font-tlf24-sans p-6">
<form novalidate class="w-[min(380px,100%)] flex flex-col gap-3.5 p-8 bg-white border border-tlf24-edge rounded-2xl shadow-[0_22px_50px_-38px_oklch(0.25_0.06_265/0.5)]">
<h1 class="text-[23px] font-bold text-tlf24-ink">Log in</h1>
<label class="flex flex-col gap-1.5"><span class="text-[13px] font-semibold text-tlf24-label">Email</span>
<input type="email" autocomplete="email" placeholder="you@company.com" required class="h-[50px] px-3.5 rounded-xl text-[15px] text-tlf24-ink bg-tlf24-field border-[1.5px] border-tlf24-fieldedge transition-[border-color,box-shadow] duration-150 placeholder:text-tlf24-ph focus-visible:outline-none focus-visible:border-tlf24-acc focus-visible:ring-4 focus-visible:ring-tlf24-halo"></label>
<div class="flex flex-col gap-1.5">
<label for="tlf-24-pw" class="text-[13px] font-semibold text-tlf24-label">Password</label>
<div class="relative">
<input id="tlf-24-pw" class="tlf-24-pw w-full h-[50px] pl-3.5 pr-14 rounded-xl text-[15px] text-tlf24-ink bg-tlf24-field border-[1.5px] border-tlf24-fieldedge transition-[border-color,box-shadow] duration-150 placeholder:text-tlf24-ph focus-visible:outline-none focus-visible:border-tlf24-acc focus-visible:ring-4 focus-visible:ring-tlf24-halo" type="password" autocomplete="current-password" placeholder="••••••••" required>
<button type="button" aria-pressed="false" aria-label="Show password" class="tlf-24-eye group absolute right-1.5 top-1/2 -translate-y-1/2 grid place-items-center size-11 rounded-[10px] border-0 cursor-pointer bg-transparent text-tlf24-mut transition-colors duration-150 hover:bg-tlf24-stage hover:text-tlf24-acc focus-visible:outline-3 focus-visible:outline-offset-1 focus-visible:outline-tlf24-ring aria-[pressed=true]:text-tlf24-acc">
<span aria-hidden="true" class="relative block w-[22px] h-3.5 border-2 border-current rounded-[50%/60%] after:content-[''] after:absolute after:left-1/2 after:top-1/2 after:size-1.5 after:-translate-x-1/2 after:-translate-y-1/2 after:rounded-full after:bg-current before:content-[''] before:absolute before:-left-[3px] before:top-[5px] before:w-7 before:h-0.5 before:bg-current before:origin-center before:rotate-[-30deg] before:opacity-0 before:transition-opacity before:duration-150 group-aria-[pressed=true]:before:opacity-100"></span>
</button>
</div>
</div>
<button type="submit" class="mt-1 h-[50px] rounded-xl border-0 cursor-pointer bg-tlf24-acc text-white text-[15px] font-semibold transition-[filter,scale] duration-150 hover:brightness-110 active:scale-[0.985] focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tlf24-ring motion-reduce:transition-none motion-reduce:active:scale-100">Continue</button>
</form>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tlf24-stage: #eef1f7;
--color-tlf24-stage: oklch(0.95 0.008 260);
--color-tlf24-edge: #e5e9f2;
--color-tlf24-edge: oklch(0.92 0.008 260);
--color-tlf24-fieldedge: #d8deeb;
--color-tlf24-fieldedge: oklch(0.88 0.012 260);
--color-tlf24-field: #fafbfe;
--color-tlf24-field: oklch(0.99 0.003 260);
--color-tlf24-ink: #1a2237;
--color-tlf24-ink: oklch(0.25 0.04 260);
--color-tlf24-label: #3d465f;
--color-tlf24-label: oklch(0.37 0.035 260);
--color-tlf24-mut: #5f6884;
--color-tlf24-mut: oklch(0.5 0.03 260);
--color-tlf24-ph: #949db5;
--color-tlf24-ph: oklch(0.67 0.025 260);
--color-tlf24-acc: #3b63c4;
--color-tlf24-acc: oklch(0.5 0.16 258);
--color-tlf24-halo: #bfcdee;
--color-tlf24-halo: oklch(0.5 0.16 258 / 0.16);
--color-tlf24-ring: #94a8e6;
--color-tlf24-ring: oklch(0.5 0.16 258 / 0.5);
--font-tlf24-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
.tlf-24 {
width: 100%;
min-height: 100svh;
}
```
## JavaScript
```js
const section = document.querySelector('.tlf-24');
const btn = section.querySelector('.tlf-24-eye');
const pw = section.querySelector('.tlf-24-pw');
btn.addEventListener('click', () => {
const show = pw.type === 'password';
pw.type = show ? 'text' : 'password';
btn.setAttribute('aria-pressed', String(show));
btn.setAttribute('aria-label', show ? 'Hide password' : 'Show password');
pw.focus();
});
```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 Show Hide Password Toggle with aria-pressed
Source: https://codefronts.com/components/tailwind-css-login-forms/tailwind-show-hide-password-toggle-aria-pressed/
The everyday accessibility win: an eye button inside the password field flips it between hidden and visible so users can catch typos. The port's move is styling from the accessibility attribute — aria-[pressed=true]: draws the strike-through on the eye, so the visual state and the announced state literally cannot diverge.
## HTML
```html
<section class="tlf-24 w-full min-h-svh grid place-items-center bg-tlf24-stage font-tlf24-sans p-6">
<form novalidate class="w-[min(380px,100%)] flex flex-col gap-3.5 p-8 bg-white border border-tlf24-edge rounded-2xl shadow-[0_22px_50px_-38px_oklch(0.25_0.06_265/0.5)]">
<h1 class="text-[23px] font-bold text-tlf24-ink">Log in</h1>
<label class="flex flex-col gap-1.5"><span class="text-[13px] font-semibold text-tlf24-label">Email</span>
<input type="email" autocomplete="email" placeholder="you@company.com" required class="h-[50px] px-3.5 rounded-xl text-[15px] text-tlf24-ink bg-tlf24-field border-[1.5px] border-tlf24-fieldedge transition-[border-color,box-shadow] duration-150 placeholder:text-tlf24-ph focus-visible:outline-none focus-visible:border-tlf24-acc focus-visible:ring-4 focus-visible:ring-tlf24-halo"></label>
<div class="flex flex-col gap-1.5">
<label for="tlf-24-pw" class="text-[13px] font-semibold text-tlf24-label">Password</label>
<div class="relative">
<input id="tlf-24-pw" class="tlf-24-pw w-full h-[50px] pl-3.5 pr-14 rounded-xl text-[15px] text-tlf24-ink bg-tlf24-field border-[1.5px] border-tlf24-fieldedge transition-[border-color,box-shadow] duration-150 placeholder:text-tlf24-ph focus-visible:outline-none focus-visible:border-tlf24-acc focus-visible:ring-4 focus-visible:ring-tlf24-halo" type="password" autocomplete="current-password" placeholder="••••••••" required>
<button type="button" aria-pressed="false" aria-label="Show password" class="tlf-24-eye group absolute right-1.5 top-1/2 -translate-y-1/2 grid place-items-center size-11 rounded-[10px] border-0 cursor-pointer bg-transparent text-tlf24-mut transition-colors duration-150 hover:bg-tlf24-stage hover:text-tlf24-acc focus-visible:outline-3 focus-visible:outline-offset-1 focus-visible:outline-tlf24-ring aria-[pressed=true]:text-tlf24-acc">
<span aria-hidden="true" class="relative block w-[22px] h-3.5 border-2 border-current rounded-[50%/60%] after:content-[''] after:absolute after:left-1/2 after:top-1/2 after:size-1.5 after:-translate-x-1/2 after:-translate-y-1/2 after:rounded-full after:bg-current before:content-[''] before:absolute before:-left-[3px] before:top-[5px] before:w-7 before:h-0.5 before:bg-current before:origin-center before:rotate-[-30deg] before:opacity-0 before:transition-opacity before:duration-150 group-aria-[pressed=true]:before:opacity-100"></span>
</button>
</div>
</div>
<button type="submit" class="mt-1 h-[50px] rounded-xl border-0 cursor-pointer bg-tlf24-acc text-white text-[15px] font-semibold transition-[filter,scale] duration-150 hover:brightness-110 active:scale-[0.985] focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tlf24-ring motion-reduce:transition-none motion-reduce:active:scale-100">Continue</button>
</form>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tlf24-stage: #eef1f7;
--color-tlf24-stage: oklch(0.95 0.008 260);
--color-tlf24-edge: #e5e9f2;
--color-tlf24-edge: oklch(0.92 0.008 260);
--color-tlf24-fieldedge: #d8deeb;
--color-tlf24-fieldedge: oklch(0.88 0.012 260);
--color-tlf24-field: #fafbfe;
--color-tlf24-field: oklch(0.99 0.003 260);
--color-tlf24-ink: #1a2237;
--color-tlf24-ink: oklch(0.25 0.04 260);
--color-tlf24-label: #3d465f;
--color-tlf24-label: oklch(0.37 0.035 260);
--color-tlf24-mut: #5f6884;
--color-tlf24-mut: oklch(0.5 0.03 260);
--color-tlf24-ph: #949db5;
--color-tlf24-ph: oklch(0.67 0.025 260);
--color-tlf24-acc: #3b63c4;
--color-tlf24-acc: oklch(0.5 0.16 258);
--color-tlf24-halo: #bfcdee;
--color-tlf24-halo: oklch(0.5 0.16 258 / 0.16);
--color-tlf24-ring: #94a8e6;
--color-tlf24-ring: oklch(0.5 0.16 258 / 0.5);
--font-tlf24-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
.tlf-24 {
width: 100%;
min-height: 100svh;
}
```
## JavaScript
```js
const section = document.querySelector('.tlf-24');
const btn = section.querySelector('.tlf-24-eye');
const pw = section.querySelector('.tlf-24-pw');
btn.addEventListener('click', () => {
const show = pw.type === 'password';
pw.type = show ? 'text' : 'password';
btn.setAttribute('aria-pressed', String(show));
btn.setAttribute('aria-label', show ? 'Hide password' : 'Show password');
pw.focus();
});
```How this works
JS does the one thing CSS cannot. An input's type is not settable from CSS, so a four-line handler flips password↔text and updates aria-pressed and aria-label. Everything visual keys off that attribute, which means there is no second source of truth to keep in sync.
Reserve the space, don't overlap it. pr-14 on the field is the difference between a toggle and a bug report: without it a long password runs under the button and the last characters are unreadable at exactly the moment the user wanted to read them.
It stays a native password field. Type-flipping does not break password managers, and autocomplete="current-password" survives the switch — which is why this is the right implementation rather than a masked text field.
Make it yours
- Accent:
--color-tlf24-accdrives the hover tint, the ring and the field focus. - Swap the CSS eye for an inline SVG pair — keep
aria-hiddenon the art and the label on the button. - Button size:
size-11plus the matchingpr-14— change both. - Add a strength meter beneath (demo 26) using the same field.
Gotchas — read before shipping
- Toggle
aria-pressedANDaria-label. A glyph swap alone tells a screen-reader user nothing. - Keep the button
type="button", or it submits the form on click. - Return focus to the input after the toggle (the handler does), otherwise keyboard users lose their place mid-password.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
Attribute-selector variants and CSS-drawn glyphs are universally supported; the type flip needs the handler.