# Password Input (/docs/password-input)





`PasswordInput` is a native password field with a built-in reveal toggle. It renders an `<input type="password">` inside the same shell as [`Input`](/docs/input) and adds an inline-end button that flips the field between masked and visible. Like the rest of the family it carries its own `label`, `description`, `error`, and `required` — no external wrapper needed — and passing `error` flips it to the invalid (red) state. Omit them and it still composes cleanly inside a [`Field`](/docs/field).

```tsx
import { PasswordInput } from '@42-central/ui-react/password-input';

<PasswordInput name="password" placeholder="Enter a password" />
```

It is an **event-based** control, like `Input`: `value`, `onChange`, `name`, and `ref` forward to the underlying `<input>`, so React Hook Form's `register` and a plain `onChange(event)` handler both work as usual. The show / hide state is owned internally by default and never leaks into the form value; lift it out with `visible` when something else needs to drive it.

## Playground [#playground]

<story.Playground />

## Variants [#variants]

Two shells. `default` is bordered; `unstyled` strips the chrome but keeps the layout (height + the reveal toggle).

<Preview align="block" code="&#x22;<PasswordInput variant=\&#x22;default\&#x22; placeholder=\&#x22;Default\&#x22; />\n<PasswordInput variant=\&#x22;unstyled\&#x22; placeholder=\&#x22;Unstyled\&#x22; />&#x22;">
  <div className="flex w-full max-w-[20rem] flex-col gap-4">
    <PasswordInput variant="default" placeholder="Default" />

    <PasswordInput variant="unstyled" placeholder="Unstyled" />
  </div>
</Preview>

## Sizes [#sizes]

Five steps — `xs` through `xl`. The control font, padding, and the toggle icon scale to match `Input`.

<Preview align="block" code="&#x22;<PasswordInput size=\&#x22;xs\&#x22; placeholder=\&#x22;xs\&#x22; />\n<PasswordInput size=\&#x22;sm\&#x22; placeholder=\&#x22;sm\&#x22; />\n<PasswordInput size=\&#x22;md\&#x22; placeholder=\&#x22;md\&#x22; />\n<PasswordInput size=\&#x22;lg\&#x22; placeholder=\&#x22;lg\&#x22; />\n<PasswordInput size=\&#x22;xl\&#x22; placeholder=\&#x22;xl\&#x22; />&#x22;">
  <div className="flex w-full max-w-[20rem] flex-col gap-3">
    <PasswordInput size="xs" placeholder="xs" />

    <PasswordInput size="sm" placeholder="sm" />

    <PasswordInput size="md" placeholder="md" />

    <PasswordInput size="lg" placeholder="lg" />

    <PasswordInput size="xl" placeholder="xl" />
  </div>
</Preview>

## States [#states]

`invalid` swaps the border and focus ring to red regardless of `color` (and sets `aria-invalid`). `disabled` dims the shell and disables both the input and the toggle; `readOnly` keeps full contrast and stays focusable but rejects edits.

<Preview align="block" code="&#x22;<PasswordInput label=\&#x22;Invalid\&#x22; invalid defaultValue=\&#x22;weak\&#x22; />\n<PasswordInput label=\&#x22;Disabled\&#x22; disabled defaultValue=\&#x22;secret\&#x22; />\n<PasswordInput label=\&#x22;Read-only\&#x22; readOnly defaultValue=\&#x22;secret\&#x22; />&#x22;">
  <div className="flex w-full max-w-[20rem] flex-col gap-4">
    <PasswordInput label="Invalid" defaultValue="weak" />

    <PasswordInput label="Disabled" defaultValue="secret" />

    <PasswordInput label="Read-only" defaultValue="secret" />
  </div>
</Preview>

## With label, description & error [#with-label-description--error]

`label`, `description` (helper text), and `error` are built in via [`Field`](/docs/field) — no external wrapper needed. Passing `error` flips the shell to the invalid (red) state; `required` adds the semantic flag and a red asterisk on the label.

<Preview align="block" code="&#x22;<PasswordInput label=\&#x22;Password\&#x22; description=\&#x22;At least 8 characters.\&#x22; placeholder=\&#x22;Enter a password\&#x22; />\n\n<PasswordInput label=\&#x22;Password\&#x22; required placeholder=\&#x22;Enter a password\&#x22; />\n\n<PasswordInput label=\&#x22;Password\&#x22; error=\&#x22;Password is too weak\&#x22; placeholder=\&#x22;Enter a password\&#x22; />&#x22;">
  <div className="flex w-full max-w-[20rem] flex-col gap-5">
    <PasswordInput label="Password" description="At least 8 characters." placeholder="Enter a password" />

    <PasswordInput label="Password" placeholder="Enter a password" />

    <PasswordInput label="Password" error="Password is too weak" placeholder="Enter a password" />
  </div>
