Installation
Install the dependencies
pnpm add cn github:sveltery/baseCopy the component
Copy the following files into src/lib/components/ui/input-otp.
<script lang="ts">
import { OTPField as Primitive, type OTPFieldRootProps } from '@sveltery/base/otp-field';
import { cn } from 'cn';
let { class: classProp, value = $bindable(), ...props }: OTPFieldRootProps = $props();
</script>
<Primitive.Root data-slot="input-otp" spellcheck={false} {...props} class={cn('gap-2 flex items-center has-disabled:opacity-50', classProp)} bind:value /><script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
import { cn } from 'cn';
let { children, class: classProp, ...props }: HTMLAttributes<HTMLDivElement> = $props();
</script>
<div data-slot="input-otp-group" {...props} class={cn('has-aria-invalid:ring-destructive/20 dark:has-aria-invalid:ring-destructive/40 has-aria-invalid:border-destructive rounded-lg has-aria-invalid:ring-3 flex items-center', classProp)}>
{@render children?.()}
</div><script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
import { cn } from 'cn';
import MinusIcon from '@lucide/svelte/icons/minus';
let { class: classProp, ...props }: HTMLAttributes<HTMLDivElement> = $props();
</script>
<div data-slot="input-otp-separator" role="separator" {...props} class={cn("[&_svg:not([class*='size-'])]:size-4 flex items-center", classProp)}>
<MinusIcon />
</div><script lang="ts">
import { OTPField as Primitive, type OTPFieldInputProps } from '@sveltery/base/otp-field';
import { cn } from 'cn';
let { class: classProp, ...props }: OTPFieldInputProps = $props();
</script>
<Primitive.Input data-slot="input-otp-slot" {...props} class={cn('dark:bg-input/30 border-input focus:border-ring focus:ring-ring/50 focus:aria-invalid:ring-destructive/20 dark:focus:aria-invalid:ring-destructive/40 aria-invalid:border-destructive focus:aria-invalid:border-destructive size-8 border-y border-r text-sm transition-all outline-none first:rounded-l-lg first:border-l last:rounded-r-lg focus:ring-3 relative flex items-center justify-center text-center focus:z-10 disabled:cursor-not-allowed', classProp)} />export { default as InputOTP } from './InputOTP.svelte';
export { default as InputOTPGroup } from './InputOTPGroup.svelte';
export { default as InputOTPSlot } from './InputOTPSlot.svelte';
export { default as InputOTPSeparator } from './InputOTPSeparator.svelte';
export type { OTPFieldRootProps as InputOTPProps, OTPFieldInputProps as InputOTPSlotProps } from '@sveltery/base/otp-field';Update the import paths
Change the imports to match your project setup.
Usage
<script lang="ts">
import { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot } from '$lib/components/ui/input-otp';
</script><InputOTP length={6}>
<InputOTPGroup>
<InputOTPSlot />
<InputOTPSlot />
<InputOTPSlot />
</InputOTPGroup>
<InputOTPSeparator />
<InputOTPGroup>
<InputOTPSlot />
<InputOTPSlot />
<InputOTPSlot />
</InputOTPGroup>
</InputOTP>Each InputOTPSlot is a real input, so there is no input-otp dependency. Slots take their position from their order, so they have no index prop. length sets the number of slots and must match the slots you render.
Typing moves to the next slot, Backspace moves back, and pasting a code fills every slot from the first. A phone that offers the code from a text message fills it too, through autocomplete="one-time-code".
Characters
validationType sets which characters a slot accepts: numeric (the default), alpha, alphanumeric or none. Characters that don't fit are dropped as the user types or pastes. onValueInvalid reports them.
In a form
Give InputOTP a name and it submits its value with its form. A native reset restores defaultValue. onValueComplete runs when every slot is filled, and autoSubmit submits the form at that point. To label it, put it in a Field and pair the FieldLabel with the InputOTP id.
<Field>
<FieldLabel for="code">Verification code</FieldLabel>
<InputOTP id="code" name="code" length={6} required>
…
</InputOTP>
</Field>API reference
InputOTP is styled on top of the Sveltery Base OTPField primitive, which owns its behavior, focus handling and accessibility. See the Base UI OTP Field documentation for the behavior it ports.