shadcn.io is not affiliated with official shadcn/ui
PIN Keypad Dialog Block for React
A mobile wallet's transfer review with its PIN dialog shown open: four masked slots over a keypad of twelve keys, typed digits and keypad presses writing the same value, and a fourth digit that approves.
Installation
Overview
Approve a payment with a PIN the way a phone does, with this keypad dialog for React with Base UI. It opens from the transfer review of Purseline, a mobile wallet: the page's header holds an outline Approve Button and the review itself, a phone with an amount, a recipient and the fees, is drawn under a soft scrim inside a hairline frame with a plus mark on each corner. The dialog centres a lock, a title and four masked slots from Input OTP above a keypad of twelve Buttons: 1 to 9, Clear, 0 and Backspace, each 48 px. A digit typed on the keyboard fills the same slots, so the keypad and the keyboard are one value, and the fourth digit approves by itself, so there is no filled button to press. A wrong PIN marks the slots, nudges them once and says how many tries are left; after the third the keys lock. For preview the dialog is drawn open in place when the page loads; the Approve button opens the real Dialog. For wallets, banking, payments and any step that a short code unlocks.
FAQ
Why is the dialog already open, and what do I keep?
So you can see it without a click. On load the dialog is a copy drawn in place over the page: a native dialog element that isn't modal, so nothing is trapped, hidden or focused. The Approve button opens the real Dialog, modal, with its focus trap, Escape and focus return, and the copy stays closed until a reload. Both show the same PinFields component and share the block's state, so digits keyed in the copy are there in the real one. In your app keep the Dialog and drop the copy: the "in-place" state, the scrim and the dialog element inside the frame.
How do the keypad and the keyboard share one value?
There is one string of up to four digits in the block's state. A key press appends to it, Backspace removes the last digit, Clear empties it, and typing or pasting into the slots replaces it, with anything that isn't a digit dropped. When the fourth digit lands the PIN is checked at once. There is no submit button on purpose: a keypad PIN is finished by its last digit, and Cancel stays for the way out.
Are the digits really hidden?
They are masked on every target and never read out: the line under the slots, which a screen reader announces when it appears, speaks counts only ("Wrong PIN. 2 tries left.") and never a digit. The slots are drawn by the input library, so the block draws a dot for each digit itself and hides the characters; the field behind them is a password input. The masking is a courtesy on a screen anyone might look at, and the demo PIN is printed in the hint on purpose.
What stays local, and how do I wire it for real?
Nothing is sent or checked anywhere. The right PIN, 2468, only turns the page's transfer row into "Sent to Tove Haldane: approved with PIN (demo)", and the header's button becomes Review again. In your app send the PIN to your server in settle, let the server count the tries and lock the account (the three here are only a demo), and move money only on its answer.
Is it accessible from the keyboard?
Yes. The real dialog is named by its title and described by its sentence, focus starts in the first slot so digits can be typed at once, and Escape, Cancel and the close button return focus to the Approve button. The keypad is a group named Keypad of real buttons, each at least 48 px, and Backspace has a name for a screen reader. The trigger is passed to DialogTrigger through render, and the dialog is controlled with open and onOpenChange. When the keys lock, focus moves to Cancel.
How does it look on a phone?
The copy shown on load docks to the foot of the page as a bottom sheet, with Cancel pinned under a body that scrolls. The real dialog keeps the Dialog's own phone layout, a sheet stuck to the bottom of the screen, and the keypad fills its width. The nudge is dropped under reduced motion, and the line says the same thing.
Related
- Pro
Email Verification Dialog
Account and securityA newsletter tool's profile page with its email verification shown open: a six-slot code that checks itself when full, three tries, a resend held for 30 seconds by a timer, a way to use a different address, and a success state.
- Pro
One-Time Code Input Form
Numbers and pickersA six-slot confirmation code in two groups of three with its behaviours in view: paste fills all six and checks at once, a wrong code is marked, counted and cleared, and a resend button waits 30 seconds.
- Pro
Quick Transfer Dialog
Payments and plansA payments app's account home with its send-money dialog shown open: five recent people in a scrolling row, an amount with quick chips, a note, the fee and balance after, and a review step before Send.
- Pro
Password Re-Entry Confirm Dialog
Destructive confirmsA hosting dashboard with an alert dialog shown open that asks for the password again before a project is deleted: a lock tile, one field, a Caps Lock note, a count of tries and a destructive confirm.
- Pro
Two-Step Setup Dialog
Account and securityA cloud editor's security page with a three-step dialog shown open: a QR code and its key, eight backup codes to copy or download, and a six-digit check, ending in a note that two-step sign-in is on.
- Pro
Add Profile Dialog
Account and securityA newsletter's writers page with its byline composer shown open: a name, what they write, a 120 character bio, a mark from six drawn glyphs or initials and a switch, beside a card that shows the byline as it will print.