</Preview>

If you omit these props, `PasswordInput` still composes cleanly inside a [`Field`](/docs/field): `disabled` / `invalid` / `readOnly` / `required` propagate from the `Field`, and the `aria-*` relationships are wired for you.

```tsx
import { Field } from '@42-central/ui-react/field';
import { PasswordInput } from '@42-central/ui-react/password-input';

<Field label="Password" description="At least 8 characters." required>
  <PasswordInput name="password" placeholder="Enter a password" />
</Field>
```

## Controlled visibility [#controlled-visibility]

`visible` / `defaultVisible` / `onVisibleChange` lift the reveal state out of the field. A common case is one "Show passwords" checkbox for a password and its confirmation: drive both from shared state, and pass `hideToggle` so the per-field buttons don't fight the checkbox.

<Preview align="block" code="&#x22;const [visible, setVisible] = useState(false)\n\n<PasswordInput label=\&#x22;Password\&#x22; visible={visible} hideToggle />\n<PasswordInput label=\&#x22;Confirm password\&#x22; visible={visible} hideToggle />\n<Checkbox\n  label=\&#x22;Show passwords\&#x22;\n  checked={visible}\n  onCheckedChange={(checked) => setVisible(checked === true)}\n/>&#x22;">
  <PasswordInputSharedVisibilityExample />
</Preview>

Keep the built-in toggle and pass `onVisibleChange` instead when you only need to observe or veto a press. It receives the next state as a plain boolean, and a controlled field changes only once you feed that back into `visible`.

## Custom toggle [#custom-toggle]

`renderToggle` is called with the current state and renders *inside* the kit's button, so the `aria-controls` / `aria-label` wiring and the keyboard handling stay intact while you swap the icon. `hideToggle` renders no button at all.

<Preview align="block" code="&#x22;<PasswordInput\n  label=\&#x22;Password\&#x22;\n  defaultValue=\&#x22;secret\&#x22;\n  renderToggle={(visible) => (visible ? <LockKeyholeOpen /> : <LockKeyhole />)}\n/>&#x22;">
  <PasswordInputCustomToggleExample />
</Preview>

The button's accessible name still comes from [`translations`](#translations), never from what `renderToggle` renders. An icon has no name of its own, so that is what you want. **Visible text needs `translations` too**: without it the button keeps the English "Show password", which overrides your "Afficher". That fails [WCAG 2.5.3 Label in Name](https://www.w3.org/WAI/WCAG22/Understanding/label-in-name.html), because a voice-control user who says "click Afficher" hits nothing. Return a label that contains the visible text. In development the kit warns in the console when it doesn't.

<Preview align="block" code="&#x22;<PasswordInput\n  label=\&#x22;Mot de passe\&#x22;\n  defaultValue=\&#x22;secret\&#x22;\n  renderToggle={(visible) => (visible ? 'Masquer' : 'Afficher')}\n  translations={{\n    visibilityTrigger: (visible) =>\n      visible ? 'Masquer le mot de passe' : 'Afficher le mot de passe',\n  }}\n/>&#x22;">
  <PasswordInputTextToggleExample />
</Preview>

## Translations [#translations]

The button's accessible label comes from `translations.visibilityTrigger`, called with the current state. It is the only text the component renders on its own. Keep it state-dependent, since the label is what tells AT whether the value is shown, and make it contain any text a custom `renderToggle` shows.

<Preview align="block" code="&#x22;<PasswordInput\n  label=\&#x22;Mot de passe\&#x22;\n  translations={{\n    visibilityTrigger: (visible) =>\n      visible ? 'Masquer le mot de passe' : 'Afficher le mot de passe',\n  }}\n/>&#x22;">
  <PasswordInputTranslationsExample />
</Preview>

## API [#api]

