→ Modal & Overlay
Unlock Dialog
A non-dismissable modal access-code prompt — no backdrop click, no Escape-to-close.
Best used for Triggering an unlock prompt from a custom button or menu item instead of gating content directly.
The modal shell <ProtectedModal> renders internally, exposed on its own for cases where you want to
trigger an unlock prompt manually rather than gating a whole content region.
- components
- knock-codes
- core
- react
"use client";
import { useId, type ReactNode } from "react";
import { PinInput } from "./PinInput.tsx";
import { DEFAULT_LABELS, type KnockCodesError, type KnockCodesLabels } from "./types.ts";
export interface UnlockDialogProps {
open: boolean;
value: string;
onChange: (value: string) => void;
onSubmit: () => void;
submitting: boolean;
error: KnockCodesError | null;
labels?: KnockCodesLabels;
/** Extra content under the form, e.g. help text or a support link. */
footer?: ReactNode;
}
/**
* The modal shell `<ProtectedModal>` renders — also usable standalone when
* you want to trigger an unlock prompt from your own button or menu item
* instead of gating content directly. Deliberately not dismissable (no
* backdrop click, no Escape-to-close): the point of a gate is that closing
* the dialog can't be how you get past it.
*/
export function UnlockDialog({ open, value, onChange, onSubmit, submitting, error, labels, footer }: UnlockDialogProps) {
const merged = { ...DEFAULT_LABELS, ...labels };
const headingId = useId();
if (!open) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">
<div
role="dialog"
aria-modal="true"
aria-labelledby={headingId}
className="w-full max-w-sm space-y-4 rounded-lg border border-gray-200 bg-white p-6 shadow-lg outline-none dark:border-gray-800 dark:bg-gray-950"
>
<h2 id={headingId} className="text-lg font-semibold text-gray-900 dark:text-gray-50">
{merged.heading}
</h2>
<PinInput
value={value}
onChange={onChange}
onSubmit={onSubmit}
submitting={submitting}
error={error}
labels={labels}
autoFocus
/>
{footer}
</div>
</div>
);
}
Add this block to your project
CLI
npx shadcn@latest add http://localhost:3000/r/react/unlock-dialog.jsonRunning this site somewhere other than localhost? Set NEXT_PUBLIC_SITE_URL and this command switches to that host automatically.
Also installs
- Knock Codes Core
- Knock Codes Types
- PIN Input
Files created (7)
- components/knock-codes/core/hash.ts
- components/knock-codes/core/verify.ts
- components/knock-codes/core/session.ts
- components/knock-codes/core/storage.ts
- components/knock-codes/react/types.ts
- components/knock-codes/react/PinInput.tsx
- components/knock-codes/react/UnlockDialog.tsx
No CLI? Copy the files by hand
- Open the Code tab in the preview above.
- Create each path listed below in your project and paste its contents in.
- Do the same for anything listed under “Also installs”, if present.
→ Props
API reference
| Prop | Type | Default | Description |
|---|---|---|---|
| open * | boolean | — | Whether the dialog is visible. |
| value * | string | — | Current input value (controlled). |
| onChange * | (value: string) => void | — | Called on every keystroke/paste. |
| onSubmit * | () => void | — | Called on Enter or the submit button. |
| submitting * | boolean | — | Disables the field and shows the busy label. |
| error * | KnockCodesError | null | — | Drives which error message renders. |
| labels | KnockCodesLabels | — | Overrides for every user-facing string. |
| footer | ReactNode | — | Extra content under the form. |
Accessibility
role="dialog" + aria-modal="true" + aria-labelledby pointing at the heading. Focus moves into the panel on open. Deliberately not dismissable via Escape or backdrop click — a gate you can dismiss without unlocking isn't a gate, so this omission is intentional, not a missing feature.
Customization
Composable standalone — trigger it from your own button or menu item instead of gating content directly, by controlling `open` yourself. Pass `footer` for help text or a support link under the form.
→ Compose with
Blocks that pair well with this one
These combine naturally with this block, whether as a shared shell, a shared session, or a common fallback.
- Protected ModalContent stays mounted and blurred behind a modal Unlock Dialog instead of being replaced.
- PIN InputMasked access-code input (PIN or passphrase) with paste support and accessible loading/error states.
- Knock CodesThe main gate — renders children when unlocked, otherwise an access-code prompt.