30 Tailwind CSS Login Forms
27 / 30

All 30 in 30 Tailwind CSS Login Forms

  1. 01Tailwind Split-Screen Brand Hero Login Form
  2. 02Tailwind Multi-Tenant Workspace Account Switcher
  3. 03Tailwind Floating Label Login Form with peer Variants
  4. 04Tailwind Web3 Wallet Connect Login Grid
  5. 05Tailwind Minimalist Fintech Login Card with Layered Shadow
  6. 06Tailwind OAuth Social Login Form with Brand SVG Buttons
  7. 07Tailwind Passwordless Magic Link Login with data-state Machine
  8. 08Tailwind Aurora Glow Login Card with Drifting Blur Blobs
  9. 09Tailwind Neon Synthwave Arcade Login Form
  10. 10Tailwind Sliding Login and Sign Up Panel with peer-checked
  11. 11Tailwind 3D Flip Login Card with Password Reset Back Face
  12. 12Tailwind Terminal CLI Prompt Login Form
  13. 13Tailwind Floating Pill Login Widget Docked Over App Content
  14. 14Tailwind Gradient Border Login Card with background-clip
  15. 15Tailwind Slide-Over Side Panel Login without JavaScript
  16. 16Tailwind Social-First Sign Up with Brand-Compliant Buttons
  17. 17Tailwind Animated Mascot Login that Covers Its Eyes
  18. 18Tailwind Accessible Compact Login Modal with Scale-In
  19. 19Tailwind Glassmorphism Login Card with backdrop-filter
  20. 20Tailwind Multi-Step Login Wizard with Radio-Driven Track
  21. 21Tailwind Animated Conic Gradient Border Login Card
  22. 22Tailwind Secure Vault Login with user-invalid Error State
  23. 23Tailwind Neo-Brutalist Login Form with Hard Offset Shadow
  24. 24Tailwind Show Hide Password Toggle with aria-pressed
  25. 25Tailwind OTP Code Input Grid with Auto-Advance
  26. 26Tailwind Password Strength Meter with data-score Variants
  27. 27Tailwind Numeric PIN Pad Login for Kiosks and POS
  28. 28Tailwind Inline Form Validation with user-valid and user-invalid
  29. 29Tailwind Particle Constellation Login over Canvas
  30. 30Tailwind Biometric Face ID Prompt with Pulse Ripples
Tailwind + JSMIT licensed

Tailwind Numeric PIN Pad Login for Kiosks and POS

A numeric PIN entry for kiosks, POS terminals and mobile web: a rigid 3×4 keypad of big round keys feeding a row of masked dots, with a press that scales and a delete key. The port keeps the script to state and moves the filled-dot look to data-[on]:, so the handler sets an attribute instead of toggling a class name it has to remember.

Live Demo
Plain CSSTailwind v4
Try it

Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time

Untitled UI React · Affiliate link

Get Untitled UI Pro

The code

