Password Input
Features
- Includes button to toggle visibility of the password
- Automatic focus restoration to the input
- Resets visibility to hidden after form submission
- Ignore password management apps like 1Password, LastPass, etc.
Installation
To use the password-input machine in your project, run the following command in your command line:
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
To set up the password-input correctly, you'll need to understand its anatomy and how we name its parts.
Each part includes a
data-partattribute to help identify them in the DOM.
Usage
First, import the password-input package into your project
import * as passwordInput from "@zag-js/password-input"
The password-input package exports two key functions:
machine— The state machine logic for the password-input widget.connect— The function that translates the machine's state to JSX attributes and event handlers.
You'll also need to provide a unique
idto theuseMachinehook. This is used to ensure that every part has a unique identifier.
Next, import the required hooks and functions for your framework and use the password-input machine in your project 🔥
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
Use the defaultVisible context property to set the initial visibility of the
password input.
const service = useMachine( passwordInput.machine, { id: useId(), defaultVisible: true, }, )
Controlling the visibility
Use the visible and onVisibilityChange context properties to control the
visibility of the password input.
The
onVisibilityChangecallback is invoked when the visibility changes.
const service = useMachine( passwordInput.machine, { id: useId(), visible: true, onVisibilityChange(details) { console.log(details) }, }, )
Ignoring password managers
Set the ignorePasswordManager context property to true to ignore password
managers like 1Password, LastPass, etc.
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(), ignorePasswordManager: 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
Configure the autoComplete context property to manage autocompletion.
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 the required context property to true to make the input required.
const service = useMachine( passwordInput.machine, { id: useId(), required: true, }, )
Making the input read only
Set the readOnly context property to true to make the input read only.
const service = useMachine( passwordInput.machine, { id: useId(), readOnly: true, }, )
Styling guide
Earlier, we mentioned that each password-input part has a data-part attribute
added to them to select and style them in the DOM.
[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.focus() => voidFocus the password input.setVisible(value: boolean) => voidSet the visibility of the password input.toggleVisible() => voidToggle the visibility of the password input.
Edit this page on GitHub