Password Input
Features
- Includes button to toggle visibility of the password
- Automatic focus restoration to the input
- Resets visibility to hidden after form submission
- Can ignore supported password managers
Installation
Install the password-input package:
npm install @zag-js/password-input @zag-js/react # or yarn add @zag-js/password-input @zag-js/react
npm install @zag-js/password-input @zag-js/solid # or yarn add @zag-js/password-input @zag-js/solid
npm install @zag-js/password-input @zag-js/vue # or yarn add @zag-js/password-input @zag-js/vue
npm install @zag-js/password-input @zag-js/svelte # or yarn add @zag-js/password-input @zag-js/svelte
Anatomy
Check the password-input anatomy and part names.
Each part includes a
data-partattribute to help identify them in the DOM.
Usage
Import the password-input package:
import * as passwordInput from "@zag-js/password-input"
The password-input package exports two key functions:
machine- State machine logic.connect- Maps machine state to JSX props and event handlers.
Pass a unique
idtouseMachineso generated element ids stay predictable.
Then use the framework integration helpers:
import * as passwordInput from "@zag-js/password-input" import { useMachine, normalizeProps } from "@zag-js/react" import { EyeIcon, EyeOffIcon } from "lucide-react" import { useId } from "react" function PasswordInput() { const service = useMachine(passwordInput.machine, { id: useId() }) const api = passwordInput.connect(service, normalizeProps) return ( <div {...api.getRootProps()}> <label {...api.getLabelProps()}>Password</label> <div {...api.getControlProps()}> <input {...api.getInputProps()} /> <button {...api.getVisibilityTriggerProps()}> <span {...api.getIndicatorProps()}> {api.visible ? <EyeIcon /> : <EyeOffIcon />} </span> </button> </div> </div> ) }
import * as passwordInput from "@zag-js/password-input" import { useMachine, normalizeProps } from "@zag-js/solid" import { createMemo, createUniqueId } from "solid-js" function PasswordInput() { const service = useMachine(passwordInput.machine, { id: createUniqueId() }) const api = createMemo(() => passwordInput.connect(service, normalizeProps)) return ( <div {...api().getRootProps()}> <label {...api().getLabelProps()}>Password</label> <div {...api().getControlProps()}> <input {...api().getInputProps()} /> <button {...api().getVisibilityTriggerProps()}> <span {...api().getIndicatorProps()}> <Show when={api().visible} fallback={<EyeOffIcon />}> <EyeIcon /> </Show> </span> </button> </div> </div> ) }
<script setup> import * as passwordInput from "@zag-js/password-input" import { useMachine, normalizeProps } from "@zag-js/vue" import { computed, useId } from "vue" import { EyeIcon, EyeOffIcon } from "lucide-vue-next" const service = useMachine(passwordInput.machine, { id: useId() }) const api = computed(() => passwordInput.connect(service, normalizeProps)) </script> <template> <div v-bind="api.getRootProps()"> <label v-bind="api.getLabelProps()">Password</label> <div v-bind="api.getControlProps()"> <input v-bind="api.getInputProps()" /> <button v-bind="api.getVisibilityTriggerProps()"> <span v-bind="api.getIndicatorProps()"> <EyeIcon v-if="api.visible" /> <EyeOffIcon v-else /> </span> </button> </div> </div> </template>
<script lang="ts"> import * as passwordInput from "@zag-js/password-input" import { normalizeProps, useMachine } from "@zag-js/svelte" import { EyeIcon, EyeOffIcon } from "lucide-svelte" const id = $props.id() const service = useMachine(passwordInput.machine, { id }) const api = $derived(passwordInput.connect(service, normalizeProps)) </script> <div {...api.getRootProps()}> <label {...api.getLabelProps()}>Password</label> <div {...api.getControlProps()}> <input {...api.getInputProps()} /> <button {...api.getVisibilityTriggerProps()}> <span {...api.getIndicatorProps()}> {#if api.visible} <EyeIcon /> {:else} <EyeOffIcon /> {/if} </span> </button> </div> </div>
Setting the initial visibility
Set defaultVisible to define the initial visibility.
const service = useMachine(passwordInput.machine, { id: useId(), defaultVisible: true, })
Controlling the visibility
Use visible and onVisibilityChange to control visibility externally.
const service = useMachine(passwordInput.machine, { id: useId(), visible: true, onVisibilityChange(details) { // details => { visible: boolean } console.log(details.visible) }, })
Ignoring password managers
Set ignorePasswordManagers to true to ignore supported password managers.
This is useful when you want to ensure that the password input is not managed by password managers. Currently, this only works for 1Password, LastPass, Bitwarden, Dashlane, and Proton Pass.
const service = useMachine(passwordInput.machine, { id: useId(), ignorePasswordManagers: true, })
Why is this useful?
-
You might want to use this primitive for non-login scenarios (e.g., "secure notes", "temporary passwords")
-
In a verify password step, you might want to disable password managers for the confirm password field to ensure manual entry
-
Building a security-sensitive app where password managers violate compliance requirements.
Managing autocompletion
Set autoComplete to control password autofill behavior.
new-password— The user is creating a new password.current-password— The user is entering an existing password.
const service = useMachine(passwordInput.machine, { id: useId(), autoComplete: "new-password", })
Making the input required
Set required to true to make the input required.
const service = useMachine(passwordInput.machine, { id: useId(), required: true, })
Making the input read only
Set readOnly to true to make the input read only.
const service = useMachine(passwordInput.machine, { id: useId(), readOnly: true, })
Setting the input name
Set name to include the password field in form submission.
const service = useMachine(passwordInput.machine, { id: useId(), name: "password", })
Customizing accessibility labels
Use translations.visibilityTrigger to customize the toggle button label.
const service = useMachine(passwordInput.machine, { id: useId(), translations: { visibilityTrigger: (visible) => visible ? "Hide password" : "Show password", }, })
Styling guide
Each part includes a data-part attribute you can target in CSS.
[data-scope="password-input"][data-part="root"] { /* styles for the root part */ } [data-scope="password-input"][data-part="input"] { /* styles for the input part */ } [data-scope="password-input"][data-part="visibility-trigger"] { /* styles for the visibility trigger part */ } [data-scope="password-input"][data-part="indicator"] { /* styles for the indicator part */ } [data-scope="password-input"][data-part="control"] { /* styles for the control part */ } [data-scope="password-input"][data-part="label"] { /* styles for the label part */ }
Visibility State
Use the [data-state="visible"] and [data-state="hidden"] attributes to style
the password input when it is visible or hidden.
[data-scope="password-input"][data-part="input"][data-state="visible"] { /* styles for the visible state (for input) */ } [data-scope="password-input"][data-part="visibility-trigger"][data-state="visible"] { /* styles for the visible state (for visibility trigger) */ }
Disabled State
Use the [data-disabled] attribute to style the password input when it is
disabled.
[data-scope="password-input"][data-part="input"][data-disabled] { /* styles for the disabled state */ }
Invalid State
Use the [data-invalid] attribute to style the password input when it is
invalid.
[data-scope="password-input"][data-part="input"][data-invalid] { /* styles for the invalid state */ }
Readonly State
Use the [data-readonly] attribute to style the password input when it is read
only.
[data-scope="password-input"][data-part="input"][data-readonly] { /* styles for the readonly state */ }
Methods and Properties
Machine Context
The password-input machine exposes the following context properties:
defaultVisiblebooleanThe default visibility of the password input.visiblebooleanWhether the password input is visible.onVisibilityChange(details: VisibilityChangeDetails) => voidFunction called when the visibility changes.idsPartial<{ input: string; visibilityTrigger: string; }>The ids of the password input partsdisabledbooleanWhether the password input is disabled.invalidbooleanThe invalid state of the password input.readOnlybooleanWhether the password input is read only.requiredbooleanWhether the password input is required.translationsPartial<{ visibilityTrigger: (visible: boolean) => string; }>The localized messages to use.ignorePasswordManagersbooleanWhen `true`, the input will ignore password managers. **Only works for the following password managers** - 1Password, LastPass, Bitwarden, Dashlane, Proton PassautoComplete"current-password" | "new-password"The autocomplete attribute for the password input.namestringThe name of the password input.dir"ltr" | "rtl"The document's text/writing direction.idstringThe unique identifier of the machine.getRootNode() => ShadowRoot | Node | DocumentA root node to correctly resolve document in custom environments. E.x.: Iframes, Electron.
Machine API
The password-input api exposes the following methods:
visiblebooleanWhether the password input is visible.disabledbooleanWhether the password input is disabled.invalidbooleanWhether the password input is invalid.focusVoidFunctionFocus the password input.setVisible(value: boolean) => voidSet the visibility of the password input.toggleVisibleVoidFunctionToggle the visibility of the password input.