<section class="tlf-27 w-full min-h-svh grid place-items-center bg-tlf27-stage font-tlf27-sans p-6">
  <form novalidate class="tlf-27-form w-[min(340px,100%)] p-8 text-center bg-tlf27-card border border-tlf27-edge rounded-[22px] shadow-[0_30px_70px_-44px_black]">
    <h1 class="mb-5 text-[19px] font-semibold text-tlf27-ink">Enter your PIN</h1>
    <div aria-hidden="true" class="tlf-27-dots flex justify-center gap-4 mb-7">
      <span class="size-3.5 rounded-full border-2 border-tlf27-dot bg-transparent transition-[background-color,border-color,scale] duration-150 data-[on]:bg-tlf27-acc data-[on]:border-tlf27-acc data-[on]:scale-115 motion-reduce:transition-colors"></span>
      <span class="size-3.5 rounded-full border-2 border-tlf27-dot bg-transparent transition-[background-color,border-color,scale] duration-150 data-[on]:bg-tlf27-acc data-[on]:border-tlf27-acc data-[on]:scale-115 motion-reduce:transition-colors"></span>
      <span class="size-3.5 rounded-full border-2 border-tlf27-dot bg-transparent transition-[background-color,border-color,scale] duration-150 data-[on]:bg-tlf27-acc data-[on]:border-tlf27-acc data-[on]:scale-115 motion-reduce:transition-colors"></span>
      <span class="size-3.5 rounded-full border-2 border-tlf27-dot bg-transparent transition-[background-color,border-color,scale] duration-150 data-[on]:bg-tlf27-acc data-[on]:border-tlf27-acc data-[on]:scale-115 motion-reduce:transition-colors"></span>
    </div>
    <input class="tlf-27-value" type="hidden" name="pin">
    <p class="tlf-27-status sr-only" role="status" aria-live="polite">0 of 4 digits entered</p>
    <div role="group" aria-label="PIN keypad" class="grid grid-cols-3 gap-3.5 justify-items-center">
      <button type="button" data-d="1" class="tlf-27-key size-16 rounded-full cursor-pointer bg-tlf27-key border-[1.5px] border-tlf27-keyedge text-tlf27-ink text-2xl font-medium transition-[background-color,border-color,scale] duration-100 hover:bg-tlf27-keyhover hover:border-tlf27-dot active:scale-90 active:bg-tlf27-press active:border-tlf27-acc focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tlf27-ring motion-reduce:transition-colors motion-reduce:active:scale-100">1</button>
      <button type="button" data-d="2" class="tlf-27-key size-16 rounded-full cursor-pointer bg-tlf27-key border-[1.5px] border-tlf27-keyedge text-tlf27-ink text-2xl font-medium transition-[background-color,border-color,scale] duration-100 hover:bg-tlf27-keyhover hover:border-tlf27-dot active:scale-90 active:bg-tlf27-press active:border-tlf27-acc focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tlf27-ring motion-reduce:transition-colors motion-reduce:active:scale-100">2</button>
      <button type="button" data-d="3" class="tlf-27-key size-16 rounded-full cursor-pointer bg-tlf27-key border-[1.5px] border-tlf27-keyedge text-tlf27-ink text-2xl font-medium transition-[background-color,border-color,scale] duration-100 hover:bg-tlf27-keyhover hover:border-tlf27-dot active:scale-90 active:bg-tlf27-press active:border-tlf27-acc focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tlf27-ring motion-reduce:transition-colors motion-reduce:active:scale-100">3</button>
      <button type="button" data-d="4" class="tlf-27-key size-16 rounded-full cursor-pointer bg-tlf27-key border-[1.5px] border-tlf27-keyedge text-tlf27-ink text-2xl font-medium transition-[background-color,border-color,scale] duration-100 hover:bg-tlf27-keyhover hover:border-tlf27-dot active:scale-90 active:bg-tlf27-press active:border-tlf27-acc focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tlf27-ring motion-reduce:transition-colors motion-reduce:active:scale-100">4</button>
      <button type="button" data-d="5" class="tlf-27-key size-16 rounded-full cursor-pointer bg-tlf27-key border-[1.5px] border-tlf27-keyedge text-tlf27-ink text-2xl font-medium transition-[background-color,border-color,scale] duration-100 hover:bg-tlf27-keyhover hover:border-tlf27-dot active:scale-90 active:bg-tlf27-press active:border-tlf27-acc focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tlf27-ring motion-reduce:transition-colors motion-reduce:active:scale-100">5</button>
      <button type="button" data-d="6" class="tlf-27-key size-16 rounded-full cursor-pointer bg-tlf27-key border-[1.5px] border-tlf27-keyedge text-tlf27-ink text-2xl font-medium transition-[background-color,border-color,scale] duration-100 hover:bg-tlf27-keyhover hover:border-tlf27-dot active:scale-90 active:bg-tlf27-press active:border-tlf27-acc focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tlf27-ring motion-reduce:transition-colors motion-reduce:active:scale-100">6</button>
      <button type="button" data-d="7" class="tlf-27-key size-16 rounded-full cursor-pointer bg-tlf27-key border-[1.5px] border-tlf27-keyedge text-tlf27-ink text-2xl font-medium transition-[background-color,border-color,scale] duration-100 hover:bg-tlf27-keyhover hover:border-tlf27-dot active:scale-90 active:bg-tlf27-press active:border-tlf27-acc focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tlf27-ring motion-reduce:transition-colors motion-reduce:active:scale-100">7</button>
      <button type="button" data-d="8" class="tlf-27-key size-16 rounded-full cursor-pointer bg-tlf27-key border-[1.5px] border-tlf27-keyedge text-tlf27-ink text-2xl font-medium transition-[background-color,border-color,scale] duration-100 hover:bg-tlf27-keyhover hover:border-tlf27-dot active:scale-90 active:bg-tlf27-press active:border-tlf27-acc focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tlf27-ring motion-reduce:transition-colors motion-reduce:active:scale-100">8</button>
      <button type="button" data-d="9" class="tlf-27-key size-16 rounded-full cursor-pointer bg-tlf27-key border-[1.5px] border-tlf27-keyedge text-tlf27-ink text-2xl font-medium transition-[background-color,border-color,scale] duration-100 hover:bg-tlf27-keyhover hover:border-tlf27-dot active:scale-90 active:bg-tlf27-press active:border-tlf27-acc focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tlf27-ring motion-reduce:transition-colors motion-reduce:active:scale-100">9</button>
      <span aria-hidden="true" class="size-16"></span>
      <button type="button" data-d="0" class="tlf-27-key size-16 rounded-full cursor-pointer bg-tlf27-key border-[1.5px] border-tlf27-keyedge text-tlf27-ink text-2xl font-medium transition-[background-color,border-color,scale] duration-100 hover:bg-tlf27-keyhover hover:border-tlf27-dot active:scale-90 active:bg-tlf27-press active:border-tlf27-acc focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tlf27-ring motion-reduce:transition-colors motion-reduce:active:scale-100">0</button>
      <button type="button" data-del aria-label="Delete last digit" class="tlf-27-key size-16 rounded-full cursor-pointer bg-tlf27-key border-[1.5px] border-tlf27-keyedge text-tlf27-mut text-xl transition-[background-color,border-color,scale] duration-100 hover:bg-tlf27-keyhover hover:border-tlf27-dot active:scale-90 active:bg-tlf27-press active:border-tlf27-acc focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tlf27-ring motion-reduce:transition-colors motion-reduce:active:scale-100">&#9003;</button>
    </div>
  </form>
