PinInput

Captures a short sequence of characters with one input per position.

Value: —

vue
<script setup lang="ts">
import { ref } from 'vue'
import { PinInputInput, PinInputRoot } from '@typlog/ui'

const value = ref<string[]>([])
</script>

<template>
  <div class="flex flex-col items-start gap-3">
    <PinInputRoot v-model="value">
      <PinInputInput
        v-for="index in 6"
        :key="index"
        :index="index - 1"
      />
    </PinInputRoot>
    <p class="text-sm text-gray-11">
      Value: {{ value.join('') || '—' }}
    </p>
  </div>
</template>

PinInputRoot provides the input state and behavior, while each PinInputInput represents one position. The number of inputs is determined by the number of PinInputInput components you render.

API Reference ​

PinInputRoot ​

PropDefaultType
color
–

Overrides the accent color inherited from ThemeProvider.

radius
–
"none""small""medium""large""full"

Controls the corner radius.

size"2"
"1""2""3"

Controls the component size.

variant"surface"
"soft""surface"

Controls the visual treatment.

PinInputInput ​

Examples ​

Size ​

vue
<script setup lang="ts">
import { PinInputInput, PinInputRoot } from '@typlog/ui'
</script>

<template>
  <div class="flex flex-col items-start gap-4">
    <PinInputRoot size="1">
      <PinInputInput v-for="index in 6" :key="index" :index="index - 1" />
    </PinInputRoot>
    <PinInputRoot size="2">
      <PinInputInput v-for="index in 6" :key="index" :index="index - 1" />
    </PinInputRoot>
    <PinInputRoot size="3">
      <PinInputInput v-for="index in 6" :key="index" :index="index - 1" />
    </PinInputRoot>
  </div>
</template>

Variant ​

vue
<script setup lang="ts">
import { PinInputInput, PinInputRoot } from '@typlog/ui'
</script>

<template>
  <div class="flex flex-col items-start gap-4">
    <PinInputRoot variant="surface">
      <PinInputInput v-for="index in 6" :key="index" :index="index - 1" />
    </PinInputRoot>
    <PinInputRoot variant="soft">
      <PinInputInput v-for="index in 6" :key="index" :index="index - 1" />
    </PinInputRoot>
  </div>
</template>

One-time password ​

Set otp and type="number" for verification codes. Reka UI handles one-time-code autocomplete, sequential focus, and complete-code events.

Enter the six-digit code.

vue
<script setup lang="ts">
import { ref } from 'vue'
import { PinInputInput, PinInputRoot } from '@typlog/ui'

const value = ref<number[]>([])
const completed = ref('')

function onComplete(code: number[]) {
  completed.value = code.join('')
}
</script>

<template>
  <div class="flex flex-col items-start gap-3">
    <PinInputRoot
      v-model="value"
      type="number"
      otp
      placeholder="·"
      @complete="onComplete"
    >
      <PinInputInput v-for="index in 6" :key="index" :index="index - 1" />
    </PinInputRoot>
    <p class="text-sm text-gray-11">
      {{ completed ? `Completed: ${completed}` : 'Enter the six-digit code.' }}
    </p>
  </div>
</template>

Mask ​

vue
<script setup lang="ts">
import { PinInputInput, PinInputRoot } from '@typlog/ui'
</script>

<template>
  <PinInputRoot mask placeholder="•">
    <PinInputInput v-for="index in 4" :key="index" :index="index - 1" />
  </PinInputRoot>
</template>

States ​

vue
<script setup lang="ts">
import { PinInputInput, PinInputRoot } from '@typlog/ui'

const filled = ['1', '2', '3']
const disabled = ['1', '2', '3']
</script>

<template>
  <div class="flex flex-col items-start gap-4">
    <PinInputRoot placeholder="·">
      <PinInputInput v-for="index in 6" :key="index" :index="index - 1" />
    </PinInputRoot>
    <PinInputRoot :model-value="filled">
      <PinInputInput v-for="index in 6" :key="index" :index="index - 1" />
    </PinInputRoot>
    <PinInputRoot disabled :model-value="disabled">
      <PinInputInput v-for="index in 6" :key="index" :index="index - 1" />
    </PinInputRoot>
    <PinInputRoot>
      <PinInputInput v-for="index in 3" :key="index" :index="index - 1" />
      <PinInputInput :index="3" disabled />
      <PinInputInput :index="4" />
      <PinInputInput :index="5" />
    </PinInputRoot>
  </div>
</template>

Color ​

vue
<script setup lang="ts">
import { PinInputInput, PinInputRoot } from '@typlog/ui'
</script>

<template>
  <div class="flex flex-col items-start gap-4">
    <PinInputRoot color="indigo">
      <PinInputInput v-for="index in 6" :key="index" :index="index - 1" />
    </PinInputRoot>
    <PinInputRoot color="green" variant="soft">
      <PinInputInput v-for="index in 6" :key="index" :index="index - 1" />
    </PinInputRoot>
    <PinInputRoot color="red" variant="surface">
      <PinInputInput v-for="index in 6" :key="index" :index="index - 1" />
    </PinInputRoot>
  </div>
</template>

Radius ​

vue
<script setup lang="ts">
import { PinInputInput, PinInputRoot } from '@typlog/ui'
</script>

<template>
  <div class="flex flex-col items-start gap-4">
    <PinInputRoot radius="none">
      <PinInputInput v-for="index in 6" :key="index" :index="index - 1" />
    </PinInputRoot>
    <PinInputRoot radius="small">
      <PinInputInput v-for="index in 6" :key="index" :index="index - 1" />
    </PinInputRoot>
    <PinInputRoot radius="medium">
      <PinInputInput v-for="index in 6" :key="index" :index="index - 1" />
    </PinInputRoot>
    <PinInputRoot radius="large">
      <PinInputInput v-for="index in 6" :key="index" :index="index - 1" />
    </PinInputRoot>
    <PinInputRoot radius="full">
      <PinInputInput v-for="index in 6" :key="index" :index="index - 1" />
    </PinInputRoot>
  </div>
</template>

Grouped inputs ​

vue
<script setup lang="ts">
import { PinInputInput, PinInputRoot } from '@typlog/ui'
</script>

<template>
  <PinInputRoot>
    <PinInputInput v-for="index in 3" :key="index" :index="index - 1" />
    <span aria-hidden="true" class="px-1 text-gray-10">—</span>
    <PinInputInput v-for="index in 3" :key="`second-${index}`" :index="index + 2" />
  </PinInputRoot>
</template>

Form ​

name and required are forwarded to the form field represented by the root.

vue
<script setup lang="ts">
import { ref } from 'vue'
import { Button, PinInputInput, PinInputRoot } from '@typlog/ui'

const value = ref<string[]>([])
const submitted = ref('')

function submit(event: Event) {
  const form = event.currentTarget as HTMLFormElement
  submitted.value = new FormData(form).get('verificationCode')?.toString() ?? ''
}
</script>

<template>
  <form class="flex flex-col items-start gap-3" @submit.prevent="submit">
    <label for="verification-code">Verification code</label>
    <PinInputRoot
      id="verification-code"
      v-model="value"
      name="verificationCode"
      required
    >
      <PinInputInput v-for="index in 6" :key="index" :index="index - 1" />
    </PinInputRoot>
    <Button type="submit">
      Submit
    </Button>
    <p v-if="submitted" class="text-sm text-gray-11">
      Submitted: {{ submitted }}
    </p>
  </form>
</template>
Last updated Sep 15, 2026