# Input Secret

A secret field with visibility and accessible copy feedback.

> For the complete documentation index, see [llms.txt](/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](/.well-known/agent-skills/site-skill.md).





## Playground [#playground]

```tsx
"use client";

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

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

## Installation [#installation]

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

```bash
npx 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 [#dependencies]

Installed automatically by the registry CLI. For manual setup:

```bash
npm install lucide-react
```

## Usage [#usage]

```tsx
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 [#examples]

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

<ComponentPreview name="input-secret-demo" title="input-secret-demo.tsx">
  <InputSecretDemo />
</ComponentPreview>

### Optional Storybook Stories [#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 [#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 [#props]

| Prop             | Type                                        | Default                     | Description                                                                  |
| ---------------- | ------------------------------------------- | --------------------------- | ---------------------------------------------------------------------------- |
| copiedLabel      | string                                      | "Secret copied."            | InputSecret: live announcement after successful copy.                        |
| copyLabel        | string                                      | "Copy secret"               | InputSecret: accessible name of copy action.                                 |
| defaultValue     | string                                      | undefined                   | InputSecret: initial uncontrolled value; examples must use fake values only. |
| disabled         | boolean                                     | false                       | InputSecret: disables editing and actions.                                   |
| emptyLabel       | string                                      | "Secret is empty."          | InputSecret: live announcement when empty.                                   |
| errorLabel       | string                                      | "Unable to copy secret."    | InputSecret: live announcement when copying fails.                           |
| label            | string                                      | undefined                   | InputSecret: visible associated label.                                       |
| onChange         | React.ChangeEventHandler\<HTMLInputElement> | undefined                   | InputSecret: native input change callback.                                   |
| readOnly         | boolean                                     | false                       | InputSecret: prevents editing but allows show/copy actions.                  |
| unavailableLabel | string                                      | "Clipboard is unavailable." | InputSecret: live announcement when Clipboard API is unavailable.            |
| value            | string                                      | uncontrolled                | InputSecret: native controlled value.                                        |

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 [#source]

```tsx
"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>
    );
  }
);

```
