shadcn.io is not affiliated with official shadcn/ui
Two-Factor Posting Comments Block for Radix UI
A budget approvals thread whose Post an approval dialog asks for a six-digit code first, shown open over a drawn thread, with three attempts, a lock and Reset, a Resend that sends nothing and a Verified mark on the new approval.
Installation
Overview
Make sure it is really them before a comment counts, with this verified-posting dialog for React with Radix UI. It opens from the approvals page of Keyholt, a tool for finance teams: the header holds an outline Post an approval Button, and the thread of three verified approvals on the Q4 travel budget is drawn under a soft scrim inside a hairline frame with a plus mark on each corner. The Dialog, titled Verify it's you, holds a Textarea for the approval and a six-slot Input OTP. The demo code, 123456, is printed under the slots with the words No code is sent. A wrong code marks the slots and says how many attempts remain; the third locks the slots until Reset, and Resend code only says where the demo code is. The one Shine Button, Verify and post, adds the approval to the thread with a Verified mark. For preview the dialog is drawn open in place when the page loads; the header button opens the real Dialog, with the same text, the same code and the same attempts. For approvals, sign-offs and any comment that has to be traced to a person.
FAQ
Why is the dialog already open?
So you can see it without a click. On load the dialog is a copy drawn in place over the thread: a native dialog element that isn't modal, so nothing is trapped, hidden or focused, and the page around the preview keeps working. The Post an approval button opens the real Dialog, modal, with its focus trap, Escape and focus return to the button. The copy stays closed until a reload. Both show the same ApprovalFields component and share the block's state, so what you typed in the copy is 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 does the code check work?
CODE is the only code the demo accepts and it is printed on screen, because nothing is sent to anyone. Digits go in one at a time or as a paste, and anything that isn't a digit is dropped. Pressing Verify and post checks the words first (at least eight characters), then that all six digits are in, then the code. A wrong code clears the slots, marks them and says "Attempts remaining: 2."; after ATTEMPTS misses the slots lock and Reset gives the three attempts back. The slots' value is one string with a maximum length of six.
What stays local, and how do I make it real?
Everything. No code is generated or sent, the thread is a list in the page, and the status line says Approval posted with a Verified mark and nothing was sent. In your app send the code from your server (an authenticator app, an SMS or an email), check it there, count the attempts there too and only then create the comment, storing who verified it and when. The Verified mark should come from that record, never from the browser.
Where does focus go?
The real dialog is named by its title and described by its sentence. Focus starts in the approval box, moves to the slots when the words are fine but the code is missing or wrong, and returns to the Post an approval button when the dialog closes by Escape, Cancel, the close button or a successful post. The trigger is the Post an approval button wrapped in DialogTrigger with asChild, and the dialog is controlled with open and onOpenChange.
Is it accessible?
The slots are named by Six-digit code and described by the line under them, which holds either the hint or the problem, so it is always in place and nothing moves when a miss appears. Problems are written in words with an icon, never colour alone, and each is also read out by a polite status message. The drawn thread is hidden from assistive technology, so the approvals are listed in text for screen readers beside it.