- Accordion
- AppShell
- Autocomplete
- Badge
- Blobatar
- BoringAvatar
- Button
- Calendar
- Checkbox
- DataGrid
- Date Picker
- Date Range Picker
- DateTime
- Dialog
- Drawer
- Dropdown
- Empty
- Error State
- Frame
- Input
- Input Amount
- Input Group
- Input OTP
- Input Password
- Input Phone
- Input Search
- Input Secret
- Loading
- PageContainer
- PageHeader
- Popover
- Select
- StatsCard
- Switch
- Tabs
- TextArea
- Toast
- Tooltip
Playground
"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:
- lucide-react
$ pnpm add lucide-reactUsage
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.
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
| Prop | Type / Description | Default |
|---|---|---|
| copiedLabel | stringInputSecret: live announcement after successful copy. | "Secret copied." |
| copyLabel | stringInputSecret: accessible name of copy action. | "Copy secret" |
| defaultValue | stringInputSecret: initial uncontrolled value; examples must use fake values only. | undefined |
| disabled | booleanInputSecret: disables editing and actions. | false |
| emptyLabel | stringInputSecret: live announcement when empty. | "Secret is empty." |
| errorLabel | stringInputSecret: live announcement when copying fails. | "Unable to copy secret." |
| label | stringInputSecret: visible associated label. | undefined |
| onChange | React.ChangeEventHandler<HTMLInputElement>InputSecret: native input change callback. | undefined |
| readOnly | booleanInputSecret: prevents editing but allows show/copy actions. | false |
| unavailableLabel | stringInputSecret: live announcement when Clipboard API is unavailable. | "Clipboard is unavailable." |
| value | stringInputSecret: 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>
);
}
);