Knock Codes
A 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.
Best used for The default choice for gating a whole page or app root in local mode.
The component every other block in this library either wraps or composes on. Local SHA-256 verification by
default; swap expectedHash for verify to upgrade to server-mode protection with no other code changes.
Inside a <KnockCodesProvider>, this joins the shared session so a <LogoutButton> in the same tab actually
relocks the gate. Nothing is rendered until storage has been read (ready), so a returning visitor doesn't
flash the PIN UI.
Outer Positioning Variants
- Full Page (
variant="page"): Centers the gate vertically and horizontally across the viewport (min-h-[100dvh]). Replaces the thinStandaloneGatewrapper. - Inline Shell (
variant="inline"): Flows naturally inside your existing layout (e.g. inside a sidebar, modal, or section card). Replaces the thinEmbeddedGatewrapper.
Minimal setup
<KnockCodes expectedHash={process.env.NEXT_PUBLIC_KNOCK_HASH}>
<YourApp />
</KnockCodes>
- components
- knock-codes
- core
- react
"use client";
import { useState, type ReactNode } from "react";
import { PinInput } from "./PinInput.tsx";
import { GateWrapper, type GateWrapperVariant } from "./GateWrapper.tsx";
import { GateSession } from "./KnockCodesContext.tsx";
import { DEFAULT_LABELS, type KnockCodesConfig, type KnockCodesLabels, type UseKnockCodesResult } from "./types.ts";
export interface KnockCodesProps extends KnockCodesConfig {
children: ReactNode;
labels?: KnockCodesLabels;
/** Visual shell around the PIN prompt. @default "page" */
variant?: GateWrapperVariant;
autoFocus?: boolean;
className?: string;
}
function KnockCodesView({
children,
labels,
variant = "page",
autoFocus = true,
className,
verify,
ready,
state,
error,
submit,
}: Pick<KnockCodesProps, "children" | "labels" | "variant" | "autoFocus" | "className" | "verify"> &
Pick<UseKnockCodesResult, "ready" | "state" | "error" | "submit">) {
const [code, setCode] = useState("");
if (!ready) return null;
if (state === "unlocked") return <>{children}</>;
const merged = { ...DEFAULT_LABELS, ...labels };
const modeLabel = verify ? "SERVER VERIFY" : "LOCAL HASH";
const handleSubmit = async () => {
await submit(code);
setCode(""); // clearing after every attempt is an implementation detail, not a contract
};
return (
<GateWrapper variant={variant} className={className}>
<div
style={{ fontFamily: "var(--ag-font, inherit)" }}
className="w-full max-w-sm rounded-[var(--ag-radius,0.75rem)] border border-[var(--ag-border,#e5e7eb)] bg-[var(--ag-card,#ffffff)] p-7 shadow-sm dark:border-[var(--ag-border-dark,#1f2937)] dark:bg-[var(--ag-card-dark,#030712)]"
>
<div className="mb-5 flex items-center justify-between gap-2">
<span className="inline-flex items-center gap-1.5 text-[10px] font-medium tracking-wider text-red-600 uppercase dark:text-red-400">
<span aria-hidden="true" className="h-1.5 w-1.5 shrink-0 rounded-full bg-red-600 dark:bg-red-400" />
Locked
</span>
<span className="rounded-full border border-gray-200 px-2 py-0.5 font-mono text-[10px] font-medium tracking-wider text-gray-500 uppercase dark:border-gray-800 dark:text-gray-400">
{modeLabel}
</span>
</div>
<div className="mb-5 space-y-1">
<h1 className="text-lg font-semibold text-gray-900 dark:text-gray-50">{merged.heading}</h1>
{merged.subcopy && <p className="text-sm text-gray-500 dark:text-gray-400">{merged.subcopy}</p>}
</div>
<PinInput
value={code}
onChange={setCode}
onSubmit={handleSubmit}
submitting={state === "submitting"}
error={error}
labels={labels}
autoFocus={autoFocus}
/>
</div>
</GateWrapper>
);
}
/**
* Wrapper component. Renders `children` only when a valid session exists;
* otherwise renders the PIN entry UI. There is no separate "mount loading"
* state — nothing is shown until storage has been read (`ready`), then the
* PIN entry UI covers the locked case.
*
* Inside a `<KnockCodesProvider>`, this joins the shared session instead of
* creating a second one. Put `expectedHash` / `verify` on the provider.
*/
export function KnockCodes({ children, labels, variant = "page", autoFocus = true, className, ...config }: KnockCodesProps) {
return (
<GateSession config={config}>
{(session) => (
<KnockCodesView
labels={labels}
variant={variant}
autoFocus={autoFocus}
className={className}
verify={config.verify}
ready={session.ready}
state={session.state}
error={session.error}
submit={session.submit}
>
{children}
</KnockCodesView>
)}
</GateSession>
);
}
Add this block to your project
Recommended
npx shadcn@latest add @knock-codes/knock-codesAlso installs
- Knock Codes Core
- Knock Codes Types
- useKnockCodes
- cx (classname helper)
- Gate Wrapper
- 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 (11)
- 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/cx.ts
- components/knock-codes/react/GateWrapper.tsx
- components/knock-codes/react/PinInput.tsx
- components/knock-codes/react/KnockCodes.tsx
Other ways
GitHub shorthand
npx shadcn@latest add trivedi-vatsal/knock-codes/knock-codesCopy 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. Mutually exclusive with verify. |
| verify | VerifyFn | — | Custom async verification function, for server mode. Mutually exclusive with expectedHash. |
| storage | "localStorage" | "sessionStorage" | "memory" | "localStorage" | Where the unlocked session persists. |
| storageKey | string | "knock-codes:session" | Storage key the session is written under. |
| timeout | number | 1800000 | Session lifetime in milliseconds (30 minutes). |
| activityTracking | boolean | false | Sliding-timeout model — interaction rewrites expiry instead of a fixed TTL. |
| validateSession | (session: KnockCodesSession) => boolean | Promise<boolean> | — | Called when a session is read from storage. Return false or throw to reject it (server-mode token check). Omitted: expiry is the only restore check. |
| children * | ReactNode | — | Rendered once unlocked. |
| labels | KnockCodesLabels | — | Overrides for every user-facing string. |
| variant | "page" | "inline" | "page" | Outer positioning — full-page centered, or flows inline in an existing layout. |
| className | string | — | Extra classes on the outer wrapper. |
Exactly one of expectedHash or verify is required.
Accessibility
The access-code field is a labeled, masked text input with native paste support. Errors and the submitting state announce through an aria-live="polite" status region, so screen reader users hear "Checking..." and any error without focus moving. The whole flow works keyboard-only: tab to the field, type, Enter to submit.
Customization
Every string is overridable through the `labels` prop for localization. The wrapper has no fixed visual opinions beyond the default card — pass `className` to restyle the shell, or skip this component and call `useKnockCodes` directly to build a fully custom prompt.
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.
- Branded AccessA copy-paste React password screen for client previews and private betas. One file, zero dependencies, local or server verification.
- Knock CodesA copy-paste React access screen for client previews and staging apps — segmented code entry, dark card, footer help text. One file, zero dependencies.
- Minimal AccessA copy-paste React access screen for internal tools and quick gates — a single masked field, no frills. One file, zero dependencies.
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 RouteA copy-paste React access screen for route-level guarding — React Router or similar, with an optional custom denial state. Zero dependencies.
- Standalone GateA copy-paste React access screen for the fastest possible integration — wrap your app, pass a hash, done. Zero dependencies.
- Protected CardA copy-paste React access screen for gating one card in a dashboard grid — blurred preview, inline unlock. Zero dependencies.