Skip to content

Instantly share code, notes, and snippets.

@mjbalcueva
Last active May 6, 2025 01:20
Show Gist options
  • Save mjbalcueva/b21f39a8787e558d4c536bf68e267398 to your computer and use it in GitHub Desktop.
Save mjbalcueva/b21f39a8787e558d4c536bf68e267398 to your computer and use it in GitHub Desktop.
shadcn ui custom password input
'use client'
import * as React from 'react'
import { EyeIcon, EyeOffIcon } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Input, type InputProps } from '@/components/ui/input'
import { cn } from '@/lib/utils'
const PasswordInput = React.forwardRef<HTMLInputElement, InputProps>(({ className, ...props }, ref) => {
const [showPassword, setShowPassword] = React.useState(false)
const disabled = props.value === '' || props.value === undefined || props.disabled
return (
<div className="relative">
<Input
type={showPassword ? 'text' : 'password'}
className={cn('hide-password-toggle pr-10', className)}
ref={ref}
{...props}
/>
<Button
type="button"
variant="ghost"
size="sm"
className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent"
onClick={() => setShowPassword((prev) => !prev)}
disabled={disabled}
>
{showPassword && !disabled ? (
<EyeIcon className="h-4 w-4" aria-hidden="true" />
) : (
<EyeOffIcon className="h-4 w-4" aria-hidden="true" />
)}
<span className="sr-only">{showPassword ? 'Hide password' : 'Show password'}</span>
</Button>
{/* hides browsers password toggles */}
<style>{`
.hide-password-toggle::-ms-reveal,
.hide-password-toggle::-ms-clear {
visibility: hidden;
pointer-events: none;
display: none;
}
`}</style>
</div>
)
})
PasswordInput.displayName = 'PasswordInput'
export { PasswordInput }
"use client"
import { useState } from "react"
import { PasswordInput } from "@/components/password-input"
import { Button } from "@/components/ui/button"
import { Label } from "@/components/ui/label"
const SampleUseCase = () => {
const [currentPassword, setCurrentPassword] = useState("")
const [password, setPassword] = useState("")
const [passwordConfirmation, setPasswordConfirmation] = useState("")
return (
<div className="space-y-4">
<div>
<Label htmlFor="current_password">Current Password</Label>
<PasswordInput
id="current_password"
value={currentPassword}
onChange={(e) => setCurrentPassword(e.target.value)}
autoComplete="current-password"
/>
</div>
<div>
<Label htmlFor="password">New Password</Label>
<PasswordInput
id="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
autoComplete="new-password"
/>
</div>
<div>
<Label htmlFor="password_confirmation">Confirm Password</Label>
<PasswordInput
id="password_confirmation"
value={passwordConfirmation}
onChange={(e) => setPasswordConfirmation(e.target.value)}
autoComplete="new-password"
/>
</div>
<Button type="submit">Save</Button>
</div>
)
}
export default SampleUseCase
@immdraselkhan
Copy link

immdraselkhan commented Jun 20, 2024

Has anyone tried to implement this with zod?

👉 Usage example

  const form = useForm<FieldKeysValues>({
    mode: "onBlur",
    resolver: zodResolver(passwordZodSchema),
    defaultValues: {
      password: "",
      password2: "",
    },
  });

👉 passwordZodSchema

export const passwordZodSchema = z
  .object({
    password: passwordSchema,
    password2: passwordSchema,
  })
  .refine(({ password, password2 }) => password === password2, {
    path: ["password2"],
    message: "Password didn't match.",
  });

👉 passwordSchema

export const passwordSchema = z
  .string({
    required_error: "Password can not be empty.",
  })
  .regex(/^.{8,20}$/, {
    message: "Minimum 8 and maximum 20 characters.",
  })
  .regex(/(?=.*[A-Z])/, {
    message: "At least one uppercase character.",
  })
  .regex(/(?=.*[a-z])/, {
    message: "At least one lowercase character.",
  })
  .regex(/(?=.*\d)/, {
    message: "At least one digit.",
  })
  .regex(/[$&+,:;=?@#|'<>.^*()%!-]/, {
    message: "At least one special character.",
  });

@JorgeCabDig
Copy link

Thanks! <3 Helped me a lot

@mirjalol-jabborov
Copy link

awesome man, thanks!