</section>
@import "tailwindcss";

@theme {
  --color-tlf27-stage: #12151d;
  --color-tlf27-stage: oklch(0.18 0.012 265);
  --color-tlf27-card: #1a1f2b;
  --color-tlf27-card: oklch(0.23 0.014 265);
  --color-tlf27-edge: #262c3a;
  --color-tlf27-edge: oklch(0.28 0.016 265);
  --color-tlf27-key: #212635;
  --color-tlf27-key: oklch(0.26 0.016 265);
  --color-tlf27-keyhover: #2b3244;
  --color-tlf27-keyhover: oklch(0.31 0.018 265);
  --color-tlf27-keyedge: #333b4d;
  --color-tlf27-keyedge: oklch(0.33 0.018 265);
  --color-tlf27-dot: #4a5468;
  --color-tlf27-dot: oklch(0.43 0.02 265);
  --color-tlf27-ink: #e8ebf3;
  --color-tlf27-ink: oklch(0.93 0.008 265);
  --color-tlf27-mut: #a8b0c4;
  --color-tlf27-mut: oklch(0.75 0.018 265);
  --color-tlf27-acc: #7b90e8;
  --color-tlf27-acc: oklch(0.68 0.15 265);
  --color-tlf27-press: #2f3a5e;
  --color-tlf27-press: oklch(0.68 0.15 265 / 0.25);
  --color-tlf27-ring: #a3b2ef;
  --color-tlf27-ring: oklch(0.68 0.15 265 / 0.6);
  --font-tlf27-sans: 'Figtree', system-ui, sans-serif;
}

