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
| Prop | Default | Description |
|---|---|---|
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. |
| Prop | Default | Description |
|---|---|---|
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>