Skip to main content
View Zag.js on Github
Join the Discord server

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.



  • 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.


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

This command will install the framework agnostic pin input logic and the reactive utilities for your framework of choice.


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.

On a high level, the pin input consists of:

  • Root: The root container for the pin input.
  • Input: The editable field for users to enter values.


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 the useSetup 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.rootProps}> <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:

  • onChange — Function invoked when the value is changed.
  • onComplete — Function invoked when all fields have been completed (by typing or pasting).
  • onInvalid — 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({ onChange(value) { // value => string[] console.log("value changed to:", value) }, onComplete(value, valueAsString) { // value => string[], valueAsString => string console.log("completed value:", value) }, onInvalid(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", }), )

Methods and Properties

The pin input's api exposes the following methods:

  • value — The value of the pin input as an array.
  • valueAsString — The value of the pin input as string.
  • isValueComplete — Whether all input's have been filled.
  • setValue() — Function used to set the a new value of the input.
  • clearValue() — Function used to clear the values.
  • setValueAtIndex() — Function used to set value at the specified index.
  • focus() — Function used to focus the first input.
const api = connect(state, send) api.value // => ["1", "3", "3"] api.valueAsString // => "133" api.isValueComplete // => true api.setValue(["3", "0", "0"]) api.clearValue() api.setValueAtIndex(1, "2") // sets the value at index 1 to "2"

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 */ }

Edit this page on GitHub

On this page