Password Input
A shadcn-style password input component built with Ark UI primitives.
import {
PasswordInput,
PasswordInputRoot,
} from "@/components/ui/password-input";
const PasswordInputDemo = () => (
<PasswordInputRoot className="w-full max-w-xs">
<PasswordInput placeholder="Enter password" />
</PasswordInputRoot>
);
export default PasswordInputDemo;
Installation
npx shadcn@latest add @ark-cn/password-inputInstall the dependency required by this primitive:
npm install @ark-ui/react lucide-reactCopy the component source into your app:
TSXcomponents/ui/password-input.tsx
"use client";
import { PasswordInput as PasswordInputPrimitive } from "@ark-ui/react/password-input";
import { EyeIcon, EyeOffIcon } from "lucide-react";
import type { ComponentProps, ReactNode } from "react";
import { Button } from "@/components/ui/button";
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
} from "@/components/ui/input-group";
import { cn } from "@/lib/utils";
export type PasswordInputRootProps = PasswordInputPrimitive.RootProps;
export const PasswordInputRoot = ({
className,
...props
}: PasswordInputRootProps) => (
<PasswordInputPrimitive.Root
className={cn(
"group/password-input flex w-full flex-col gap-1.5",
className,
)}
data-slot="password-input"
{...props}
/>
);
export type PasswordInputLabelProps = PasswordInputPrimitive.LabelProps;
export const PasswordInputLabel = ({
className,
...props
}: PasswordInputLabelProps) => (
<PasswordInputPrimitive.Label
className={cn(
"text-sm font-medium text-foreground leading-none select-none",
className,
)}
data-slot="password-input-label"
{...props}
/>
);
export type PasswordInputControlProps = PasswordInputPrimitive.ControlProps;
export const PasswordInputControl = ({
...props
}: PasswordInputControlProps) => (
<PasswordInputPrimitive.Control
data-slot="password-input-control"
{...props}
/>
);
export type PasswordInputInputProps = PasswordInputPrimitive.InputProps;
export const PasswordInputInput = ({ ...props }: PasswordInputInputProps) => (
<PasswordInputPrimitive.Input data-slot="password-input-input" {...props} />
);
export type PasswordInputProps = {
startAddon?: ReactNode;
endAddon?: ReactNode;
variant?: ComponentProps<typeof Button>["variant"];
size?: ComponentProps<typeof Button>["size"];
hideVisibilityToggle?: boolean;
} & Omit<PasswordInputInputProps, "size">;
export const PasswordInput = ({
startAddon,
endAddon,
variant = "ghost",
size = "icon-xs",
hideVisibilityToggle = false,
...props
}: PasswordInputProps) => {
return (
<PasswordInputControl asChild>
<InputGroup>
{startAddon && (
<InputGroupAddon align="inline-start">{startAddon}</InputGroupAddon>
)}
<PasswordInputInput {...props} asChild>
<InputGroupInput />
</PasswordInputInput>
<InputGroupAddon align="inline-end">
{endAddon && <>{endAddon}</>}
{!hideVisibilityToggle && (
<PasswordInputVisibilityTrigger asChild>
<Button variant={variant} size={size}>
<PasswordInputIndicator fallback={<EyeOffIcon />}>
<EyeIcon />
</PasswordInputIndicator>
</Button>
</PasswordInputVisibilityTrigger>
)}
</InputGroupAddon>
</InputGroup>
</PasswordInputControl>
);
};
export type PasswordInputVisibilityTriggerProps =
PasswordInputPrimitive.VisibilityTriggerProps;
export const PasswordInputVisibilityTrigger = ({
...props
}: PasswordInputVisibilityTriggerProps) => (
<PasswordInputPrimitive.VisibilityTrigger
data-slot="password-input-visibility-trigger"
{...props}
/>
);
export type PasswordInputIndicatorProps = PasswordInputPrimitive.IndicatorProps;
export const PasswordInputIndicator = ({
...props
}: PasswordInputIndicatorProps) => (
<PasswordInputPrimitive.Indicator
data-slot="password-input-indicator"
{...props}
/>
);
export type PasswordInputRootProviderProps =
PasswordInputPrimitive.RootProviderProps;
export const PasswordInputRootProvider = (
props: PasswordInputRootProviderProps,
) => (
<PasswordInputPrimitive.RootProvider
data-slot="password-input-root-provider"
{...props}
/>
);
export type PasswordInputContextProps = PasswordInputPrimitive.ContextProps;
export const PasswordInputContext = (props: PasswordInputContextProps) => (
<PasswordInputPrimitive.Context {...props} />
);
export type { PasswordInputVisibilityChangeDetails } from "@ark-ui/react/password-input";
export {
type UsePasswordInputProps,
type UsePasswordInputReturn,
usePasswordInput,
usePasswordInputContext,
} from "@ark-ui/react/password-input";
Update import aliases to match your project setup.
Usage
import * as PasswordInput from "@/components/ui/password-input"Read exported parts in src/components/ui/password-input.tsx and compose the primitive according to the Ark UI pattern for this component.
Examples
Default
import {
PasswordInput,
PasswordInputLabel,
PasswordInputRoot,
} from "@/components/ui/password-input";
const PasswordInputDefault = () => (
<div className="w-full max-w-xs">
<PasswordInputRoot>
<PasswordInputLabel>Password</PasswordInputLabel>
<PasswordInput placeholder="Enter password" />
</PasswordInputRoot>
</div>
);
export default PasswordInputDefault;
Autocomplete
import {
PasswordInput,
PasswordInputLabel,
PasswordInputRoot,
} from "@/components/ui/password-input";
const PasswordInputAutocomplete = () => (
<div className="flex w-full max-w-md flex-col gap-4 sm:flex-row">
<PasswordInputRoot autoComplete="new-password" className="flex-1">
<PasswordInputLabel>New password</PasswordInputLabel>
<PasswordInput name="new-password" placeholder="Create a password" />
</PasswordInputRoot>
<PasswordInputRoot autoComplete="current-password" className="flex-1">
<PasswordInputLabel>Current password</PasswordInputLabel>
<PasswordInput name="current-password" placeholder="Sign in" />
</PasswordInputRoot>
</div>
);
export default PasswordInputAutocomplete;
Controlled visibility
Controlled visible: false
import { useState } from "react";
import {
PasswordInput,
PasswordInputLabel,
PasswordInputRoot,
} from "@/components/ui/password-input";
const PasswordInputControlledVisibility = () => {
const [visible, setVisible] = useState(false);
return (
<div className="flex w-full max-w-xs flex-col gap-2">
<p className="text-muted-foreground text-xs">
Controlled visible:{" "}
<span className="font-medium text-foreground">{String(visible)}</span>
</p>
<PasswordInputRoot
onVisibilityChange={(details) => setVisible(details.visible)}
visible={visible}
>
<PasswordInputLabel>Password</PasswordInputLabel>
<PasswordInput placeholder="Toggle is controlled externally" />
</PasswordInputRoot>
</div>
);
};
export default PasswordInputControlledVisibility;
Root provider
usePasswordInput + PasswordInputRootProvider - visible false
import { Button } from "@/components/ui/button";
import {
PasswordInput,
PasswordInputLabel,
PasswordInputRootProvider,
usePasswordInput,
} from "@/components/ui/password-input";
const PasswordInputRootProviderDemo = () => {
const store = usePasswordInput({ defaultVisible: false });
return (
<PasswordInputRootProvider value={store}>
<div className="flex w-full max-w-xs flex-col gap-2">
<p className="text-muted-foreground text-xs">
usePasswordInput + PasswordInputRootProvider - visible{" "}
<span className="font-medium text-foreground">
{String(store.visible)}
</span>
</p>
<PasswordInputLabel>Password</PasswordInputLabel>
<PasswordInput defaultValue="hunter2" />
<Button
onClick={store.toggleVisible}
size="sm"
type="button"
variant="outline"
>
Toggle from outside
</Button>
</div>
</PasswordInputRootProvider>
);
};
export default PasswordInputRootProviderDemo;
With field
import { Field, FieldDescription, FieldLabel } from "@/components/ui/field";
import {
PasswordInput,
PasswordInputRoot,
} from "@/components/ui/password-input";
const PasswordInputWithField = () => (
<Field className="max-w-xs">
<FieldLabel>Account password</FieldLabel>
<PasswordInputRoot>
<PasswordInput placeholder="Enter your password" />
</PasswordInputRoot>
<FieldDescription>
Field label and helper text around the password input.
</FieldDescription>
</Field>
);
export default PasswordInputWithField;
Ignore password managers
import {
PasswordInput,
PasswordInputLabel,
PasswordInputRoot,
} from "@/components/ui/password-input";
const PasswordInputIgnoreManagers = () => (
<div className="w-full max-w-xs">
<PasswordInputRoot ignorePasswordManagers>
<PasswordInputLabel>API key</PasswordInputLabel>
<PasswordInput placeholder="sk-..." />
</PasswordInputRoot>
</div>
);
export default PasswordInputIgnoreManagers;
Strength meter
Type to measure strength.
import { useMemo, useState } from "react";
import {
PasswordInput,
PasswordInputLabel,
PasswordInputRoot,
} from "@/components/ui/password-input";
import { cn } from "@/lib/utils";
type StrengthLabel = "Weak" | "Fair" | "Good" | "Strong";
const STRENGTH_BAR = [
"bg-destructive/80",
"bg-orange-500/90",
"bg-amber-400/90",
"bg-emerald-600/90",
] as const;
const getStrength = (value: string): { id: number; value: StrengthLabel } => {
if (!value) {
return { id: 0, value: "Weak" };
}
let score = 0;
if (value.length >= 8) score += 1;
if (/[A-Z]/.test(value)) score += 1;
if (/\d/.test(value)) score += 1;
if (/[^A-Za-z0-9]/.test(value)) score += 1;
const id = Math.max(0, Math.min(3, score - 1));
const labels: StrengthLabel[] = ["Weak", "Fair", "Good", "Strong"];
return { id, value: labels[id] };
};
const PasswordInputStrengthMeter = () => {
const [value, setValue] = useState("");
const result = useMemo(() => getStrength(value), [value]);
const widthPct =
value.length === 0 ? 0 : Math.min(100, ((result.id + 1) / 4) * 100);
return (
<div className="flex w-full max-w-xs flex-col gap-2">
<PasswordInputRoot>
<PasswordInputLabel>Password</PasswordInputLabel>
<PasswordInput
onChange={(event) => setValue(event.currentTarget.value)}
value={value}
/>
</PasswordInputRoot>
<div className="space-y-1">
<div className="h-1.5 w-full overflow-hidden rounded-full bg-muted">
<div
className={cn(
"h-full transition-[width] duration-200",
STRENGTH_BAR[result.id] ?? STRENGTH_BAR[0],
)}
style={{ width: `${widthPct}%` }}
/>
</div>
<p className="text-muted-foreground text-xs">
{value.length === 0 ? (
"Type to measure strength."
) : (
<>
Strength:{" "}
<span className="font-medium text-foreground">
{result.value}
</span>
</>
)}
</p>
</div>
</div>
);
};
export default PasswordInputStrengthMeter;
Validation
Enter 8+ characters (invalid while too short).
import { useState } from "react";
import {
PasswordInput,
PasswordInputLabel,
PasswordInputRoot,
} from "@/components/ui/password-input";
const PasswordInputValidation = () => {
const [value, setValue] = useState("");
const invalid = value.length > 0 && value.length < 8;
return (
<div className="w-full max-w-xs">
<PasswordInputRoot invalid={invalid}>
<PasswordInputLabel>Password</PasswordInputLabel>
<PasswordInput
onChange={(event) => setValue(event.currentTarget.value)}
value={value}
/>
</PasswordInputRoot>
{invalid ? (
<p className="mt-1.5 text-destructive text-xs">
Use at least 8 characters.
</p>
) : (
<p className="mt-1.5 text-muted-foreground text-xs">
Enter 8+ characters (invalid while too short).
</p>
)}
</div>
);
};
export default PasswordInputValidation;
Context
Context: password is masked
import {
PasswordInput,
PasswordInputContext,
PasswordInputLabel,
PasswordInputRoot,
} from "@/components/ui/password-input";
const PasswordInputContextDemo = () => (
<PasswordInputRoot className="max-w-xs">
<PasswordInputContext>
{(context) => (
<p className="text-muted-foreground text-xs">
Context: password is{" "}
<span className="font-medium text-foreground">
{context.visible ? "shown" : "masked"}
</span>
</p>
)}
</PasswordInputContext>
<PasswordInputLabel>Password</PasswordInputLabel>
<PasswordInput placeholder="Try the eye toggle" />
</PasswordInputRoot>
);
export default PasswordInputContextDemo;
Translations
import {
PasswordInput,
PasswordInputLabel,
PasswordInputRoot,
} from "@/components/ui/password-input";
const PasswordInputTranslations = () => (
<div className="w-full max-w-xs">
<PasswordInputRoot
translations={{
visibilityTrigger: (visible) =>
visible ? "Hide password" : "Show password",
}}
>
<PasswordInputLabel>Password</PasswordInputLabel>
<PasswordInput />
</PasswordInputRoot>
</div>
);
export default PasswordInputTranslations;
API reference
This component mirrors the upstream Ark UI primitive. All props and DOM behavior are defined by Ark unless you see an ark-cn-only row below.
PasswordInput
| Prop | Type | Description |
|---|---|---|
| startAddon? | ReactNode | Slot before the input. |
| endAddon? | ReactNode | Slot after the input. |
| variant? | ButtonVariant | Button style for the visibility toggle (default ghost). |
| size? | ButtonSize | Button size for the visibility toggle (default icon-xs). |
| hideVisibilityToggle? | boolean | Removes the eye toggle from the end addon. |
See the ARK UI documentation for the full API.