@jesseemana
Copy link

sweet

@Ruhtra
Copy link

Ruhtra commented Oct 7, 2024

I had a problem where the default input password Eye button appeared and was left with 2 reveal buttons

The solution that i found was to include the following code in my index.css, that removes the default button from the html input:

@layer utilities {
  /* Clean Eye button in input */
  input::-ms-reveal,
  input::-ms-clear {
    display: none;
  }
}

@AjayRathod67
Copy link

Thanks a lot

@kingtirano
Copy link

Has anyone tried to implement this with zod?

👉 Usage example

  const form = useForm<FieldKeysValues>({
    mode: "onBlur",
    resolver: zodResolver(passwordZodSchema),
    defaultValues: {
      password: "",
      password2: "",
    },
  });

👉 passwordZodSchema

export const passwordZodSchema = z
  .object({
    password: passwordSchema,
    password2: passwordSchema,
  })
  .refine(({ password, password2 }) => password === password2, {
    path: ["password2"],
    message: "Password didn't match.",
  });

👉 passwordSchema

export const passwordSchema = z
  .string({
    required_error: "Password can not be empty.",
  })
  .regex(/^.{8,20}$/, {
    message: "Minimum 8 and maximum 20 characters.",
  })
  .regex(/(?=.*[A-Z])/, {
    message: "At least one uppercase character.",
  })
  .regex(/(?=.*[a-z])/, {
    message: "At least one lowercase character.",
  })
  .regex(/(?=.*\d)/, {
    message: "At least one digit.",
  })
  .regex(/[$&+,:;=?@#|'<>.^*()%!-]/, {
    message: "At least one special character.",
  });

Yes, I was already using normal input with zod and I took this code to replace the input.

@kingtirano
Copy link

Thank You bro.

@ghazimuaz
Copy link

import { EyeOffIcon, EyeIcon } from "lucide-react";
import { useFormContext } from "react-hook-form";
import { Box } from "@/components/ui/box";
import {
  FormField,
  FormItem,
  FormControl,
  FormMessage,
  FormDescription,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { createElement, useState } from "react";

type PasswordFieldProps = {
  name?: string;
  placeholder?: string;
  description?: string | JSX.Element;
};

export function PasswordField({
  name = "password",
  placeholder = "Enter password",
  description,
}: PasswordFieldProps) {
  const { control, getFieldState } = useFormContext();
  const [passwordVisibility, setPasswordVisibility] = useState(false);

  return (
    <FormField
      control={control}
      name={name}
      render={({ field }) => (
        <FormItem>
          <FormControl>
            <Box className="relative">
              <Input
                {...field}
                type={passwordVisibility ? "text" : "password"}
                autoComplete="on"
                placeholder={placeholder}
                className={`pr-12 ${getFieldState(name).error && "text-destructive"}`}
              />
              <Box
                className="absolute inset-y-0 right-0 flex cursor-pointer items-center p-3 text-muted-foreground"
                onClick={() => setPasswordVisibility(!passwordVisibility)}
              >
                {createElement(passwordVisibility ? EyeOffIcon : EyeIcon, {
                  className: "h-6 w-6",
                })}
              </Box>
            </Box>
          </FormControl>
          <FormMessage />
          {description && <FormDescription>{description}</FormDescription>}
        </FormItem>
      )}
    />
  );
}

This is another approach with better control. Make sure to wrap the form using FormProvider.

Usage example:

<PasswordField
  // description={<Link href="reset">Forgot your password?</Link>}
  description={"Forgot your password?"}
/>

Thank you for this, helped me very much

@fillsanches
Copy link

For those who received a warning Module '"@/components/ui/input"' has no exported member 'InputProps'.

Just edit src/components/ui/input.tsx, so add the section commented below:

import * as React from "react";

import { cn } from "@/lib/utils";
//BEGIN - ADD THIS PART

export interface InputProps
  extends React.InputHTMLAttributes<HTMLInputElement> {}

//END - ADD THIS PART
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
  ({ className, type, ...props }, ref) => {
    return (
      <input
        type={type}
        className={cn(
          "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
          className
        )}
        ref={ref}
        {...props}
      />
    );
  }
);
Input.displayName = "Input";

export { Input };

@d-pamneja
Copy link

Thanks a lot mate. Great work!!

@omotsuebe
Copy link

