Input OTP
A specialized input component for one-time password entry
Basic Usage
Default 6-digit OTP input with two groups separated by a dash.
1
2<script lang="ts">
3 import { InputOTP } from "@kareyes/aether";
4
5 let basicValue = $state("");
6</script>
7
8<InputOTP maxlength={6} groups={2} bind:value={basicValue} />
9{#if basicValue}
10 <p class="text-sm text-muted-foreground">Value: {basicValue}</p>
11{/if}Variants
Available visual styles for the OTP input.
default
outline
underline
1
2<script lang="ts">
3 import { InputOTP } from "@kareyes/aether";
4</script>
5
6<div class="space-y-6">
7 <div class="space-y-2">
8 <p class="text-sm font-medium">Default</p>
9 <InputOTP maxlength={6} groups={2} variant="default" />
10 </div>
11 <div class="space-y-2">
12 <p class="text-sm font-medium">Outline</p>
13 <InputOTP maxlength={6} groups={2} variant="outline" />
14 </div>
15 <div class="space-y-2">
16 <p class="text-sm font-medium">Underline</p>
17 <InputOTP maxlength={6} groups={2} variant="underline" />
18 </div>
19</div>Sizes
Different size options for the OTP input.
sm
default
lg
1
2<script lang="ts">
3 import { InputOTP } from "@kareyes/aether";
4</script>
5
6<div class="space-y-6">
7 <div class="space-y-2">
8 <p class="text-sm font-medium">Small</p>
9 <InputOTP maxlength={6} groups={1} size="sm" />
10 </div>
11 <div class="space-y-2">
12 <p class="text-sm font-medium">Default</p>
13 <InputOTP maxlength={6} groups={1} size="default" />
14 </div>
15 <div class="space-y-2">
16 <p class="text-sm font-medium">Large</p>
17 <InputOTP maxlength={6} groups={1} size="lg" />
18 </div>
19</div>Groups Configuration
Control how digits are grouped with automatic separators.
Single Group (4 digits)
Two Groups (6 digits)
Three Groups (6 digits)
Without Separators
1
2<script lang="ts">
3 import { InputOTP } from "@kareyes/aether";
4</script>
5
6<div class="space-y-6">
7 <!-- Single Group (4 digits) -->
8 <InputOTP maxlength={4} groups={1} />
9
10 <!-- Two Groups (6 digits) -->
11 <InputOTP maxlength={6} groups={2} />
12
13 <!-- Three Groups (6 digits) -->
14 <InputOTP maxlength={6} groups={3} />
15
16 <!-- Without Separators -->
17 <InputOTP maxlength={6} groups={2} showSeparator={false} />
18</div>Error State
OTP input showing an error state with optional error message.
1
2<script lang="ts">
3 import { InputOTP, Field } from "@kareyes/aether";
4
5 let errorValue = $state("");
6 let showError = $state(false);
7</script>
8
9<Field label="Verification Code" error={showError ? "Invalid verification code. Please try again." : undefined}>
10 <InputOTP
11 maxlength={6}
12 groups={2}
13 bind:value={errorValue}
14 error={showError}
15 />
16 <button
17 type="button"
18 class="text-sm px-3 py-1.5 rounded-md bg-destructive text-destructive-foreground hover:bg-destructive/90"
19 onclick={() => showError = !showError}
20 >
21 Toggle Error: {showError ? 'On' : 'Off'}
22 </button>
23</Field>onError Callback
Get notified when the error state changes.
Error callback triggered: No
(Error shows when value is partially filled)
1
2<script lang="ts">
3 import { InputOTP } from "@kareyes/aether";
4
5 let errorCallbackValue = $state("");
6 let errorCallbackTriggered = $state(false);
7
8 function handleErrorChange(hasError: boolean) {
9 errorCallbackTriggered = hasError;
10 }
11</script>
12
13<InputOTP
14 maxlength={6}
15 groups={2}
16 bind:value={errorCallbackValue}
17 error={errorCallbackValue.length > 0 && errorCallbackValue.length < 6}
18 onError={handleErrorChange}
19/>
20<p class="text-sm text-muted-foreground">
21 Error callback triggered:
22 <span class={errorCallbackTriggered ? "text-destructive" : "text-green-600"}>
23 {errorCallbackTriggered ? 'Yes' : 'No'}
24 </span>
25</p>Disabled State
OTP input in disabled state.
1
2<script lang="ts">
3 import { InputOTP } from "@kareyes/aether";
4</script>
5
6<InputOTP maxlength={6} groups={2} disabled />Complete Handler
Callback triggered when all slots are filled.
1
2<script lang="ts">
3 import { InputOTP } from "@kareyes/aether";
4
5 let completedValue = $state("");
6 let isComplete = $state(false);
7
8 function handleComplete(value: string) {
9 isComplete = true;
10 setTimeout(() => { isComplete = false; }, 2000);
11 }
12</script>
13
14<InputOTP
15 maxlength={6}
16 groups={2}
17 bind:value={completedValue}
18 onComplete={handleComplete}
19/>
20{#if isComplete}
21 <p class="text-sm text-green-600">OTP Complete: {completedValue}</p>
22{/if}Variant + Size Combinations
Combining different variants with sizes.
Outline + Small
Default + Default
Underline + Large
1
2<script lang="ts">
3 import { InputOTP } from "@kareyes/aether";
4</script>
5
6<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
7 <div class="space-y-2">
8 <p class="text-sm font-medium">Outline + Small</p>
9 <InputOTP maxlength={4} groups={1} variant="outline" size="sm" />
10 </div>
11 <div class="space-y-2">
12 <p class="text-sm font-medium">Default + Default</p>
13 <InputOTP maxlength={4} groups={1} variant="default" size="default" />
14 </div>
15 <div class="space-y-2">
16 <p class="text-sm font-medium">Underline + Large</p>
17 <InputOTP maxlength={4} groups={1} variant="underline" size="lg" />
18 </div>
19</div>Features
- 3 Visual Variants: default, outline, underline
- 3 Sizes: sm, default, lg
- Flexible Grouping: Split digits into groups with automatic separators
- Pattern Validation: Restrict input to specific character patterns
- Copy/Paste Support: Full clipboard functionality
- Complete Callback: Get notified when all slots are filled
- Error State: Visual feedback for validation errors
- Full TypeScript Support: Complete type safety and IntelliSense
- Accessibility: Built on accessible Bits UI primitives
Basic Usage
<script lang="ts">
import { InputOTP } from "@kareyes/aether";
let value = $state("");
</script>
<InputOTP maxlength={6} groups={2} bind:value />
Variants
<InputOTP maxlength={6} variant="default" groups={2} />
<InputOTP maxlength={6} variant="outline" groups={2} />
<InputOTP maxlength={6} variant="underline" groups={2} />
Sizes
<InputOTP maxlength={6} size="sm" groups={1} />
<InputOTP maxlength={6} size="default" groups={1} />
<InputOTP maxlength={6} size="lg" groups={1} />
Groups Configuration
Control how the OTP digits are grouped:
<!-- Single group (no separator) -->
<InputOTP maxlength={6} groups={1} />
<!-- Two groups: 3-3 -->
<InputOTP maxlength={6} groups={2} />
<!-- Three groups: 2-2-2 -->
<InputOTP maxlength={6} groups={3} />
<!-- Hide separator even with multiple groups -->
<InputOTP maxlength={6} groups={2} showSeparator={false} />
Pattern Validation
Restrict input to specific character patterns using regex strings:
<script lang="ts">
import { InputOTP } from "@kareyes/aether";
import { REGEXP_ONLY_DIGITS, REGEXP_ONLY_DIGITS_AND_CHARS } from "bits-ui";
</script>
<!-- Digits only (0-9) -->
<InputOTP maxlength={6} pattern={REGEXP_ONLY_DIGITS} />
<!-- Alphanumeric (letters and numbers) -->
<InputOTP maxlength={6} pattern={REGEXP_ONLY_DIGITS_AND_CHARS} />
<!-- Custom pattern -->
<InputOTP maxlength={6} pattern="^[A-Z0-9]+$" />
Complete Callback
Get notified when all OTP slots are filled:
<script lang="ts">
import { InputOTP } from "@kareyes/aether";
let otpValue = $state("");
function handleComplete(value: string) {
console.log("OTP Complete:", value);
// Trigger verification...
}
</script>
<InputOTP
maxlength={6}
groups={2}
bind:value={otpValue}
onComplete={handleComplete}
/>
Error State
Display validation errors with visual feedback:
<script lang="ts">
import { InputOTP } from "@kareyes/aether";
let otpValue = $state("");
let hasError = $state(false);
function handleErrorChange(error: boolean) {
console.log("Error state:", error);
}
</script>
<InputOTP
maxlength={6}
groups={2}
bind:value={otpValue}
error={hasError}
onError={handleErrorChange}
/>
{#if hasError}
<p class="text-destructive text-sm mt-1">Invalid verification code</p>
{/if}
Disabled State
<InputOTP maxlength={6} groups={2} disabled />
Value Change Callback
Track every value change:
<script lang="ts">
import { InputOTP } from "@kareyes/aether";
let otpValue = $state("");
function handleValueChange(value: string) {
console.log("Current value:", value);
}
</script>
<InputOTP
maxlength={6}
groups={2}
bind:value={otpValue}
onValueChange={handleValueChange}
/>
Props
| Prop | Type | Default | Description |
|---|---|---|---|
maxlength |
number |
required | Maximum length of the OTP input |
value |
string |
"" |
Current OTP value (bindable) |
variant |
"default" | "outline" | "underline" |
"default" |
Visual style variant |
size |
"sm" | "default" | "lg" |
"default" |
Size of input slots |
groups |
number |
1 |
Number of groups to split the input into |
showSeparator |
boolean |
true (when groups > 1) |
Show separator between groups |
disabled |
boolean |
false |
Whether the input is disabled |
pattern |
string |
undefined |
Regex pattern string to validate input |
error |
boolean |
false |
Whether the input has an error state |
onComplete |
(value: string) => void |
undefined |
Callback when all slots are filled |
onValueChange |
(value: string) => void |
undefined |
Callback when value changes |
onError |
(hasError: boolean) => void |
undefined |
Callback when error state changes |
class |
string |
undefined |
Additional CSS classes |
Primitive Components
For more control, you can use the primitive components directly:
<script lang="ts">
import { InputOTPPrimitives } from "@kareyes/aether";
</script>
<InputOTPPrimitives.Root maxlength={6} variant="default" size="default">
{#snippet children({ cells })}
<InputOTPPrimitives.Group>
{#each cells.slice(0, 3) as cell (cell)}
<InputOTPPrimitives.Slot {cell} />
{/each}
</InputOTPPrimitives.Group>
<InputOTPPrimitives.Separator />
<InputOTPPrimitives.Group>
{#each cells.slice(3, 6) as cell (cell)}
<InputOTPPrimitives.Slot {cell} />
{/each}
</InputOTPPrimitives.Group>
{/snippet}
</InputOTPPrimitives.Root>
Primitive Components
| Component | Description |
|---|---|
InputOTPPrimitives.Root |
The root container that provides context |
InputOTPPrimitives.Group |
Groups slots together visually |
InputOTPPrimitives.Slot |
Individual input slot for a single character |
InputOTPPrimitives.Separator |
Visual separator between groups |
Accessibility
- Built on Bits UI PinInput accessible primitives
- Proper focus management and keyboard navigation
- Supports
aria-invalidfor error states - Screen reader friendly
Pattern Constants
Bits UI provides helpful pattern constants:
<script>
import {
REGEXP_ONLY_DIGITS, // "^\\d+$"
REGEXP_ONLY_CHARS, // "^[a-zA-Z]+$"
REGEXP_ONLY_DIGITS_AND_CHARS // "^[a-zA-Z0-9]+$"
} from "bits-ui";
</script>
Using InputOTP with Field Component
The Field component provides a consistent way to add labels, descriptions, and error handling to your InputOTP components.
Basic Field Usage
<script lang="ts">
import { InputOTP, Field } from "@kareyes/aether";
let code = $state("");
</script>
<Field
label="Verification Code"
description="Enter the 6-digit code sent to your phone"
>
<InputOTP maxlength={6} groups={2} bind:value={code} />
</Field>
With Validation
<script lang="ts">
import { InputOTP, Field } from "@kareyes/aether";
import { REGEXP_ONLY_DIGITS } from "bits-ui";
let code = $state("");
let error = $derived(code.length > 0 && code.length < 6);
</script>
<Field
label="OTP Code"
description="Please enter the complete 6-digit code"
required
error={error ? "Code must be 6 digits" : undefined}
>
<InputOTP
maxlength={6}
groups={2}
pattern={REGEXP_ONLY_DIGITS}
bind:value={code}
error={error}
/>
</Field>
Different Variants with Field
<script lang="ts">
import { InputOTP, Field } from "@kareyes/aether";
</script>
<Field
label="Security Code"
description="Outline variant for better visibility"
>
<InputOTP
maxlength={6}
variant="outline"
groups={3}
size="lg"
/>
</Field>
<Field
label="Access Code"
description="Underline variant for minimal design"
>
<InputOTP
maxlength={4}
variant="underline"
groups={1}
/>
</Field>
Complete Form Example
<script lang="ts">
import { InputOTP, FieldPrimitives, Field, Button } from "@kareyes/aether";
import { REGEXP_ONLY_DIGITS, REGEXP_ONLY_DIGITS_AND_CHARS } from "bits-ui";
let verificationCode = $state("");
let backupCode = $state("");
let codeError = $derived(
verificationCode.length > 0 && verificationCode.length < 6
? "Verification code must be 6 digits"
: undefined
);
function handleSubmit() {
console.log("Codes:", { verificationCode, backupCode });
}
</script>
<FieldPrimitives.Set>
<FieldPrimitives.Legend>Two-Factor Authentication</FieldPrimitives.Legend>
<FieldPrimitives.Description>
Enter the verification codes to access your account
</FieldPrimitives.Description>
<FieldPrimitives.Separator />
<FieldPrimitives.Group class="gap-4">
<Field
label="Verification Code"
description="Enter the 6-digit code from your authenticator app"
required
error={codeError}
>
<InputOTP
maxlength={6}
groups={2}
pattern={REGEXP_ONLY_DIGITS}
bind:value={verificationCode}
error={!!codeError}
variant="outline"
size="lg"
/>
</Field>
<Field
label="Backup Code (Optional)"
description="Use a backup code if you don't have access to your authenticator"
>
<InputOTP
maxlength={8}
groups={2}
pattern={REGEXP_ONLY_DIGITS_AND_CHARS}
bind:value={backupCode}
variant="underline"
/>
</Field>
</FieldPrimitives.Group>
<div class="flex gap-4 pt-4">
<Button onclick={handleSubmit} disabled={!!codeError}>
Verify & Login
</Button>
<Button variant="outline" type="button">
Resend Code
</Button>
</div>
</FieldPrimitives.Set>