paceui
componentform-passwordsource: ok · hit
Form Password
A password input field with visibility toggle and validation.
npx shadcn add "https://paceui.com/r/form-password.json"free
No selection tags yet — empty means no evidence, not lack of support.
Dependencies
Bundle weight · lightRegistry dependencies
buttonfieldinput
Files4 · source cached 10/8/2026, 3:38:34 PM
components/ui/forms/form-password.tsx
import { type AnyFormApi } from "@tanstack/react-form";
import { useState } from "react";
import { EyeIcon, EyeOffIcon, LockIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { FormInput, type FormInputProps } from "@/components/ui/forms/form-input";
type FormPasswordInputProps<TForm extends AnyFormApi> = FormInputProps<TForm> & {};
export const FormPassword = <TForm extends AnyFormApi>({ prefix, suffix, ...rest }: FormPasswordInputProps<TForm>) => {
const [show, setShow] = useState(false);
return (
<FormInput
{...rest}
type={show ? "text" : "password"}
prefix={prefix ?? <LockIcon className="text-muted-foreground size-4.5" />}
suffix={
suffix ?? (
<Button
aria-label="Show Password"
type="button"
variant="ghost"
size="icon-sm"
className="text-muted-foreground"
onClick={() => setShow(!show)}>
{show ? <EyeIcon className="size-4.5" /> : <EyeOffIcon className="size-4.5" />}
</Button>
)
}
/>
);
};
components/ui/forms/form-input.tsx
import { type AnyFormApi, type DeepKeys, useField } from "@tanstack/react-form";
import type { ComponentProps, ReactNode } from "react";
import { cn } from "@/lib/utils";
import { Field, FieldLabel } from "@/components/ui/field";
import { FormError } from "@/components/ui/forms/form-error";
import { type GetFormData } from "@/components/ui/forms/helpers";
import { Input } from "@/components/ui/input";
export type FormInputProps<TForm extends AnyFormApi> = Omit<ComponentProps<"input">, "name" | "form" | "prefix"> & {
label?: ReactNode;
form: TForm;
name: DeepKeys<GetFormData<TForm>>;
prefix?: ReactNode;
suffix?: ReactNode;
};
export const FormInput = <TForm extends AnyFormApi>({
label,
form,
name,
className,
prefix,
suffix,
...rest
}: FormInputProps<TForm>) => {
const field = useField({
form,
name,
});
const errors = field.state.meta.errors[0];
const error = typeof errors === "object" ? errors.message : errors;
return (
<Field data-invalid={error != undefined} className="gap-1.5">
{label && <FieldLabel htmlFor={field.name}>{label}</FieldLabel>}
<div className="relative">
{prefix && <div className="absolute inset-s-3 top-1/2 -translate-y-1/2">{prefix}</div>}
<Input
id={field.name}
className={cn({ "ps-10": prefix, "pe-10": suffix }, className)}
name={field.name}
value={field.state.value ?? ""}
onBlur={field.handleBlur}
onChange={(e) => {
const val = e.target.value;
field.handleChange(rest.type === "number" ? Number(val) : val);
}}
aria-invalid={error != undefined}
{...rest}
/>
{suffix && <div className="absolute inset-e-3 top-1/2 -translate-y-1/2">{suffix}</div>}
</div>
<FormError form={form} name={name} className="mt-0.5" />
</Field>
);
};
components/ui/forms/form-error.tsx
import { type AnyFormApi, type DeepKeys, useField } from "@tanstack/react-form";
import type { InputHTMLAttributes } from "react";
import { cn } from "@/lib/utils";
import { type GetFormData } from "@/components/ui/forms/helpers";
export type FormErrorProps<TForm extends AnyFormApi> = Omit<
InputHTMLAttributes<HTMLParagraphElement>,
"name" | "form"
> & {
form: TForm;
name: DeepKeys<GetFormData<TForm>>;
};
export const FormError = <TForm extends AnyFormApi>({ form, name = "", className, ...rest }: FormErrorProps<TForm>) => {
const field = useField({
form,
name,
});
const errors = field.state.meta.errors[0];
const error = typeof errors === "object" ? errors.message : errors;
return (
<>
{error && (
<p {...rest} className={cn("text-destructive text-sm", className)}>
{error}
</p>
)}
</>
);
};
components/ui/forms/helpers.ts
import { type AnyFormApi, type DeepKeys, type FormApi } from "@tanstack/react-form";
export type GetFormData<TForm> =
TForm extends FormApi<infer TData, any, any, any, any, any, any, any, any, any, any, any> ? TData : never;
export const setFieldError = <TForm extends AnyFormApi>(
form: TForm,
field: DeepKeys<GetFormData<TForm>>,
error: string,
) => {
form.setFieldMeta(field, (prev) => ({
...prev,
errorMap: {
...prev.errorMap,
onSubmit: error,
},
}));
};
export const clearFormErrors = <TForm extends AnyFormApi>(form: TForm) => {
form.setErrorMap({ onDynamic: { fields: {} } });
};