Using zod here is how I implemented mine.

'use client';  
  
import React, { useState, forwardRef } from 'react';  
import { EyeIcon, EyeOffIcon } from 'lucide-react';  
  
import { Button } from '@/components/ui/button';  
import { Input, type InputProps } from '@/components/ui/input';  
import { cn } from '@/lib/utils';  
  
const PasswordInput = forwardRef<HTMLInputElement, InputProps>(  
    ({ className, ...props }, ref) => {  
        const [showPassword, setShowPassword] = useState(false);  
  
        return (  
            <div className="relative">  
 <Input  type={showPassword ? 'text' : 'password'}  
                    className={cn('hide-password-toggle pr-10', className)}  
                    ref={ref}  
                    {...props}  
                />  
 <Button  type="button"  
  variant="ghost"  
  size="sm"  
  className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent"  
  onClick={() => setShowPassword((prev) => !prev)}  
                >  
  {showPassword ? (  
                        <EyeOffIcon className="h-4 w-4" aria-hidden="true" />) : (  
                        <EyeIcon className="h-4 w-4" aria-hidden="true" />  
  )}  
                    <span className="sr-only">{showPassword ? 'Hide password' : 'Show password'}</span>  
 </Button> <style>{`.hide-password-toggle::-ms-reveal,.hide-password-toggle::-ms-clear {visibility: hidden;pointer-events: none; display: none;}`}</style>  
 </div>  );  
    }  
);  
  
PasswordInput.displayName = 'PasswordInput';  
export { PasswordInput };

Usage

import { SignupFormSchema, SignupFormState } from '@/lib/userValidation';
const {  register,  handleSubmit,  formState: { errors }} = useForm<SignupFormState>({  
  resolver: zodResolver(SignupFormSchema),  
});
<div>  
 <Label htmlFor="password">Password</Label>  
 <PasswordInput id="password" placeholder="Enter your password" {...register("password")} />  
</div>

@joshpachner
Copy link

Thank you. I'll name my firstborn after you.
You are what gives me hope in humanity.

@IsoardiMarius
Copy link

thanks

@milinddhamu
Copy link

Shadcn components supposed to support ssr , so converting it directly to Client component doesnt make sense. I hope there's any better way to toggle.

@abustamam
Copy link

Shadcn components supposed to support ssr , so converting it directly to Client component doesnt make sense. I hope there's any better way to toggle.

Server components don't support hooks. So any UI, shad or otherwise, that requires interactivity, needs the use client directive.

https://react.dev/reference/rsc/server-components#adding-interactivity-to-server-components

@luislobo9b
Copy link

My Input component doesn't have InputProps, so I made this small change:

Before:

import { Input, type InputProps } from '@/components/ui/input'
const PasswordInput = React.forwardRef<HTMLInputElement, InputProps>(({ className, ...props }, ref) => {

After:

import { Input } from '@/components/ui/input'
const PasswordInput = React.forwardRef<HTMLInputElement, React.ComponentProps<typeof Input>>(({ className, ...props }, ref) => {

In other words, I simply replaced InputProps with React.ComponentProps<typeof Input>.

@abustamam
Copy link

My Input component doesn't have InputProps, so I made this small change:

Before:

import { Input, type InputProps } from '@/components/ui/input'
const PasswordInput = React.forwardRef<HTMLInputElement, InputProps>(({ className, ...props }, ref) => {

After:

import { Input } from '@/components/ui/input'
const PasswordInput = React.forwardRef<HTMLInputElement, React.ComponentProps<typeof Input>>(({ className, ...props }, ref) => {

In other words, I simply replaced InputProps with React.ComponentProps<typeof Input>.

That works; but you can also just export InputProps from the input file like @fillsanches recommended here

export interface InputProps
  extends React.InputHTMLAttributes<HTMLInputElement> {}

@luislobo9b
Copy link

@abustamam
Got it. It's better this way because if I want to add a customization, it will reflect on everyone using the Input, centralizing the typing. Thanks.

@fransachmadhw
Copy link

thank you, it works perfectly

@MatiasGOrtega
Copy link

Based on your ideas, I'm sharing my results with you. Sorry for the language in the code; I speak Spanish.
https://codesandbox.io/p/devbox/pruebas-con-tailwindcss-ngjr23

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment