Unlock Dialog
A copy-paste React unlock dialog for triggering an access-code prompt from a custom button or menu item. Zero dependencies.
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.
Minimal setup
const { state, error, submit } = useKnockCodes({ expectedHash: hash });
const [code, setCode] = useState("");
<UnlockDialog
open={state !== "unlocked"}
value={code}
onChange={setCode}
onSubmit={() => submit(code)}
submitting={state === "submitting"}
error={error}
/>
This page is protected
- 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;
autoFocus?: boolean;
/** 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, autoFocus = true, 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={autoFocus}
/>
{footer}
</div>
</div>
);
}
Add this block to your project
Recommended
npx shadcn@latest add @knock-codes/unlock-dialogAlso installs
- Knock Codes Core
- Knock Codes Types
- PIN Input
These install together as one atomic unit — even a presentational or read-only piece needs the full verification stack (hook, types, core) behind it to actually run.
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
Other ways
GitHub shorthand
npx shadcn@latest add trivedi-vatsal/knock-codes/unlock-dialogCopy 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.
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.
Need a hash? Use the hash generator on Getting Started — computed locally, never sent anywhere.
The honest version
Knock Codes stops casual visitors, search engines, and forwarded links. Local mode does not stop anyone who opens DevTools — the hash ships in your client bundle by design. Server mode (swap one prop) hides the hash from the client; children you already bundled are still in the JavaScript, and a forged session works unless you wire validateSession. A velvet rope, with an optional real lock. Never marketed as more than that.
Used in these templates
Want the whole screen instead of assembling it yourself? These templates already build on this block.
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 ModalA copy-paste React access screen for a paywall-style preview — content stays mounted and blurred behind a modal prompt. Zero dependencies.
- PIN InputA copy-paste React access-code field for building a custom gate on useKnockCodes — masked field or segmented boxes, paste support, accessible errors. Zero dependencies.
- Knock CodesA copy-paste React access screen for gating a whole page or app root — local hash or server verification, one prop swap between them. Zero dependencies.