← ui

veiled

ui

a value that identifies its owner, shown as dots and peeked for a few seconds

npx shadcn add @ag/veiled
Already installed? Preview an update
npx shadcn add @ag/veiled --dry-run

Your 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; keep leaves 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.
Updated 2026-10-19 · release notes

also called secret, redacted, masked, spoiler

shelf text & code

a serial, veiled

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

veiled.demo.tsx
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

veiled.tsx
"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>
  )
}