.tlf-27 {
  width: 100%;
  min-height: 100svh;
  color-scheme: dark;
}
const section = document.querySelector('.tlf-27');
const dots = [...section.querySelectorAll('.tlf-27-dots span')];
const value = section.querySelector('.tlf-27-value');
const status = section.querySelector('.tlf-27-status');
const MAX = dots.length;
let pin = '';

function render() {
  dots.forEach((d, i) => {
    if (i < pin.length) d.setAttribute('data-on', '');
    else d.removeAttribute('data-on');
  });
  value.value = pin;
  status.textContent = pin.length + ' of ' + MAX + ' digits entered';
}

section.querySelectorAll('.tlf-27-key').forEach((key) => {
  key.addEventListener('click', () => {
    if (key.hasAttribute('data-del')) pin = pin.slice(0, -1);
    else if (pin.length < MAX) pin += key.dataset.d;
    render();
    if (pin.length === MAX) setTimeout(() => { pin = ''; render(); }, 600);
  });
});

render();

Setup — do this once per project

Skip this if your project already runs Tailwind v4.

1Load Tailwind
<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.

2Add the stylesheetRequiredthe @theme block is what makes the custom utilities exist
@import "tailwindcss";

@theme {
  --color-tlf27-stage: #12151d;
  --color-tlf27-stage: oklch(0.18 0.012 265);
  --color-tlf27-card: #1a1f2b;
  --color-tlf27-card: oklch(0.23 0.014 265);
  --color-tlf27-edge: #262c3a;
  --color-tlf27-edge: oklch(0.28 0.016 265);
  --color-tlf27-key: #212635;
  --color-tlf27-key: oklch(0.26 0.016 265);
  --color-tlf27-keyhover: #2b3244;
  --color-tlf27-keyhover: oklch(0.31 0.018 265);
  --color-tlf27-keyedge: #333b4d;
  --color-tlf27-keyedge: oklch(0.33 0.018 265);
  --color-tlf27-dot: #4a5468;
  --color-tlf27-dot: oklch(0.43 0.02 265);
  --color-tlf27-ink: #e8ebf3;
  --color-tlf27-ink: oklch(0.93 0.008 265);
  --color-tlf27-mut: #a8b0c4;
  --color-tlf27-mut: oklch(0.75 0.018 265);
  --color-tlf27-acc: #7b90e8;
  --color-tlf27-acc: oklch(0.68 0.15 265);
  --color-tlf27-press: #2f3a5e;
  --color-tlf27-press: oklch(0.68 0.15 265 / 0.25);
  --color-tlf27-ring: #a3b2ef;
  --color-tlf27-ring: oklch(0.68 0.15 265 / 0.6);
  --font-tlf27-sans: 'Figtree', system-ui, sans-serif;
}

.tlf-27 {
  width: 100%;
  min-height: 100svh;
  color-scheme: dark;
}
3Fonts used by this demooptional — skip if you have your own type stack
<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=Figtree:wght@400;500;600;700&display=swap">

What changed in the port

Which parts became utilities, which stayed CSS, and why.

  • data-[on]: instead of a .is-on class is a small but real improvement: the attribute name lives in the handler and in the variant, both visible, with no stylesheet in between to drift.
  • scale-115 exists in v4's scale (and arbitrary values cover the rest), so the dot's 1.15× pop needs no bracket value.
  • An sr-only live region announcing "2 of 4 digits entered" was added: the original conveyed PIN progress entirely visually, which on a kiosk is the one place that matters most.
  • UI change from the CSS original: Figtree replaces the system stack, the delete key's ink lifted from #98a0b4 to oklch(0.75 0.018 265), key borders brightened so the pad is visible in daylight on a kiosk screen, and the auto-clear delay went 500ms → 600ms so the fourth dot is seen before it resets.
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 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 Numeric PIN Pad Login for Kiosks and POS
Source: https://codefronts.com/components/tailwind-css-login-forms/tailwind-numeric-pin-pad-login-kiosk-pos/