<TypeTable
  type="{
  value: {
    type: 'string',
    description: 'Controlled value, forwarded to the native input.',
  },
  defaultValue: {
    type: 'string',
    description: 'Uncontrolled initial value.',
  },
  onChange: {
    type: '(e: ChangeEvent<HTMLInputElement>) => void',
    description: 'Native change event, forwarded straight to the input.',
  },
  visible: {
    type: 'boolean',
    description: 'Controlled reveal state: true shows the value as plain text.',
  },
  defaultVisible: {
    type: 'boolean',
    default: 'false',
    description: 'Uncontrolled initial reveal state.',
  },
  onVisibleChange: {
    type: '(visible: boolean) => void',
    description: 'Fires with the next reveal state when the toggle is pressed.',
  },
  renderToggle: {
    type: '(visible: boolean) => ReactNode',
    description: &#x22;Content of the reveal button, called with the current state. The button and its a11y wiring stay the kit's; its name still comes from translations, so visible text needs a matching label.&#x22;,
  },
  hideToggle: {
    type: 'boolean',
    default: 'false',
    description: 'Renders no reveal button; drive visible from your own control. renderToggle is ignored while set.',
  },
  toggleTabIndex: {
    type: 'number',
    default: '0',
    description: 'tabIndex of the reveal button. -1 takes it out of the tab order (pointer-only).',
  },
  translations: {
    type: '{ visibilityTrigger?: (visible: boolean) => string }',
    description: &#x22;Localized accessible label of the reveal button. Also names a custom renderToggle, so it must contain any text that shows.&#x22;,
  },
  variant: {
    type: '&#x22;default&#x22; | &#x22;unstyled&#x22;',
    default: '&#x22;default&#x22;',
    description: 'Shell treatment: bordered or chrome-less.',
  },
  size: {
    type: '&#x22;xs&#x22; | &#x22;sm&#x22; | &#x22;md&#x22; | &#x22;lg&#x22; | &#x22;xl&#x22;',
    default: '&#x22;md&#x22;',
    description: 'Height step, control font, and toggle icon size.',
  },
  color: {
    type: 'Color',
    default: '&#x22;brand&#x22;',
    description: 'Focus-ring accent. Any kit palette or a consumer-defined one.',
  },
  invalid: {
    type: 'boolean',
    default: 'false',
    description: 'Red border + ring; sets aria-invalid on the input.',
  },
  disabled: {
    type: 'boolean',
    default: 'false',
    description: 'Dims the shell and disables the input + toggle.',
  },
  readOnly: {
    type: 'boolean',
    default: 'false',
    description: 'Focusable and full-contrast, but the value cannot change.',
  },
  required: {
    type: 'boolean',
    default: 'false',
    description: 'Required flag for native validation + asterisk on the label.',
  },
  placeholder: {
    type: 'string',
    description: 'Placeholder shown while the field is empty.',
  },
  label: {
    type: 'ReactNode',
    description: 'Field label above the control.',
  },
  description: {
    type: 'ReactNode',
    description: 'Field helper text below the control.',
  },
  error: {
    type: 'ReactNode',
    description: 'Field error; sets the invalid (red) state.',
  },
  startSlot: {
    type: 'ReactNode',
    description: 'Content at the inline-start (icon, prefix). RTL-aware.',
  },
  classNames: {
    type: '{ root?, input?, visibilityTrigger?, field? }',
    description: &#x22;Per-part class overrides. visibilityTrigger lands on the reveal button, merged after the kit's classes so it wins.&#x22;,
  },
  testId: {
    type: 'string',
    description: 'data-testid on the real <input>, not the shell wrapper. See the Testing guide.',
  },
}"
/>

`name`, `placeholder`, `id`, `ref`, and `onBlur` forward to the underlying `<input>`. `className` styles the shell root. `testId` lands on that same `<input>` — see the [Testing](/docs/testing) guide.

## Accessibility [#accessibility]

* Pass `label` / `description` / `error` to wire the accessible name and `aria-*` relationships via the built-in [`Field`](/docs/field). Omit them and it composes inside an outer `Field` (or your own `<label htmlFor>`) instead.
* The reveal toggle is a real `<button>` with `aria-controls` pointing at the input. It is exposed to AT rather than nested in an `aria-hidden` decoration.
* Its state lives in its label, which flips between "Show password" and "Hide password" (see [Translations](#translations)). It carries no `aria-pressed` or `aria-expanded`: revealing a value doesn't expand anything, and a pressed state on top of a label that already changes would read "Hide password, pressed". If you localize it, keep the label state-dependent.
* The toggle is a tab stop right after the input, and Enter / Space toggle it like a click, keeping focus on the button. Pass `toggleTabIndex={-1}` to take it out of the tab order, for a form where keyboard users should go straight from field to field; it stays clickable.
* A custom `renderToggle` renders inside that same button, so it keeps those attributes, including the `aria-label`, which overrides the content. If the content is visible text, pass `translations` whose label contains it (WCAG 2.5.3). With `hideToggle`, the control you replace it with owns the accessible name. A labelled checkbox works out of the box.
* `invalid` sets `aria-invalid` on the input.
