Protected Modal
A copy-paste React access screen for a paywall-style preview — content stays mounted and blurred behind a modal prompt. Zero dependencies.
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.
Minimal setup
<ProtectedModal expectedHash={process.env.NEXT_PUBLIC_KNOCK_HASH}>
<Dashboard />
</ProtectedModal>
- components
- knock-codes
- core
- react
"use client";
import { useState, type ReactNode } from "react";
import { UnlockDialog } from "./UnlockDialog.tsx";
import { GateSession } from "./KnockCodesContext.tsx";
import type { KnockCodesConfig, KnockCodesLabels, UseKnockCodesResult } 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) {
return (
<GateSession config={config}>
{(session) => (
<ProtectedModalView labels={labels} className={className} session={session}>
{children}
</ProtectedModalView>
)}
</GateSession>
);
}
function ProtectedModalView({
children,
labels,
className,
session,
}: Pick<ProtectedModalProps, "children" | "labels" | "className"> & { session: UseKnockCodesResult }) {
const { ready, state, error, submit } = session;
const [code, setCode] = useState("");
if (!ready) return null;
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
Recommended
npx shadcn@latest add @knock-codes/protected-modalAlso installs
- Knock Codes Core
- Knock Codes Types
- useKnockCodes
- PIN Input
- Unlock Dialog
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 (10)
- 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/KnockCodesContext.tsx
- components/knock-codes/react/PinInput.tsx
- components/knock-codes/react/UnlockDialog.tsx
- components/knock-codes/react/ProtectedModal.tsx
Other ways
GitHub shorthand
npx shadcn@latest add trivedi-vatsal/knock-codes/protected-modalCopy 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 |
|---|---|---|---|
| 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. |
Exactly one of expectedHash or verify is required.
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.
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.
- Unlock DialogA copy-paste React unlock dialog for triggering an access-code prompt from a custom button or menu item. Zero dependencies.
- Protected CardA copy-paste React access screen for gating one card in a dashboard grid — blurred preview, inline unlock. 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.