A numeric PIN entry for kiosks, POS terminals and mobile web: a rigid 3&times;4 keypad of big round keys feeding a row of masked dots, with a press that scales and a delete key. The port keeps the script to state and moves the filled-dot look to data-[on]:, so the handler sets an attribute instead of toggling a class name it has to remember.
## HTML
```html
<section class="tlf-27 w-full min-h-svh grid place-items-center bg-tlf27-stage font-tlf27-sans p-6">
  <form novalidate class="tlf-27-form w-[min(340px,100%)] p-8 text-center bg-tlf27-card border border-tlf27-edge rounded-[22px] shadow-[0_30px_70px_-44px_black]">
    <h1 class="mb-5 text-[19px] font-semibold text-tlf27-ink">Enter your PIN</h1>
    <div aria-hidden="true" class="tlf-27-dots flex justify-center gap-4 mb-7">
      <span class="size-3.5 rounded-full border-2 border-tlf27-dot bg-transparent transition-[background-color,border-color,scale] duration-150 data-[on]:bg-tlf27-acc data-[on]:border-tlf27-acc data-[on]:scale-115 motion-reduce:transition-colors"></span>
      <span class="size-3.5 rounded-full border-2 border-tlf27-dot bg-transparent transition-[background-color,border-color,scale] duration-150 data-[on]:bg-tlf27-acc data-[on]:border-tlf27-acc data-[on]:scale-115 motion-reduce:transition-colors"></span>
      <span class="size-3.5 rounded-full border-2 border-tlf27-dot bg-transparent transition-[background-color,border-color,scale] duration-150 data-[on]:bg-tlf27-acc data-[on]:border-tlf27-acc data-[on]:scale-115 motion-reduce:transition-colors"></span>
      <span class="size-3.5 rounded-full border-2 border-tlf27-dot bg-transparent transition-[background-color,border-color,scale] duration-150 data-[on]:bg-tlf27-acc data-[on]:border-tlf27-acc data-[on]:scale-115 motion-reduce:transition-colors"></span>
    </div>
    <input class="tlf-27-value" type="hidden" name="pin">
    <p class="tlf-27-status sr-only" role="status" aria-live="polite">0 of 4 digits entered</p>
    <div role="group" aria-label="PIN keypad" class="grid grid-cols-3 gap-3.5 justify-items-center">
      <button type="button" data-d="1" class="tlf-27-key size-16 rounded-full cursor-pointer bg-tlf27-key border-[1.5px] border-tlf27-keyedge text-tlf27-ink text-2xl font-medium transition-[background-color,border-color,scale] duration-100 hover:bg-tlf27-keyhover hover:border-tlf27-dot active:scale-90 active:bg-tlf27-press active:border-tlf27-acc focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tlf27-ring motion-reduce:transition-colors motion-reduce:active:scale-100">1</button>
      <button type="button" data-d="2" class="tlf-27-key size-16 rounded-full cursor-pointer bg-tlf27-key border-[1.5px] border-tlf27-keyedge text-tlf27-ink text-2xl font-medium transition-[background-color,border-color,scale] duration-100 hover:bg-tlf27-keyhover hover:border-tlf27-dot active:scale-90 active:bg-tlf27-press active:border-tlf27-acc focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tlf27-ring motion-reduce:transition-colors motion-reduce:active:scale-100">2</button>
      <button type="button" data-d="3" class="tlf-27-key size-16 rounded-full cursor-pointer bg-tlf27-key border-[1.5px] border-tlf27-keyedge text-tlf27-ink text-2xl font-medium transition-[background-color,border-color,scale] duration-100 hover:bg-tlf27-keyhover hover:border-tlf27-dot active:scale-90 active:bg-tlf27-press active:border-tlf27-acc focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tlf27-ring motion-reduce:transition-colors motion-reduce:active:scale-100">3</button>
      <button type="button" data-d="4" class="tlf-27-key size-16 rounded-full cursor-pointer bg-tlf27-key border-[1.5px] border-tlf27-keyedge text-tlf27-ink text-2xl font-medium transition-[background-color,border-color,scale] duration-100 hover:bg-tlf27-keyhover hover:border-tlf27-dot active:scale-90 active:bg-tlf27-press active:border-tlf27-acc focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tlf27-ring motion-reduce:transition-colors motion-reduce:active:scale-100">4</button>
      <button type="button" data-d="5" class="tlf-27-key size-16 rounded-full cursor-pointer bg-tlf27-key border-[1.5px] border-tlf27-keyedge text-tlf27-ink text-2xl font-medium transition-[background-color,border-color,scale] duration-100 hover:bg-tlf27-keyhover hover:border-tlf27-dot active:scale-90 active:bg-tlf27-press active:border-tlf27-acc focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tlf27-ring motion-reduce:transition-colors motion-reduce:active:scale-100">5</button>
      <button type="button" data-d="6" class="tlf-27-key size-16 rounded-full cursor-pointer bg-tlf27-key border-[1.5px] border-tlf27-keyedge text-tlf27-ink text-2xl font-medium transition-[background-color,border-color,scale] duration-100 hover:bg-tlf27-keyhover hover:border-tlf27-dot active:scale-90 active:bg-tlf27-press active:border-tlf27-acc focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tlf27-ring motion-reduce:transition-colors motion-reduce:active:scale-100">6</button>
      <button type="button" data-d="7" class="tlf-27-key size-16 rounded-full cursor-pointer bg-tlf27-key border-[1.5px] border-tlf27-keyedge text-tlf27-ink text-2xl font-medium transition-[background-color,border-color,scale] duration-100 hover:bg-tlf27-keyhover hover:border-tlf27-dot active:scale-90 active:bg-tlf27-press active:border-tlf27-acc focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tlf27-ring motion-reduce:transition-colors motion-reduce:active:scale-100">7</button>
      <button type="button" data-d="8" class="tlf-27-key size-16 rounded-full cursor-pointer bg-tlf27-key border-[1.5px] border-tlf27-keyedge text-tlf27-ink text-2xl font-medium transition-[background-color,border-color,scale] duration-100 hover:bg-tlf27-keyhover hover:border-tlf27-dot active:scale-90 active:bg-tlf27-press active:border-tlf27-acc focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tlf27-ring motion-reduce:transition-colors motion-reduce:active:scale-100">8</button>
      <button type="button" data-d="9" class="tlf-27-key size-16 rounded-full cursor-pointer bg-tlf27-key border-[1.5px] border-tlf27-keyedge text-tlf27-ink text-2xl font-medium transition-[background-color,border-color,scale] duration-100 hover:bg-tlf27-keyhover hover:border-tlf27-dot active:scale-90 active:bg-tlf27-press active:border-tlf27-acc focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tlf27-ring motion-reduce:transition-colors motion-reduce:active:scale-100">9</button>
      <span aria-hidden="true" class="size-16"></span>
      <button type="button" data-d="0" class="tlf-27-key size-16 rounded-full cursor-pointer bg-tlf27-key border-[1.5px] border-tlf27-keyedge text-tlf27-ink text-2xl font-medium transition-[background-color,border-color,scale] duration-100 hover:bg-tlf27-keyhover hover:border-tlf27-dot active:scale-90 active:bg-tlf27-press active:border-tlf27-acc focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tlf27-ring motion-reduce:transition-colors motion-reduce:active:scale-100">0</button>
      <button type="button" data-del aria-label="Delete last digit" class="tlf-27-key size-16 rounded-full cursor-pointer bg-tlf27-key border-[1.5px] border-tlf27-keyedge text-tlf27-mut text-xl transition-[background-color,border-color,scale] duration-100 hover:bg-tlf27-keyhover hover:border-tlf27-dot active:scale-90 active:bg-tlf27-press active:border-tlf27-acc focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tlf27-ring motion-reduce:transition-colors motion-reduce:active:scale-100">&#9003;</button>
    </div>
  </form>
</section>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-tlf27-stage: #12151d;
  --color-tlf27-stage: oklch(0.18 0.012 265);
  --color-tlf27-card: #1a1f2b;
  --color-tlf27-card: oklch(0.23 0.014 265);
  --color-tlf27-edge: #262c3a;
  --color-tlf27-edge: oklch(0.28 0.016 265);
  --color-tlf27-key: #212635;
  --color-tlf27-key: oklch(0.26 0.016 265);
  --color-tlf27-keyhover: #2b3244;
  --color-tlf27-keyhover: oklch(0.31 0.018 265);
  --color-tlf27-keyedge: #333b4d;
  --color-tlf27-keyedge: oklch(0.33 0.018 265);
  --color-tlf27-dot: #4a5468;
  --color-tlf27-dot: oklch(0.43 0.02 265);
  --color-tlf27-ink: #e8ebf3;
  --color-tlf27-ink: oklch(0.93 0.008 265);
  --color-tlf27-mut: #a8b0c4;
  --color-tlf27-mut: oklch(0.75 0.018 265);
  --color-tlf27-acc: #7b90e8;
  --color-tlf27-acc: oklch(0.68 0.15 265);
  --color-tlf27-press: #2f3a5e;
  --color-tlf27-press: oklch(0.68 0.15 265 / 0.25);
  --color-tlf27-ring: #a3b2ef;
  --color-tlf27-ring: oklch(0.68 0.15 265 / 0.6);
  --font-tlf27-sans: 'Figtree', system-ui, sans-serif;
}

.tlf-27 {
  width: 100%;
  min-height: 100svh;
  color-scheme: dark;
}
```

