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
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-partattribute 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
idto theuseMachinehook. 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 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
Use the defaultValue context property to set the initial value of the pin
input. The value 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>
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 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 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(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 service = 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:
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
Edit this page on GitHub