Skip to content
  1. Inputs
  2. Password input

Password input

A password field with a button to reveal what was typed. Everything else is the Input.

import { Field, Label } from '@/components/fieldset'
import { PasswordInput } from '@/components/password-input'

export default function Example() {
  return (
    <Field>
      <Label>Password</Label>
      <PasswordInput name="password" autoComplete="current-password" />
    </Field>
  )
}
<script setup lang="ts">
import Field from '@/components/field.vue'
import Label from '@/components/label.vue'
import PasswordInput from '@/components/password-input.vue'
</script>

<template>
  <Field>
    <Label>Password</Label>
    <PasswordInput name="password" autocomplete="current-password" />
  </Field>
</template>

Installation

Copy these files into the folder where you keep your components. They import each other with relative paths, so the folder can live anywhere. The packages they rely on are listed on the Installation page.

'use client'

import { EyeIcon, EyeSlashIcon } from '@heroicons/react/16/solid'
import type React from 'react'
import { useState } from 'react'
import { Input, InputGroup } from './input'

export function PasswordInput({
  className,
  ...props
}: Omit<React.ComponentPropsWithoutRef<typeof Input>, 'type'>) {
  let [visible, setVisible] = useState(false)

  return (
    <InputGroup className={className}>
      <Input {...props} type={visible ? 'text' : 'password'} />
      <button
        type="button"
        data-slot="icon"
        aria-label="Show password"
        aria-pressed={visible}
        className="pointer-events-auto! cursor-pointer rounded-sm hover:text-zinc-950! focus:outline-hidden focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent-500 dark:hover:text-white!"
        onClick={() => setVisible((value) => !value)}
      >
        <span className="absolute top-1/2 left-1/2 size-[max(100%,3rem)] -translate-1/2 pointer-fine:hidden" aria-hidden="true" />
        {visible ? <EyeSlashIcon className="size-full" /> : <EyeIcon className="size-full" />}
      </button>
    </InputGroup>
  )
}

Also copy Input.

<script setup lang="ts">
import { EyeIcon, EyeSlashIcon } from '@heroicons/vue/16/solid'
import { computed, ref, useAttrs } from 'vue'
import Input from './input.vue'
import InputGroup from './input-group.vue'

defineOptions({ inheritAttrs: false })

const attrs = useAttrs()
const inputAttrs = computed(() => {
  const { class: _class, ...rest } = attrs
  return rest
})

const visible = ref(false)
</script>

<template>
  <InputGroup :class="attrs.class">
    <Input v-bind="inputAttrs" :type="visible ? 'text' : 'password'" />
    <button
      type="button"
      data-slot="icon"
      aria-label="Show password"
      :aria-pressed="visible"
      class="pointer-events-auto! cursor-pointer rounded-sm hover:text-zinc-950! focus:outline-hidden focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent-500 dark:hover:text-white!"
      @click="visible = !visible"
    >
      <span class="absolute top-1/2 left-1/2 size-[max(100%,3rem)] -translate-1/2 pointer-fine:hidden" aria-hidden="true" />
      <EyeSlashIcon v-if="visible" class="size-full" />
      <EyeIcon v-else class="size-full" />
    </button>
  </InputGroup>
</template>

Also copy Input.

Component API

PropDefaultDescription
PasswordInputrenders a <Input> element
name-Name of the field in the form.
autoComplete-Say `current-password` or `new-password`, so password managers fill the right field.
placeholder-Hint shown while the field is empty.
invalid-Marks the field as invalid.
disabled-Disables the field.
className-Lands on the group around the field, for margins. Every other prop of `Input` is forwarded.
PropDefaultDescription
PasswordInputrenders a <Input> element
name-Name of the field in the form.
autocomplete-Say `current-password` or `new-password`, so password managers fill the right field.
placeholder-Hint shown while the field is empty.
invalid-Marks the field as invalid.
disabled-Disables the field.
v-model-Bound value, forwarded to the Input.
class-Lands on the group around the field, for margins. Every other attribute of `Input` is forwarded.

Examples

Basic example

Use it inside a Field with a Label, like the Input. The eye at the end switches the field between hidden and plain text, and says so to assistive tech with aria-pressed:

import { Field, Label } from '@/components/fieldset'
import { PasswordInput } from '@/components/password-input'

export default function Example() {
  return (
    <Field>
      <Label>Password</Label>
      <PasswordInput name="password" autoComplete="current-password" />
    </Field>
  )
}
<script setup lang="ts">
import Field from '@/components/field.vue'
import Label from '@/components/label.vue'
import PasswordInput from '@/components/password-input.vue'
</script>

<template>
  <Field>
    <Label>Password</Label>
    <PasswordInput name="password" autocomplete="current-password" />
  </Field>
</template>

Choosing a password

On a sign-up form, tell the browser the field takes a new-password so it offers to generate one, and put the rule in the placeholder:

<Field>
  <Label>Password</Label>
  <PasswordInput name="password" placeholder="Minimum 12 characters" autoComplete="new-password" />
</Field>
<Field>
  <Label>Password</Label>
  <PasswordInput name="password" placeholder="Minimum 12 characters" autocomplete="new-password" />
</Field>

Changing a password

Each field reveals on its own. The current password is a current-password, the two others are new-password:

<FieldGroup>
  <Field>
    <Label>Current password</Label>
    <PasswordInput name="current_password" autoComplete="current-password" />
  </Field>
  <Field>
    <Label>New password</Label>
    <PasswordInput name="password" autoComplete="new-password" />
  </Field>
  <Field>
    <Label>Confirm new password</Label>
    <PasswordInput name="password_confirmation" autoComplete="new-password" />
  </Field>
</FieldGroup>
<FieldGroup>
  <Field>
    <Label>Current password</Label>
    <PasswordInput name="current_password" autocomplete="current-password" />
  </Field>
  <Field>
    <Label>New password</Label>
    <PasswordInput name="password" autocomplete="new-password" />
  </Field>
  <Field>
    <Label>Confirm new password</Label>
    <PasswordInput name="password_confirmation" autocomplete="new-password" />
  </Field>
</FieldGroup>

Validation

Pass invalid and an ErrorMessage, exactly as with the Input:

That password is not right.

<Field>
  <Label>Password</Label>
  <PasswordInput name="password" defaultValue="hunter2" invalid />
  <ErrorMessage>That password is not right.</ErrorMessage>
</Field>
<Field>
  <Label>Password</Label>
  <PasswordInput name="password" model-value="hunter2" invalid />
  <ErrorMessage>That password is not right.</ErrorMessage>
</Field>