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.
- Support for masking value (for sensitive data).
- Support for copy/paste to autofill all fields.
- Supports fast paste SMS-code.
Installation
To use the pin input machine in your project, run the following command in your command line:
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
This command will install the framework agnostic pin input logic and the reactive utilities for your framework of choice.
Anatomy
To set up the pin input correctly, you'll need to understand its anatomy and how we name its parts.
Each part includes a
data-part
attribute to help identify them in the DOM.
Usage
First, import the pin input package into your project
import * as pinInput from "@zag-js/pin-input"
The pin input package exports two key functions:
machine
— The state machine logic for the pin input widget.connect
— The function that translates the machine's state to JSX attributes and event handlers.
You'll need to provide a unique
id
to theuseMachine
hook. This is used to ensure that the every part has a unique identifier.
Next, import the required hooks and functions for your framework and use the pin input machine in your project 🔥
import * as pinInput from "@zag-js/pin-input" import { useMachine, normalizeProps } from "@zag-js/react" export function PinInput() { const [state, send] = useMachine(pinInput.machine({ id: "1" })) const api = pinInput.connect(state, send, 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 [state, send] = useMachine(pinInput.machine({ id: createUniqueId() })) const api = createMemo(() => pinInput.connect(state, send, 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 [state, send] = useMachine(pinInput.machine({ id: "1" })); const api = computed(() => pinInput.connect(state.value, send, 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 [snapshot, send] = useMachine( pinInput.machine({ id: "1", }), ) const api = $derived(pinInput.connect(snapshot, send, 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
To set the initial value of the pin input, pass the value
property to the
machine's context.
const [state, send] = useMachine( pinInput.machine({ value: ["1", "2", ""], }), )
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 [state, send] = useMachine( pinInput.machine({ placeholder: "*", }), )
Blur on complete
By default, the last input maintains focus when filled and we invoke the
onComplete
callback. To blur the last input when the user completes the input,
set the blurOnComplete: true
in the machine's context.
const [state, send] = 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 [state, send] = 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 [state, send] = 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 [state, send] = 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 [state, send] = useMachine( pinInput.machine({ onValueChange(value) { // value => string[] console.log("value changed to:", 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) }, }), )
RTL support
The pin input machine supports RTL writing directions. To set the dir
property
in the machine's context.
When this attribute is set, we attach a dir
attribute to the root part.
const [state, send] = useMachine( pinInput.machine({ dir: "rtl", }), )
Styling guide
Earlier, we mentioned that each pin input's part has a data-part
attribute
added to them to select and style them in the DOM.
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-invalid] { /* styles for when the input is disabled */ }
Methods and Properties
Machine Context
The pin input machine exposes the following context properties:
name
string
The name of the input element. Useful for form submission.form
string
The associate form of the underlying input element.pattern
string
The regular expression that the user-entered input value is checked against.ids
Partial<{ root: string; hiddenInput: string; label: string; control: string; input(id: string): string; }>
The ids of the elements in the pin input. Useful for composition.disabled
boolean
Whether the inputs are disabledplaceholder
string
The placeholder text for the inputautoFocus
boolean
Whether to auto-focus the first input.invalid
boolean
Whether the pin input is in the invalid staterequired
boolean
Whether the pin input is requiredreadOnly
boolean
Whether the pin input is in the valid stateotp
boolean
If `true`, the pin input component signals to its fields that they should use `autocomplete="one-time-code"`.value
string[]
The value of the the pin input.type
"alphanumeric" | "numeric" | "alphabetic"
The type of value the pin-input should allowonValueComplete
(details: ValueChangeDetails) => void
Function called when all inputs have valid valuesonValueChange
(details: ValueChangeDetails) => void
Function called on input changeonValueInvalid
(details: ValueInvalidDetails) => void
Function called when an invalid value is enteredmask
boolean
If `true`, the input's value will be masked just like `type=password`blurOnComplete
boolean
Whether to blur the input when the value is completeselectOnFocus
boolean
Whether to select input value when input is focusedtranslations
IntlTranslations
Specifies the localized strings that identifies the accessibility elements and their statesdir
"ltr" | "rtl"
The document's text/writing direction.id
string
The unique identifier of the machine.getRootNode
() => ShadowRoot | Node | Document
A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron.
Machine API
The pin input api
exposes the following methods:
value
string[]
The value of the input as an array of strings.valueAsString
string
The value of the input as a string.complete
boolean
Whether all inputs are filled.setValue
(value: string[]) => void
Function to set the value of the inputs.clearValue
() => void
Function to clear the value of the inputs.setValueAtIndex
(index: number, value: string) => void
Function to set the value of the input at a specific index.focus
() => void
Function 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
Edit this page on GitHub