Password field

A password, with a button that shows and hides it.

At least 12 characters.

Installation

npx shadcn@latest add @formdef/password-field

Register it under widgets in lib/formdef/index.tsx, as widgets: { password: PasswordField }. The widget serves string values.

Usage

export const  = createForm({
  : .({ : .().(1, 'Enter your password') }),
  : { : '' },
  fields: {
    password: {
      : 'password',
      : {
        : 'Password',
        : 'current-password',
        : <a href="/reset-password">Forgot password?</a>,
        : { : 'Passwort anzeigen', : 'Passwort verbergen' },
      },
    },
  },
})

The button beside the input shows the password as plain text and hides it again. Its accessible name is Show password or Hide password; reveal renames both, for instance into the language of your form.

Set autoComplete to current-password on a sign-in form and to new-password where someone chooses one, so a password manager knows whether to fill in or to suggest.

Props

Prop

Type

Source

'use client'

import { type FieldOf, fieldParts } from '@formdef/core'
import { EyeIcon, EyeOffIcon } from 'lucide-react'
import { type ComponentProps, useState } from 'react'

import { FieldFrame } from '@/components/formdef/field-frame'
import {
  InputGroup,
  InputGroupAddon,
  InputGroupButton,
  InputGroupInput,
} from '@/components/ui/input-group'
import type { FieldLabels } from '@/lib/formdef/field-labels'

type Attrs = Pick<
  ComponentProps<'input'>,
  | 'className'
  | 'placeholder'
  | 'autoComplete'
  | 'autoFocus'
  | 'disabled'
  | 'readOnly'
  | 'required'
  | 'maxLength'
>

type PasswordFieldProps = FieldOf<string> &
  FieldLabels &
  Attrs & {
    reveal?: { show?: string | undefined; hide?: string | undefined } | undefined
  }

const SHOW_PASSWORD = 'Show password'
const HIDE_PASSWORD = 'Hide password'

export const PasswordField = ({
  field,
  label,
  labelAside,
  description,
  reveal,
  ...attrs
}: PasswordFieldProps) => {
  const [isVisible, setIsVisible] = useState(false)
  const parts = fieldParts(field, { description })
  const value = field.state.value ?? ''
  const toggleLabel = isVisible ? (reveal?.hide ?? HIDE_PASSWORD) : (reveal?.show ?? SHOW_PASSWORD)

  return (
    <FieldFrame
      parts={parts}
      label={label}
      labelAside={labelAside}
      description={description}
      disabled={attrs.disabled}
    >
      <InputGroup>
        <InputGroupInput
          {...attrs}
          {...parts.control}
          type={isVisible ? 'text' : 'password'}
          spellCheck={false}
          autoCapitalize="none"
          value={value}
          onChange={(event) => field.handleChange(event.target.value)}
        />
        <InputGroupAddon align="inline-end">
          <InputGroupButton
            size="icon-xs"
            disabled={attrs.disabled}
            aria-label={toggleLabel}
            onClick={() => setIsVisible((wasVisible) => !wasVisible)}
          >
            {isVisible ? <EyeOffIcon /> : <EyeIcon />}
          </InputGroupButton>
        </InputGroupAddon>
      </InputGroup>
    </FieldFrame>
  )
}