a value that identifies its owner, shown as dots and peeked for a few seconds
npx shadcn add @ag/veiledAlready installed? Preview an update
npx shadcn add @ag/veiled --dry-runYour installed source stays unchanged until you update it. How updates work
Latest changes · 2026-10-19
- New: a value that identifies its owner (a serial, a licence, an account key) shown as dots, so a screenshot or a shared screen never leaks it. A click peeks it for five seconds and it veils itself again;
keepleaves its first characters legible.<Copy value={serial} />beside it copies it without showing it. It keeps a value off a screen, not out of a browser: its props travel with the page.
also called secret, redacted, masked, spoiler
shelf text & code
Click it: it shows for five seconds, then veils itself.
serial
Copy beside it, never around it: the value peeks, the mark copies.
serial
keep leaves the first characters, enough to tell two apart.
the demo above, verbatim
import { Sample } from "@/app/samples"
import { Copy } from "@/registry/base-nova/ui/copy"
import { Veiled } from "@/registry/base-nova/ui/veiled"
// #region values
const serial = "C02XK1ABJG5H"
const licence = "LIC-7Q4M-2KD9-PX3R-88TB"
// #endregion
export default function Demo() {
return (
<div className="space-y-8">
<Sample
name="serial"
glance
title="a serial, veiled"
description="Click it: it shows for five seconds, then veils itself."
with="values"
>
<p className="text-sm text-foreground/80">
serial <Veiled value={serial} />
</p>
</Sample>
<Sample
name="copy"
title="copied without being shown"
description="Copy beside it, never around it: the value peeks, the mark copies."
with="values"
>
<div className="flex items-center gap-1 text-sm">
<span className="mr-2 text-muted-foreground">serial</span>
<Veiled value={serial} />
<Copy value={serial} tip="copy without showing it" />
</div>
</Sample>
<Sample
name="keep"
title="its start kept legible"
description="keep leaves the first characters, enough to tell two apart."
with="values"
>
<ul className="space-y-1 text-sm">
<li>
<Veiled value={licence} keep={4} />
</li>
<li>
<Veiled value="LIC-2B8N-9QW1-ZZ4D-01MK" keep={4} />
</li>
</ul>
</Sample>
</div>
)
}the source, every file an install writes
"use client"
// A VALUE THAT IDENTIFIES ITS OWNER (a serial, a licence, an account key) shown
// veiled: dots, so a screenshot or a shared screen never leaks it. A click peeks it
// for a few seconds and it veils itself again. While veiled the value is in no text
// on screen, no title and nothing a screen reader says, so selecting the page finds
// dots. It is in the page's data all the same (a client component's props travel
// with the page): Veiled keeps a value off a screen, not out of a browser. A secret
// the browser must not hold is never sent to it.
//
// Monospaced, so a dot is as wide as any character and peeking never moves the line;
// the eye waits under the hand in a slot that is always there. `keep` leaves the
// first characters legible (enough to tell two apart).
//
// To copy it without showing it, a `Copy` beside it, never around it: a click on the
// value peeks, a click on the mark copies.
//
// <Veiled value={serial} />
// <Veiled value={serial} /> <Copy value={serial} tip="copy without showing it" />
//
// Swift's `Veiled` is the same.
import { Eye, EyeOff } from "lucide-react"
import * as React from "react"
import { cn } from "@/lib/utils"
/** How long a peek lasts: long enough to read it out, short enough that it never
* stays up for the next screenshot. */
export const PEEK_MS = 5000
export interface VeiledProps
extends Omit<React.ComponentProps<"button">, "value" | "children"> {
value: string
/** Leading characters left legible. Default none. */
keep?: number
}
export function Veiled({
value,
keep = 0,
className,
onClick,
...props
}: VeiledProps) {
// The value being peeked, not a flag: a new value is not it, so it arrives
// veiled, whatever the old one was doing.
const [peeked, setPeeked] = React.useState<string | null>(null)
const shown = peeked === value
const timer = React.useRef(0)
React.useEffect(() => () => window.clearTimeout(timer.current), [])
const veiled =
value.slice(0, keep) + "•".repeat(Math.max(0, value.length - keep))
return (
<button
data-slot="veiled"
data-shown={shown ? "" : undefined}
type="button"
aria-label={shown ? `veiled value: ${value}` : "veiled value, hidden"}
aria-pressed={shown}
onClick={(event) => {
onClick?.(event)
if (event.defaultPrevented) return
window.clearTimeout(timer.current)
if (shown) return setPeeked(null)
setPeeked(value)
timer.current = window.setTimeout(() => setPeeked(null), PEEK_MS)
}}
className={cn(
"group/veiled inline-flex cursor-pointer items-center gap-1 rounded-sm font-mono outline-none focus-visible:ring-1 focus-visible:ring-current/40",
className,
)}
{...props}
>
<span data-slot="veiled-value" aria-hidden>
{shown ? value : veiled}
</span>
<span
data-slot="veiled-mark"
aria-hidden
className="shrink-0 text-current/60 opacity-0 transition-opacity group-hover/veiled:opacity-100 group-focus-visible/veiled:opacity-100"
>
{shown ? <EyeOff size={12} /> : <Eye size={12} />}
</span>
</button>
)
}