Protected Card
A copy-paste React access screen for gating one card in a dashboard grid — blurred preview, inline unlock. Zero dependencies.
Best used for A single gated tile inside a grid of otherwise-public cards, e.g. one locked dashboard widget.
Click "Unlock" to reveal an inline access-code form in place, without leaving the card or navigating away.
Minimal setup
<ProtectedCard expectedHash={process.env.NEXT_PUBLIC_KNOCK_HASH}>
<AccountSummary />
</ProtectedCard>
- components
- knock-codes
- core
- react
"use client";
import { useState, type ReactNode } from "react";
import { PinInput } from "./PinInput.tsx";
import { GateWrapper } from "./GateWrapper.tsx";
import { GateSession } from "./KnockCodesContext.tsx";
import type { KnockCodesConfig, UseKnockCodesResult } from "./types.ts";
import { cx } from "./cx.ts";
export interface ProtectedCardProps extends KnockCodesConfig {
children: ReactNode;
autoFocus?: boolean;
className?: string;
}
function StatusBadge({ unlocked }: { unlocked: boolean }) {
return (
<span
className={cx(
"absolute top-3 left-3 z-10 inline-flex items-center gap-1.5 rounded-full border px-2 py-0.5 font-mono text-[10px] font-medium tracking-wider uppercase",
unlocked
? "border-[#187c74]/30 bg-[#187c74]/10 text-[#187c74] dark:border-[#4fd1c5]/30 dark:bg-[#4fd1c5]/10 dark:text-[#4fd1c5]"
: "border-[#e5484d]/30 bg-[#e5484d]/10 text-[#e5484d] dark:border-[#ff6169]/30 dark:bg-[#ff6169]/10 dark:text-[#ff6169]"
)}
>
<span
aria-hidden="true"
className={cx("h-1.5 w-1.5 shrink-0 rounded-full", unlocked ? "bg-[#187c74] dark:bg-[#4fd1c5]" : "bg-[#e5484d] dark:bg-[#ff6169]")}
/>
{unlocked ? "Unlocked" : "Locked"}
</span>
);
}
/**
* A card-shaped gate for protecting one section of a page rather than the
* whole viewport — locked content stays mounted and blurred behind an
* "Unlock" button instead of being replaced outright, so the card's size
* and position never jump between locked and unlocked.
*/
export function ProtectedCard({ children, autoFocus = true, className, ...config }: ProtectedCardProps) {
return (
<GateSession config={config}>
{(session) => (
<ProtectedCardView autoFocus={autoFocus} className={className} session={session}>
{children}
</ProtectedCardView>
)}
</GateSession>
);
}
function ProtectedCardView({
children,
autoFocus = true,
className,
session,
}: Pick<ProtectedCardProps, "children" | "autoFocus" | "className"> & { session: UseKnockCodesResult }) {
const { ready, state, error, submit } = session;
const [expanded, setExpanded] = useState(false);
const [code, setCode] = useState("");
if (!ready) return null;
const cardClassName = cx(
"relative w-full max-w-sm rounded-[var(--ag-radius,0.75rem)] border border-[var(--ag-border,#e5e7eb)] bg-[var(--ag-card,#ffffff)] shadow-sm dark:border-[var(--ag-border-dark,#1f2937)] dark:bg-[var(--ag-card-dark,#030712)]",
className
);
if (state === "unlocked") {
return (
<GateWrapper variant="inline">
<div style={{ fontFamily: "var(--ag-font, inherit)" }} className={cx(cardClassName, "p-6")}>
<StatusBadge unlocked />
<div className="pt-5">{children}</div>
</div>
</GateWrapper>
);
}
const handleSubmit = async () => {
await submit(code);
setCode("");
};
return (
<GateWrapper variant="inline">
<div style={{ fontFamily: "var(--ag-font, inherit)" }} className={cx(cardClassName, "overflow-hidden p-6")}>
<StatusBadge unlocked={false} />
<div aria-hidden="true" className="pointer-events-none pt-5 select-none blur-sm">
{children}
</div>
<div className="absolute inset-0 flex items-center justify-center bg-[var(--ag-card,#ffffff)]/90 p-4 dark:bg-[var(--ag-card-dark,#030712)]/90">
{expanded ? (
<div className="w-full max-w-xs">
<PinInput
value={code}
onChange={setCode}
onSubmit={handleSubmit}
submitting={state === "submitting"}
error={error}
autoFocus={autoFocus}
/>
</div>
) : (
<button
type="button"
onClick={() => setExpanded(true)}
className="rounded-[var(--ag-radius,0.375rem)] bg-[var(--ag-primary,#187c74)] px-4 py-2 text-sm font-medium text-[var(--ag-primary-fg,#f7f3ea)] transition-colors hover:opacity-90 dark:bg-[var(--ag-primary-dark,#4fd1c5)] dark:text-[var(--ag-primary-fg-dark,#0e1311)]"
>
Unlock
</button>
)}
</div>
</div>
</GateWrapper>
);
}
Add this block to your project
Recommended
npx shadcn@latest add @knock-codes/protected-cardAlso installs
- Knock Codes Core
- Knock Codes Types
- useKnockCodes
- PIN Input
- cx (classname helper)
- Gate Wrapper
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/PinInput.tsx
- components/knock-codes/react/cx.ts
- components/knock-codes/react/GateWrapper.tsx
- components/knock-codes/react/ProtectedCard.tsx
Other ways
GitHub shorthand
npx shadcn@latest add trivedi-vatsal/knock-codes/protected-cardCopy 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 | — | The card's content — blurred while locked, revealed once unlocked. |
| className | string | — | Extra classes on the card. |
Exactly one of expectedHash or verify is required.
Accessibility
The blurred preview behind the Unlock button is aria-hidden and inert, matching Protected Modal's approach — it's decorative once the button is shown, not a second interactive surface.
Customization
Locked content stays mounted and blurred behind an Unlock button instead of being replaced outright, so the card's size and position never jump between locked and unlocked. Good fit for a dashboard tile or a gated preview inside a grid of otherwise-public cards.
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.
- Embedded GateA copy-paste React access screen for gating a widget or section inside an existing layout — Knock Codes, fixed to the inline shell. Zero dependencies.
- Gate WrapperA copy-paste React layout primitive for composing a fully custom access-code UI — page-centered or inline positioning, no verification logic. Zero dependencies.