PIN Input

Allows users to input a sequence of one-character alphanumeric inputs.

	<script lang="ts">
  import { PinInput, type PinInputRootSnippetProps } from "bits-ui";
  import { toast } from "svelte-sonner";
  import { cn } from "$lib/utils/styles.js";
 
  let value = $state("");
 
  type CellProps = PinInputRootSnippetProps["cells"][0];
 
  function onComplete() {
    toast.success(`Completed with value ${value}`);
    value = "";
  }
</script>
 
<PinInput.Root
  bind:value
  class="group/pininput flex items-center text-foreground has-[:disabled]:opacity-30"
  maxlength={6}
  {onComplete}
>
  {#snippet children({ cells })}
    <div class="flex">
      {#each cells.slice(0, 3) as cell}
        {@render Cell(cell)}
      {/each}
    </div>
 
    <div class="flex w-10 items-center justify-center">
      <div class="h-1 w-3 rounded-full bg-border"></div>
    </div>
 
    <div class="flex">
      {#each cells.slice(3, 6) as cell}
        {@render Cell(cell)}
      {/each}
    </div>
  {/snippet}
</PinInput.Root>
 
{#snippet Cell(cell: CellProps)}
  <PinInput.Cell
    {cell}
    class={cn(
      // Custom class to override global focus styles
      "focus-override",
      "relative h-14 w-10 text-[2rem]",
      "flex items-center justify-center",
      "transition-all duration-200",
      "border-y border-r border-foreground/20 first:rounded-l-md first:border-l last:rounded-r-md",
      "text-foreground group-focus-within/pininput:border-foreground/40 group-hover/pininput:border-foreground/40",
      "outline outline-0",
      "data-[active]:outline-1 data-[active]:outline-white"
    )}
  >
    {#if cell.char !== null}
      <div>
        {cell.char}
      </div>
    {/if}
    {#if cell.hasFakeCaret}
      <div
        class="pointer-events-none absolute inset-0 flex animate-caret-blink items-center justify-center"
      >
        <div class="h-8 w-px bg-white"></div>
      </div>
    {/if}
  </PinInput.Cell>
{/snippet}

Overview

The PIN Input component provides a customizable solution for One-Time Password (OTP), Two-Factor Authentication (2FA), or Multi-Factor Authentication (MFA) input fields. Due to the lack of a native HTML element for these purposes, developers often resort to either basic input fields or custom implementations. This component offers a robust, accessible, and flexible alternative.

Key Features

  • Invisible Input Technique: Utilizes an invisible input element for seamless integration with form submissions and browser autofill functionality.
  • Customizable Appearance: Allows for custom designs while maintaining core functionality.
  • Accessibility: Ensures keyboard navigation and screen reader compatibility.
  • Flexible Configuration: Supports various PIN lengths and input types (numeric, alphanumeric).

Architecture

  1. Root Container: A relatively positioned root element that encapsulates the entire component.
  2. Invisible Input: A hidden input field that manages the actual value and interacts with the browser's built-in features.
  3. Visual Cells: Customizable elements representing each character of the PIN, rendered as siblings to the invisible input.

This structure allows for a seamless user experience while providing developers with full control over the visual representation.

Structure

	<script lang="ts">
	import { PinInput } from "bits-ui";
</script>
 
<PinInput.Root maxlength={6}>
	{#snippet children({ cells })}
		{#each cells as cell}
			<PinInput.Cell {cell} />
		{/each}
	{/snippet}
</PinInput.Root>

API Reference

PINInput.Root

The pin input container component.

Property Type Description
value $bindable
string

The value of the input.

Default: undefined
onValueChange
function

A callback function that is called when the value of the input changes.

Default: undefined
controlledValue
boolean

Whether or not the value is controlled or not. If true, the component will not update the value state internally, instead it will call onValueChange when it would have otherwise, and it is up to you to update the value prop that is passed to the component.

Default: false
disabled
boolean

Whether or not the pin input is disabled.

Default: false
textalign
enum

Where is the text located within the input. Affects click-holding or long-press behavior

Default: 'left'
maxlength
number

The maximum length of the pin input.

Default: 6
onComplete
function

A callback function that is called when the input is completely filled.

Default: undefined
inputId
string

Optionally provide an ID to apply to the hidden input element.

Default: undefined
pushPasswordManagerStrategy
enum

Enabled by default, it's an optional strategy for detecting Password Managers in the page and then shifting their badges to the right side, outside the input.

Default: undefined
ref $bindable
HTMLDivElement

The underlying DOM element being rendered. You can bind to this to get a reference to the element.

Default: undefined
children
Snippet

The children content to render.

Default: undefined
child
Snippet

Use render delegation to render your own element. See delegation docs for more information.

Default: undefined
Data Attribute Value Description
data-pin-input-root
''

Present on the root element.

PINInput.Cell

A single cell of the pin input.

Property Type Description
cell
object

The cell object provided by the cells snippet prop from the PinInput.Root component.

Default: undefined
ref $bindable
HTMLDivElement

The underlying DOM element being rendered. You can bind to this to get a reference to the element.

Default: undefined
children
Snippet

The children content to render.

Default: undefined
child
Snippet

Use render delegation to render your own element. See delegation docs for more information.

Default: undefined
Data Attribute Value Description
data-active
''

Present when the cell is active.

data-inactive
''

Present when the cell is inactive.

data-pin-input-cell
''

Present on the cell element.