shadimage
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 · light

Registry 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: {} } });
};

More from paceui

Browse all components in this library →