→ Core
Knock Codes
The main gate — renders children when unlocked, otherwise an access-code prompt.
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.
- components
- knock-codes
- core
- react
"use client";
import { useState, type ReactNode } from "react";
import { useKnockCodes } from "./useKnockCodes.ts";
import { PinInput } from "./PinInput.tsx";
import { GateWrapper, type GateWrapperVariant } from "./GateWrapper.tsx";
import { DEFAULT_LABELS, type KnockCodesConfig, type KnockCodesLabels } from "./types.ts";
import { cx } from "./cx.ts";
export interface KnockCodesProps extends KnockCodesConfig {
children: ReactNode;
labels?: KnockCodesLabels;
/** Visual shell around the PIN prompt. @default "page" */
variant?: GateWrapperVariant;
className?: string;
}
/**
* Wrapper component. Renders `children` only when a valid session exists;
* otherwise renders the PIN entry UI. There is no separate "mount loading"
* state — the PIN entry UI covers it.
*/
export function KnockCodes({ children, labels, variant = "page", className, ...config }: KnockCodesProps) {
const { state, error, submit } = useKnockCodes(config);
const [code, setCode] = useState("");
if (state === "unlocked") return <>{children}</>;
const merged = { ...DEFAULT_LABELS, ...labels };
const modeLabel = config.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={cx(
"relative w-full max-w-sm space-y-4 border border-[var(--ag-border,#d9d2c2)] bg-[var(--ag-card,#fbf8f1)] p-6 shadow-sm",
"rounded-[var(--ag-radius,0.5rem)] dark:border-[var(--ag-border-dark,#26302b)] dark:bg-[var(--ag-card-dark,#171d1a)]"
)}
>
<span aria-hidden="true" className="absolute -top-px -left-px h-2.5 w-2.5 border-t border-l border-[#191a18]/25 dark:border-[#edeae0]/25" />
<span aria-hidden="true" className="absolute -top-px -right-px h-2.5 w-2.5 border-t border-r border-[#191a18]/25 dark:border-[#edeae0]/25" />
<span aria-hidden="true" className="absolute -bottom-px -left-px h-2.5 w-2.5 border-b border-l border-[#191a18]/25 dark:border-[#edeae0]/25" />
<span aria-hidden="true" className="absolute -bottom-px -right-px h-2.5 w-2.5 border-b border-r border-[#191a18]/25 dark:border-[#edeae0]/25" />
<div className="flex items-center justify-between gap-2">
<span className="inline-flex items-center gap-1.5 text-[10px] font-medium tracking-wider text-[#e5484d] uppercase dark:text-[#ff6169]">
<span aria-hidden="true" className="h-1.5 w-1.5 shrink-0 rounded-full bg-[#e5484d] dark:bg-[#ff6169]" />
Locked
</span>
<span className="rounded-full border border-[#d9d2c2] px-2 py-0.5 font-mono text-[10px] font-medium tracking-wider text-[#6b6456] uppercase dark:border-[#26302b] dark:text-[#9aa39c]">
{modeLabel}
</span>
</div>
<div className="space-y-1">
<h1 className="text-lg font-semibold text-[#191a18] dark:text-[#edeae0]">{merged.heading}</h1>
{merged.subcopy && <p className="text-sm text-[#6b6456] dark:text-[#9aa39c]">{merged.subcopy}</p>}
</div>
<PinInput
value={code}
onChange={setCode}
onSubmit={handleSubmit}
submitting={state === "submitting"}
error={error}
labels={labels}
autoFocus
/>
</div>
</GateWrapper>
);
}
Add this block to your project
CLI
npx shadcn@latest add http://localhost:3000/r/react/knock-codes.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
- cx (classname helper)
- Gate Wrapper
- PIN Input
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/cx.ts
- components/knock-codes/react/GateWrapper.tsx
- components/knock-codes/react/PinInput.tsx
- components/knock-codes/react/KnockCodes.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. 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. |
| 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. |
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.
→ 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.