---
title: "Password input"
description: "A password field with a button that shows and hides what was typed, built on input group."
url: "https://susonwaiba.github.io/htmx-ui/docs/components/password-input"
section: "Components"
---

# Password input

A password field with a button that shows and hides what was typed, built on input group.

```html
<div class="field w-full max-w-sm">
  <label class="label" for="pw-demo">Password</label>
  <div class="input-group password-input" data-password-input>
    <input class="input" id="pw-demo" type="password" name="password" autocomplete="current-password" value="correct horse battery" />
    <span class="input-group-addon input-group-addon-inline-end">
      <button type="button" class="btn btn-ghost btn-icon password-toggle" data-password-toggle aria-label="Show password" aria-pressed="false">
        <span class="password-toggle-show"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4" aria-hidden="true"><path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0"/><circle cx="12" cy="12" r="3"/></svg></span>
        <span class="password-toggle-hide"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4" aria-hidden="true"><path d="M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49"/><path d="M14.084 14.158a3 3 0 0 1-4.242-4.242"/><path d="M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143"/><path d="m2 2 20 20"/></svg></span>
      </button>
    </span>
  </div>
</div>
```

## Markup

- Wrap a `type="password"` `.input` in an [input group](https://susonwaiba.github.io/htmx-ui/docs/components/input-group) with `.password-input` and `data-password-input`.
- Put the toggle in an `.input-group-addon-inline-end`: a `.btn.btn-ghost.btn-icon.password-toggle` with `data-password-toggle`, holding `.password-toggle-show` (the eye) and `.password-toggle-hide` (the eye crossed out).
- Give it `type="button"` so it never submits the form, and `aria-label="Show password"`.

The behaviour switches the input between `type="password"` and `type="text"` and sets `aria-pressed` on the button, which swaps the icons. The label stays "Show password": screen readers say "Show password, toggle button, pressed" once it's on, the pattern assistive technology expects. The caret and any selection stay where they were, and tapping the toggle while typing keeps focus (and the phone keyboard) in the input.

## Text toggle

Words instead of an icon: drop `.btn-icon` and write Show and Hide in the two spans. They share one space, so the button doesn't change width.

```html
<div class="field w-full max-w-sm">
  <label class="label" for="pw-text">Password</label>
  <div class="input-group password-input" data-password-input>
    <input class="input" id="pw-text" type="password" name="password" autocomplete="current-password" />
    <span class="input-group-addon input-group-addon-inline-end">
      <button type="button" class="btn btn-ghost password-toggle" data-password-toggle aria-label="Show password" aria-pressed="false">
        <span class="password-toggle-show">Show</span>
        <span class="password-toggle-hide">Hide</span>
      </button>
    </span>
  </div>
</div>
```

## Checkbox

A [checkbox](https://susonwaiba.github.io/htmx-ui/docs/components/checkbox) with `data-password-toggle` anywhere inside the wrapper works as the toggle too. Here one checkbox shows both fields of a new-password form:

```html
<div class="password-input flex w-full max-w-sm flex-col gap-4" data-password-input>
  <div class="field">
    <label class="label" for="pw-new">New password</label>
    <input class="input" id="pw-new" type="password" name="new-password" autocomplete="new-password" data-password />
  </div>
  <div class="field">
    <label class="label" for="pw-confirm">Confirm password</label>
    <input class="input" id="pw-confirm" type="password" name="confirm-password" autocomplete="new-password" data-password />
  </div>
  <div class="field field-horizontal">
    <input class="checkbox" type="checkbox" id="pw-show-both" data-password-toggle />
    <label class="field-label" for="pw-show-both">Show passwords</label>
  </div>
</div>
```

Every input with `data-password` (or `type="password"`) inside the wrapper is shown and hidden together.

## Caps Lock warning

Elements with `data-password-caps` in the field are shown only while Caps Lock is on as the user types in the password, and hidden again when it goes off or the input loses focus. Try it:

```html
    <div class="field w-full max-w-sm">
      <label class="label" for="pw-caps">Password</label>
      <div class="input-group password-input" data-password-input>
  <input class="input" type="password" name="password" id="pw-caps" autocomplete="current-password" autocapitalize="off" spellcheck="false" />
  <span class="input-group-addon input-group-addon-inline-end">
    <button type="button" class="btn btn-ghost btn-icon password-toggle" data-password-toggle aria-label="Show password" aria-pressed="false">
      <span class="password-toggle-show"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4" aria-hidden="true"><path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0"/><circle cx="12" cy="12" r="3"/></svg></span>
      <span class="password-toggle-hide"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4" aria-hidden="true"><path d="M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49"/><path d="M14.084 14.158a3 3 0 0 1-4.242-4.242"/><path d="M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143"/><path d="m2 2 20 20"/></svg></span>
    </button>
  </span>
</div>
<p class="field-warning" data-password-caps hidden><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4" aria-hidden="true"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"/><path d="M12 9v4M12 17h.01"/></svg> Caps Lock is on.</p>
    </div>
```

## States

As with any [input group](https://susonwaiba.github.io/htmx-ui/docs/components/input-group), `aria-invalid="true"` on the input turns the border red and `disabled` fades the whole field; a disabled input disables its toggle. Pair them with [field](https://susonwaiba.github.io/htmx-ui/docs/components/field) messages:

```html
    <div class="field w-full max-w-sm self-center">
      <label class="label" for="pw-invalid">Password</label>
      <div class="input-group password-input" data-password-input>
  <input class="input" type="password" name="password" id="pw-invalid" autocomplete="current-password" autocapitalize="off" spellcheck="false" value="hunter2" aria-invalid="true" aria-describedby="pw-invalid-error" />
  <span class="input-group-addon input-group-addon-inline-end">
    <button type="button" class="btn btn-ghost btn-icon password-toggle" data-password-toggle aria-label="Show password" aria-pressed="false">
      <span class="password-toggle-show"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4" aria-hidden="true"><path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0"/><circle cx="12" cy="12" r="3"/></svg></span>
      <span class="password-toggle-hide"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4" aria-hidden="true"><path d="M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49"/><path d="M14.084 14.158a3 3 0 0 1-4.242-4.242"/><path d="M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143"/><path d="m2 2 20 20"/></svg></span>
    </button>
  </span>
</div>

      <p class="field-error" id="pw-invalid-error"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4" aria-hidden="true"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6M9 9l6 6"/></svg> That password is not right.</p>
    </div>
    <div class="field w-full max-w-sm self-center">
      <label class="label" for="pw-new-help">New password</label>
      <div class="input-group password-input" data-password-input>
  <input class="input" type="password" name="new-password" id="pw-new-help" autocomplete="new-password" autocapitalize="off" spellcheck="false" minlength="12" aria-describedby="pw-new-help-text" />
  <span class="input-group-addon input-group-addon-inline-end">
    <button type="button" class="btn btn-ghost btn-icon password-toggle" data-password-toggle aria-label="Show password" aria-pressed="false">
      <span class="password-toggle-show"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4" aria-hidden="true"><path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0"/><circle cx="12" cy="12" r="3"/></svg></span>
      <span class="password-toggle-hide"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4" aria-hidden="true"><path d="M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49"/><path d="M14.084 14.158a3 3 0 0 1-4.242-4.242"/><path d="M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143"/><path d="m2 2 20 20"/></svg></span>
    </button>
  </span>
</div>

      <p class="field-description" id="pw-new-help-text">At least 12 characters.</p>
    </div>
    <div class="field w-full max-w-sm self-center">
      <label class="label" for="pw-disabled">Disabled</label>
      <div class="input-group password-input" data-password-input>
  <input class="input" type="password" name="password" id="pw-disabled" autocomplete="current-password" autocapitalize="off" spellcheck="false" value="secret" disabled />
  <span class="input-group-addon input-group-addon-inline-end">
    <button type="button" class="btn btn-ghost btn-icon password-toggle" data-password-toggle aria-label="Show password" aria-pressed="false">
      <span class="password-toggle-show"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4" aria-hidden="true"><path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0"/><circle cx="12" cy="12" r="3"/></svg></span>
      <span class="password-toggle-hide"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4" aria-hidden="true"><path d="M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49"/><path d="M14.084 14.158a3 3 0 0 1-4.242-4.242"/><path d="M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143"/><path d="m2 2 20 20"/></svg></span>
    </button>
  </span>
</div>

    </div>
```

## Browsers and password managers

- **Saving the password.** Submitting the form hides the password again first, so browsers and password managers see a password field and offer to save it, and it isn't left on screen after an htmx swap.
- **Autofill.** Use `autocomplete="current-password"` on sign-in forms and `"new-password"` on sign-up and change-password forms, so password managers fill the right one and suggest strong passwords. The macro sets `autocapitalize="off"` and `spellcheck="false"` too, so a shown password isn't capitalised or sent to a spell checker.
- **Edge's own eye.** Microsoft Edge draws a reveal button inside password inputs; `.password-input` hides it so there is only one toggle. Safari's key icon for saved passwords stays.

## With htmx

The toggle is a `type="button"`, so it never sends the form. The behaviour runs again on inputs htmx swaps in, so a server re-render with an error keeps working. It also fires a bubbling `password-input:toggle` with `detail.visible` on the wrapper.

## Nunjucks macro

The `password_input` macro writes the group, input and toggle.

```jinja page.html
{% from "components/password-input/password-input.html" import password_input %}

<div class="field">
  <label class="label" for="password">Password</label>
  {{ password_input("password", id="password", required=true, caps=true) }}
</div>
{{ password_input("new-password", autocomplete="new-password", toggle="text", minlength=12) }}
{{ password_input("password", invalid=true, attrs={"aria-describedby": "password-error"}) }}
```

## Reference

| Class / attribute | Description |
| --- | --- |
| `.password-input` | On the .input-group. Add data-password-input. |
| `.password-toggle` | The show / hide button. Add data-password-toggle. |
| `.password-toggle-show / .password-toggle-hide` | Its look while hidden / while shown. |
| `data-password-toggle` | On the button, or on a checkbox. |
| `data-password` | Marks more inputs to show and hide together (default: type="password"). |
| `data-password-caps` | Shown only while Caps Lock is on. |
| `data-visible` | Set on the wrapper while the password is shown. |
| `password-input:toggle` | Event on the wrapper with detail { visible }. |
