Pin Input
The pin input is optimized for entering a sequence of digits or letters. The input fields allow one character at a time. When the digit or letter is entered, focus transfers to the next input in the sequence, until every input is filled.
Features
- Automatically focuses the next field on typing and focuses the previous field on deletion
- Supports numeric and alphanumeric values
- Supports masking value (for sensitive data)
- Supports copy/paste to autofill all fields
- Supports fast paste SMS-code
Installation
Install the pin input package:
npm install @zag-js/pin-input @zag-js/react # or yarn add @zag-js/pin-input @zag-js/react
npm install @zag-js/pin-input @zag-js/solid # or yarn add @zag-js/pin-input @zag-js/solid
npm install @zag-js/pin-input @zag-js/vue # or yarn add @zag-js/pin-input @zag-js/vue
npm install @zag-js/pin-input @zag-js/svelte # or yarn add @zag-js/pin-input @zag-js/svelte
Anatomy
Check the pin input anatomy and part names.
Each part includes a
data-partattribute to help identify them in the DOM.
Usage
Import the pin input package:
import * as pinInput from "@zag-js/pin-input"
These are the key exports:
machine- State machine logic.connect- Maps machine state to JSX props and event handlers.
You'll need to provide a unique
idto theuseMachinehook. This is used to ensure every part has a unique identifier.
Then use the framework integration helpers:
import * as pinInput from "@zag-js/pin-input" import { useMachine, normalizeProps } from "@zag-js/react" export function PinInput() { const service = useMachine(pinInput.machine, { id: "1" }) const api = pinInput.connect(service, normalizeProps) return ( <div> <div {...api.getRootProps()}> <input {...api.getInputProps({ index: 0 })} /> <input {...api.getInputProps({ index: 1 })} /> <input {...api.getInputProps({ index: 2 })} /> </div> <button onClick={api.clearValue}>Clear</button> </div> ) }
import * as pinInput from "@zag-js/pin-input" import { normalizeProps, useMachine } from "@zag-js/solid" import { createMemo, createUniqueId } from "solid-js" export function PinInput() { const service = useMachine(pinInput.machine, { id: createUniqueId() }) const api = createMemo(() => pinInput.connect(service, normalizeProps)) return ( <div> <div {...api().getRootProps()}> <input {...api().getInputProps({ index: 0 })} /> <input {...api().getInputProps({ index: 1 })} /> <input {...api().getInputProps({ index: 2 })} /> </div> <button onClick={api().clearValue}>Clear</button> </div> ) }
<script setup> import * as pinInput from "@zag-js/pin-input" import { normalizeProps, useMachine } from "@zag-js/vue" import { computed } from "vue" const service = useMachine(pinInput.machine, { id: "1" }) const api = computed(() => pinInput.connect(service, normalizeProps)) </script> <template> <div> <div ref="ref" v-bind="api.getRootProps()"> <input v-bind="api.getInputProps({ index: 0 })" /> <input v-bind="api.getInputProps({ index: 1 })" /> <input v-bind="api.getInputProps({ index: 2 })" /> </div> <button @click="api.clearValue">Clear</button> </div> </template>
<script lang="ts"> import * as pinInput from "@zag-js/pin-input" import { useMachine, normalizeProps } from "@zag-js/svelte" const id = $props.id() const service = useMachine(pinInput.machine, { id }) const api = $derived(pinInput.connect(service, normalizeProps)) </script> <div> <div {...api.getRootProps()}> <input {...api.getInputProps({ index: 0 })} /> <input {...api.getInputProps({ index: 1 })} /> <input {...api.getInputProps({ index: 2 })} /> </div> <button onclick={api.clearValue}>Clear</button> </div>
Setting a default value
Set defaultValue to define the initial pin value. It must be an array of
strings.
const service = useMachine(pinInput.machine, { defaultValue: ["1", "2", ""], })
Controlled value
Use the value and onValueChange properties to programmatically control the
value of the pin input.
import { useState } from "react" export function ControlledPinInput() { const [value, setValue] = useState(["", "", "", ""]) const service = useMachine(pinInput.machine, { value, onValueChange(details) { setValue(details.value) }, }) }
import { createSignal } from "solid-js" export function ControlledPinInput() { const [value, setValue] = createSignal(["", "", "", ""]) const service = useMachine(pinInput.machine, { get value() { return value() }, onValueChange(details) { setValue(details.value) }, }) }
<script setup lang="ts"> import { ref } from "vue" const valueRef = ref(["", "", "", ""]) const service = useMachine(pinInput.machine, { get value() { return valueRef.value }, onValueChange(details) { valueRef.value = details.value }, }) </script>
<script lang="ts"> let value = $state(["", "", "", ""]) const service = useMachine(pinInput.machine, { get value() { return value }, onValueChange(details) { value = details.value }, }) </script>
Setting input count
Set count to define the number of input fields to render.
const service = useMachine(pinInput.machine, { count: 6, })
Changing the placeholder
To customize the default pin input placeholder (○) for each input, pass the
placeholder prop and set it to your desired value.
const service = useMachine(pinInput.machine, { placeholder: "*", })
Blur on complete
By default, the last input maintains focus when filled and onValueComplete is
invoked. To blur the last input when the user completes the input, set the
blurOnComplete: true in the machine's context.
const service = useMachine(pinInput.machine, { blurOnComplete: true, })
Allowing alphanumeric values
By default, the pin input accepts only number values but you can choose between
numeric, alphanumeric and alphabetic values. To change the input mode,
pass the type context property and set its value to alphanumeric.
const service = useMachine(pinInput.machine, { type: "alphanumeric", })
Using OTP mode
To trigger smartphone OTP auto-suggestion, it is recommended to set the
autocomplete attribute to "one-time-code". The pin-input machine provides
support for this automatically when you set the otp context property to
true.
const service = useMachine(pinInput.machine, { otp: true, })
Securing the text input
When collecting private or sensitive information using the pin input, you might
need to mask the value entered, similar to <input type="password"/>. Pass the
mask context property and set it to true.
const service = useMachine(pinInput.machine, { mask: true, })
Listening for changes
The pin input machine invokes several callback functions when the user enters:
onValueChange— Function invoked when the value is changed.onValueComplete— Function invoked when all fields have been completed (by typing or pasting).onValueInvalid— Function invoked when an invalid value is entered into the input. An invalid value is any value that doesn't match the specified "type".
const service = useMachine(pinInput.machine, { onValueChange(details) { // details => { value: string[], valueAsString: string } console.log("value changed to:", details.value) }, onValueComplete(details) { // details => { value: string[], valueAsString: string } console.log("completed value:", details) }, onValueInvalid(details) { // details => { index: number, value: string } console.log("invalid value:", details) }, })
Autofocus and selection behavior
Use autoFocus to focus the first input on mount, and selectOnFocus to select
the active character on focus.
const service = useMachine(pinInput.machine, { autoFocus: true, selectOnFocus: true, })
RTL support
The pin input machine supports RTL writing directions. Set dir to rtl.
When this attribute is set, we attach a dir attribute to the root part.
const service = useMachine(pinInput.machine, { dir: "rtl", })
Submitting with an external form
Set form if the hidden input should submit with a form outside the current DOM
subtree.
const service = useMachine(pinInput.machine, { name: "value", form: "checkout-form", })
Customizing accessibility labels
Use translations.inputLabel to customize screen-reader labels per input.
const service = useMachine(pinInput.machine, { count: 4, translations: { inputLabel: (index, length) => `Digit ${index + 1} of ${length}`, }, })
Styling guide
Each part includes a data-part attribute you can target in CSS.
Completed state
When all values have been filled, we attach a data-complete attribute to the
root and input parts.
[data-part="root"][data-complete] { /* styles for when all value has been filled */ } [data-part="input"][data-complete] { /* styles for when all value has been filled */ }
Invalid state
When an invalid value is entered, we attach a data-invalid attribute to the
affected input part.
[data-part="input"][data-invalid] { /* styles for when the input is invalid */ }
Disabled state
When the pin-input is disabled, we attach a data-disabled attribute to the
root and input parts.
[data-part="root"][data-disabled] { /* styles for when the input is disabled */ } [data-part="input"][data-disabled] { /* styles for when the input is disabled */ }
Methods and Properties
Machine Context
The pin input machine exposes the following context properties:
namestringThe name of the input element. Useful for form submission.formstringThe associate form of the underlying input element.patternstringThe regular expression that the user-entered input value is checked against.idsPartial<{ root: string; hiddenInput: string; label: string; control: string; input: (id: string) => string; }>The ids of the elements in the pin input. Useful for composition.disabledbooleanWhether the inputs are disabledplaceholderstringThe placeholder text for the inputautoFocusbooleanWhether to auto-focus the first input.invalidbooleanWhether the pin input is in the invalid staterequiredbooleanWhether the pin input is requiredreadOnlybooleanWhether the pin input is in the valid stateotpbooleanIf `true`, the pin input component signals to its fields that they should use `autocomplete="one-time-code"`.valuestring[]The controlled value of the the pin input.defaultValuestring[]The initial value of the the pin input when rendered. Use when you don't need to control the value of the pin input.type"alphanumeric" | "numeric" | "alphabetic"The type of value the pin-input should allowonValueComplete(details: ValueChangeDetails) => voidFunction called when all inputs have valid valuesonValueChange(details: ValueChangeDetails) => voidFunction called on input changeonValueInvalid(details: ValueInvalidDetails) => voidFunction called when an invalid value is enteredmaskbooleanIf `true`, the input's value will be masked just like `type=password`blurOnCompletebooleanWhether to blur the input when the value is completeselectOnFocusbooleanWhether to select input value when input is focusedtranslationsIntlTranslationsSpecifies the localized strings that identifies the accessibility elements and their statescountnumberThe number of inputs to render to improve SSR aria attributes. This will be required in next major version.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 pin input api exposes the following methods:
valuestring[]The value of the input as an array of strings.valueAsStringstringThe value of the input as a string.completebooleanWhether all inputs are filled.countnumberThe number of inputs to renderitemsnumber[]The array of input values.setValue(value: string[]) => voidFunction to set the value of the inputs.clearValueVoidFunctionFunction to clear the value of the inputs.setValueAtIndex(index: number, value: string) => voidFunction to set the value of the input at a specific index.focusVoidFunctionFunction to focus the pin-input. This will focus the first input.
Data Attributes
Accessibility
Keyboard Interactions
- ArrowLeftMoves focus to the previous input
- ArrowRightMoves focus to the next input
- BackspaceDeletes the value in the current input and moves focus to the previous input
- DeleteDeletes the value in the current input
- Control + VPastes the value into the input fields