→ Modal & Overlay
Protected Modal
Content stays mounted and blurred behind a modal Unlock Dialog instead of being replaced.
Best used for A paywall-style preview where the blurred content itself is part of the pitch.
Content and gate coexist in the DOM at all times — only the modal's visibility changes, which keeps layout shifts to zero on unlock.
- components
- knock-codes
- core
- react
"use client";
import { useState, type ReactNode } from "react";
import { useKnockCodes } from "./useKnockCodes.ts";
import { UnlockDialog } from "./UnlockDialog.tsx";
import type { KnockCodesConfig, KnockCodesLabels } from "./types.ts";
import { cx } from "./cx.ts";
export interface ProtectedModalProps extends KnockCodesConfig {
children: ReactNode;
labels?: KnockCodesLabels;
className?: string;
}
/**
* Content stays mounted (blurred and inert) behind a modal `<UnlockDialog>`
* instead of being replaced outright — useful when the protected content's
* layout should stay stable underneath the prompt rather than collapsing to
* a bare PIN screen.
*/
export function ProtectedModal({ children, labels, className, ...config }: ProtectedModalProps) {
const { state, error, submit } = useKnockCodes(config);
const [code, setCode] = useState("");
const locked = state !== "unlocked";
const handleSubmit = async () => {
await submit(code);
setCode("");
};
return (
<div className={cx("relative", className)}>
<div aria-hidden={locked} className={cx(locked && "pointer-events-none select-none blur-sm")}>
{children}
</div>
<UnlockDialog
open={locked}
value={code}
onChange={setCode}
onSubmit={handleSubmit}
submitting={state === "submitting"}
error={error}
labels={labels}
/>
</div>
);
}
Add this block to your project
CLI
npx shadcn@latest add http://localhost:3000/r/react/protected-modal.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
- useKnockCodes
- PIN Input
- Unlock Dialog
Files created (9)
- 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/useKnockCodes.ts
- components/knock-codes/react/PinInput.tsx
- components/knock-codes/react/UnlockDialog.tsx
- components/knock-codes/react/ProtectedModal.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 |
|---|---|---|---|
| expectedHash | string | — | SHA-256 hex hash to verify against, for local mode. |
| verify | VerifyFn | — | Custom async verification function, for server mode. |
| children * | ReactNode | — | Stays mounted, blurred while locked. |
| labels | KnockCodesLabels | — | Overrides for every user-facing string. |
| className | string | — | Extra classes on the outer relative container. |
Accessibility
The blurred content is marked aria-hidden and pointer-events-none while locked, so screen readers and keyboard tabbing skip straight past it to the dialog — it's visually present but not reachable, which is the correct behavior for inert background content.
Customization
Best for cases where the protected content's layout should stay stable underneath the prompt — a preview behind a paywall-style modal — rather than collapsing to a bare access-code screen the way Knock Codes does.
→ 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.