## JavaScript
```js
const section = document.querySelector('.tlf-27');
const dots = [...section.querySelectorAll('.tlf-27-dots span')];
const value = section.querySelector('.tlf-27-value');
const status = section.querySelector('.tlf-27-status');
const MAX = dots.length;
let pin = '';

function render() {
  dots.forEach((d, i) => {
    if (i < pin.length) d.setAttribute('data-on', '');
    else d.removeAttribute('data-on');
  });
  value.value = pin;
  status.textContent = pin.length + ' of ' + MAX + ' digits entered';
}

section.querySelectorAll('.tlf-27-key').forEach((key) => {
  key.addEventListener('click', () => {
    if (key.hasAttribute('data-del')) pin = pin.slice(0, -1);
    else if (pin.length < MAX) pin += key.dataset.d;
    render();
    if (pin.length === MAX) setTimeout(() => { pin = ''; render(); }, 600);
  });
});

render();
```

How this works

The grid is the design. Three equal columns, twelve cells, one of them deliberately empty. Nothing is positioned; the keypad cannot drift out of alignment because the grid does not allow it.

Dots read an attribute. render() sets or removes data-on per dot and data-[on]: does the rest — no class-name string shared between the script and a stylesheet, which is the small bug this port removes.

Real buttons, real labels. Every key is a <button type="button">, so the pad is tabbable and Enter/Space work; the delete key carries an aria-label because ⌫ announces as nothing useful.

Make it yours

  • PIN length: add dots, and the script reads the count from the DOM.
  • Key size: size-16 on the keys and the spacer together.
  • Accent: --color-tlf27-acc drives filled dots, the press wash and the rings.
  • Fill the empty cell with a biometric or an SOS key — the grid already reserves it.

Gotchas — read before shipping

  • Keys must be buttons. A <div> keypad needs tabindex, keydown and role work to reach parity, and kiosks are exactly where that gets skipped.
  • Always ship a delete key. Forcing a full re-entry on one mistyped digit is the fastest way to get a PIN pad replaced.
  • Keep the spacer element. Removing it reflows 0 and &#9003; into the wrong cells, which users notice instantly because phone keypads are muscle memory.

Browser support

ChromeSafariFirefoxEdge
111+16.4+128+111+

Grid, data-attribute variants and transforms are universally supported; entry needs the handler.

Techniques used in this demo

Utilitiesfirst-class utility or variant

HTML & JSno Tailwind involved

Search CodeFronts

Loading…