For the complete documentation index, see llms.txt. Markdown variants are available by appending .md to any URL or sending an Accept: text/markdown header. An agent skill is available at /.well-known/agent-skills/site-skill.md.
1
Support

Input Secret

A secret field with visibility and accessible copy feedback.

Playground

Live preview
This preview uses a fake value. Clipboard feedback is announced without repeating the secret.
Customize input-secret
Customize
input-secret-demo.tsx
"use client";

import { InputSecret } from "@/components/ui/input-secret";

export function InputSecretDemo() {
  return (
    <InputSecret
        label={"API secret"}
        defaultValue="vnd_test_example_secret"
    />
  );
}

Installation

Initialize shadcn in your React project, then install this component from the Vandor UI registry:

$ pnpm dlx shadcn@latest add https://vandor-ui.vercel.app/r/input-secret.json

The default installation target is components/ui/input-secret.tsx. Review the CLI's overwrite prompt before replacing local customizations.

Dependencies

Installed automatically by the registry CLI. For manual setup:

Dependencies1 required packages
  • lucide-react
$ pnpm add lucide-react

Usage

import { InputSecret } from "@/components/ui/input-secret";
 
<InputSecret label="API secret" defaultValue="vnd_live_example" />;

Use for values that should be hidden by default but may be intentionally revealed or copied.

Examples

This preview uses a clearly fake value. Never place production credentials in examples or source snippets.

input-secret-demo.tsx
import { InputSecret } from "@/components/ui/input-secret";

export const InputSecretDemo = () => (
  <InputSecret
    label="API secret"
    defaultValue="vnd_test_example_secret"
    className="max-w-sm"
  />
);

Optional Storybook Stories

Install Input Secret first, then run npx shadcn@latest add https://vandor-ui.vercel.app/r/input-secret-stories.json. Stories cover reveal/hide, copying a fake credential, empty values, controlled editing, and disabled/read-only/invalid states. Never enter real credentials in Storybook. Clipboard requires a secure origin and browser permission; failures are announced by the component.

Stories install beside Input Secret under your configured UI alias and use relative imports. Use your existing matching @storybook/react packages (verified with 10.6.1), discover **/*.stories.tsx, and load your global Tailwind/theme stylesheet. No addons, extra packages, core overwrites, or Storybook config edits are required. Default-value Controls remount uncontrolled examples; Controlled owns local state without echoing the secret. Storybook UI is not verified because no server exists here.

Behavior & Accessibility

Visibility and copy actions are separately named. Copy success and failure are announced without placing the secret in status text. Clipboard access may be unavailable or denied; no fallback displays the secret elsewhere.

Props

Component props, types, defaults, and descriptions
PropType / DescriptionDefault
copiedLabelstring

InputSecret: live announcement after successful copy.

"Secret copied."
copyLabelstring

InputSecret: accessible name of copy action.

"Copy secret"
defaultValuestring

InputSecret: initial uncontrolled value; examples must use fake values only.

undefined
disabledboolean

InputSecret: disables editing and actions.

false
emptyLabelstring

InputSecret: live announcement when empty.

"Secret is empty."
errorLabelstring

InputSecret: live announcement when copying fails.

"Unable to copy secret."
labelstring

InputSecret: visible associated label.

undefined
onChangeReact.ChangeEventHandler<HTMLInputElement>

InputSecret: native input change callback.

undefined
readOnlyboolean

InputSecret: prevents editing but allows show/copy actions.

false
unavailableLabelstring

InputSecret: live announcement when Clipboard API is unavailable.

"Clipboard is unavailable."
valuestring

InputSecret: native controlled value.

uncontrolled

The reference lists component-specific props and selected native props, not the complete React API. Other native attributes are forwarded to the rendered element.

Source

"use client";
 
import { cn } from "cn";
import { Copy, Eye, EyeOff } from "lucide-react";
import * as React from "react";
 
import { Button } from "./button";
import { Input } from "./input";
import type { InputProps } from "./input";
 
export type InputSecretProps = Omit<InputProps, "icon" | "type"> & {
  copyLabel?: string;
  copiedLabel?: string;
  emptyLabel?: string;
  unavailableLabel?: string;
  errorLabel?: string;
};
 
export const InputSecret = React.forwardRef<HTMLInputElement, InputSecretProps>(
  function InputSecret(
    {
      className,
      copyLabel = "Copy secret",
      copiedLabel = "Secret copied.",
      emptyLabel = "Secret is empty.",
      unavailableLabel = "Clipboard is unavailable.",
      errorLabel = "Unable to copy secret.",
      disabled,
      readOnly,
      onChange,
      ...props
    },
    forwardedRef
  ) {
    const inputRef = React.useRef<HTMLInputElement>(null);
    React.useImperativeHandle(
      forwardedRef,
      () => inputRef.current as HTMLInputElement
    );
    const [visible, setVisible] = React.useState(false);
    const [status, setStatus] = React.useState("");
 
    const copySecret = async () => {
      const secret = inputRef.current?.value;
      if (!secret) {
        setStatus(emptyLabel);
        return;
      }
      try {
        if (
          typeof navigator === "undefined" ||
          !navigator.clipboard?.writeText
        ) {
          setStatus(unavailableLabel);
          return;
        }
        await navigator.clipboard.writeText(secret);
        setStatus(copiedLabel);
      } catch {
        setStatus(errorLabel);
      }
    };
 
    return (
      <div data-slot="secret-input" className="relative min-w-0">
        <Input
          {...props}
          ref={inputRef}
          type={visible ? "text" : "password"}
          disabled={disabled}
          readOnly={readOnly}
          onChange={(event) => {
            setStatus("");
            onChange?.(event);
          }}
          className={cn("pr-20", className)}
        />
        <div className="absolute top-1/2 right-1 z-10 flex -translate-y-1/2 items-center gap-0.5">
          <Button
            type="button"
            variant="ghost"
            size="icon-sm"
            disabled={disabled}
            aria-label={visible ? "Hide secret" : "Show secret"}
            aria-pressed={visible}
            onClick={() => setVisible((current) => !current)}
          >
            {visible ? (
              <EyeOff aria-hidden="true" />
            ) : (
              <Eye aria-hidden="true" />
            )}
          </Button>
          <Button
            type="button"
            variant="ghost"
            size="icon-sm"
            disabled={disabled}
            aria-label={copyLabel}
            onClick={copySecret}
          >
            <Copy aria-hidden="true" />
          </Button>
        </div>
        <span className="sr-only" role="status" aria-live="polite">
          {status}
        </span>
      </div>
    );
  }
);