COMPONENT / classified-card
Classified Card_
Dashed dossier frame with a rotated stamp and inline redaction bars that declassify on click. Visual only: never for real secrets. Requires @cybr/tokens.
Install
1. Map the
@cybrnamespace, once per project. Add this inside yourcomponents.json, or into its existing"registries"object. Nocomponents.jsonyet? Runnpx shadcn@latest initfirst to create one."registries": { "@cybr": "https://cybr-ui.com/r/{name}.json" }2. Add components:
$ npx shadcn@latest add @cybr/classified-card
If shadcn reports Unknown registry "@cybr", step 1 is missing from that project's components.json.
Preview
REDACTEDCLICK BAR TO DECLASSIFY
Subject entered 0300 hrs and relay.
registry/ui/classified-card.tsx
"use client";
import { useEffect, useRef, useState, type ComponentProps, type ReactNode } from "react";
import { cn } from "@/lib/utils";
// Dashed dossier frame with a rotated stamp. Wrap words in <Redacted> to black
// them out until clicked. This is a visual effect only: redacted text is in the
// page HTML, so never use it for real secrets.
export function ClassifiedCard({
stamp = "REDACTED",
hint,
className,
children,
...props
}: ComponentProps<"div"> & { stamp?: ReactNode; hint?: ReactNode }) {
return (
<div
className={cn(
"flex flex-col gap-2.5 border-(length:--border-base) border-dashed border-dim p-4.5 font-mono text-[0.8125rem] [line-height:1.7] text-ink",
className,
)}
{...props}
>
{stamp ? (
<span className="-rotate-3 self-start border-(length:--border-base) border-solid border-signal px-2 py-0.5 text-caption font-bold tracking-[0.2em] text-signal">
{stamp}
</span>
) : null}
<div>{children}</div>
{hint ? <span className="text-caption tracking-[0.12em] text-label">{hint}</span> : null}
</div>
);
}
// An ink bar over inline text. Activating it reveals the text for good and
// moves focus onto it, so screen readers read what was uncovered.
export function Redacted({
className,
children,
onClick,
...props
}: Omit<ComponentProps<"button">, "children" | "type"> & { children: ReactNode }) {
const [revealed, setRevealed] = useState(false);
const revealedRef = useRef<HTMLSpanElement>(null);
useEffect(() => {
if (revealed) revealedRef.current?.focus();
}, [revealed]);
if (revealed) {
return (
<span ref={revealedRef} tabIndex={-1} className="outline-none">
{children}
</span>
);
}
return (
<button
type="button"
onClick={(event) => {
onClick?.(event);
if (!event.defaultPrevented) setRevealed(true);
}}
className={cn(
"inline cursor-pointer rounded-none border-0 bg-transparent p-0 text-left [font:inherit] [color:inherit]",
"focus-visible:outline-(length:--focus-width) focus-visible:outline-offset-2 focus-visible:outline-solid focus-visible:outline-signal",
className,
)}
{...props}
>
<span aria-hidden="true" className="bg-ink box-decoration-clone text-transparent select-none">
{children}
</span>
<span className="sr-only">Redacted. Activate to declassify.</span>
</button>
);
}
Repo source. The shadcn CLI rewrites import aliases to match your project.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| stamp | ReactNode | "REDACTED" | Rotated stamp at the top. Pass null to hide it. |
| hint | ReactNode | — | Small line under the body, such as an instruction. |
| children | ReactNode | — | Body text. Wrap words in <Redacted> to black them out. |
| ...props | ComponentProps<"div"> | — | Every other native <div> attribute is forwarded; className is merged last with cn(). |
| Redacted children | ReactNode | — | The hidden text. |
| Redacted ...props | Omit<ComponentProps<"button">, "children" | "type"> | — | Forwarded to the bar's <button> while it is concealed. Call preventDefault in onClick to keep it concealed. |
Usage
Needs the stylesheet from @cybr/tokens. After the install, import it in app/globals.css:
@import "tailwindcss";
@import "../styles/cybr-tokens.css";
import { ClassifiedCard, Redacted } from "@/components/ui/classified-card";
export function Dossier() {
return (
<ClassifiedCard hint="CLICK BAR TO DECLASSIFY">
Subject entered <Redacted>sector nine at</Redacted> 0300 hrs.
</ClassifiedCard>
);
}
States
- Concealed: an
--inkbar with transparent text, the width of the hidden words. - Focus-visible: a 3px solid
--signaloutline around the bar. - Declassified: click, Enter or Space reveals the text for good. There is no re-redact.
- Without JavaScript: the bars stay concealed. That is the intended effect.
Accessibility
- This is a visual effect, not security. The hidden text is in the page HTML. Never use it for real secrets.
- Each bar is a native
<button>. Screen readers hear "Redacted. Activate to declassify." and never the hidden words while concealed. - After a reveal, the button is replaced by the text and focus moves onto it, so focus never drops to the page and screen readers read what was uncovered.
- The stamp is real text,
--signalon the surrounding background at 11px bold. On--bgthat is 4.64:1. - Body text is
--ink. If you override colours withclassName, keep text contrast at 4.5:1 or above. - Nothing animates.