<script lang="ts">
import * as InputOTP from "$lib/components/ui/input-otp/index.js";
</script>
<InputOTP.Root maxlength={6}>
{#snippet children({ cells })}
<InputOTP.Group>
{#each cells.slice(0, 3) as cell (cell)}
<InputOTP.Slot {cell} />
{/each}
</InputOTP.Group>
<InputOTP.Separator />
<InputOTP.Group>
{#each cells.slice(3, 6) as cell (cell)}
<InputOTP.Slot {cell} />
{/each}
</InputOTP.Group>
{/snippet}
</InputOTP.Root> About
Input OTP is built on top of Bits UI's PinInput which is inspired by @guilherme_rodz's Input OTP component.
Installation
pnpm dlx jsrepo@latest add input-otpnpx jsrepo@latest add input-otpyarn dlx jsrepo@latest add input-otpbunx jsrepo@latest add input-otpInstall bits-ui:
pnpm add bits-ui -Dnpm install bits-ui -Dyarn add bits-ui -Dbun add bits-ui -DCopy and paste the following code into your project.
import Root from './input-otp.svelte';
import Group from './input-otp-group.svelte';
import Slot from './input-otp-slot.svelte';
import Separator from './input-otp-separator.svelte';
export {
Root,
Group,
Slot,
Separator,
Root as InputOTP,
Group as InputOTPGroup,
Slot as InputOTPSlot,
Separator as InputOTPSeparator
};
Usage
<script lang="ts">
import * as InputOTP from '$lib/components/ui/input-otp/index.js';
</script> <InputOTP.Root maxlength={6}>
{#snippet children({ cells })}
<InputOTP.Group>
{#each cells.slice(0, 3) as cell}
<InputOTP.Slot {cell} />
{/each}
</InputOTP.Group>
<InputOTP.Separator />
<InputOTP.Group>
{#each cells.slice(3, 6) as cell}
<InputOTP.Slot {cell} />
{/each}
</InputOTP.Group>
{/snippet}
</InputOTP.Root> Examples
Pattern
Use the pattern prop to define a custom pattern for the OTP input.
<script lang="ts">
import * as InputOTP from "$lib/components/ui/input-otp/index.js";
import { REGEXP_ONLY_DIGITS_AND_CHARS } from "bits-ui";
</script>
<InputOTP.Root maxlength={6} pattern={REGEXP_ONLY_DIGITS_AND_CHARS}>
{#snippet children({ cells })}
<InputOTP.Group>
{#each cells as cell (cell)}
<InputOTP.Slot {cell} />
{/each}
</InputOTP.Group>
{/snippet}
</InputOTP.Root> <script lang="ts">
import * as InputOTP from '$lib/components/ui/input-otp/index.js';
import { REGEXP_ONLY_DIGITS_AND_CHARS } from 'bits-ui';
</script>
<InputOTP.Root maxlength={6} pattern={REGEXP_ONLY_DIGITS_AND_CHARS}>
<!-- ... -->
</InputOTP.Root> Separator
You can use the InputOTP.Separator component to add a separator between the groups of cells.
<script lang="ts">
import * as InputOTP from "$lib/components/ui/input-otp/index.js";
</script>
<InputOTP.Root maxlength={6}>
{#snippet children({ cells })}
<InputOTP.Group>
{#each cells.slice(0, 2) as cell (cell)}
<InputOTP.Slot {cell} />
{/each}
</InputOTP.Group>
<InputOTP.Separator />
<InputOTP.Group>
{#each cells.slice(2, 4) as cell (cell)}
<InputOTP.Slot {cell} />
{/each}
</InputOTP.Group>
<InputOTP.Separator />
<InputOTP.Group>
{#each cells.slice(4, 6) as cell (cell)}
<InputOTP.Slot {cell} />
{/each}
</InputOTP.Group>
{/snippet}
</InputOTP.Root> <script lang="ts">
import * as InputOTP from '$lib/components/ui/input-otp/index.js';
</script>
<InputOTP.Root maxlength={4}>
{#snippet children({ cells })}
<InputOTP.Group>
{#each cells.slice(0, 2) as cell}
<InputOTP.Slot {cell} />
{/each}
</InputOTP.Group>
<InputOTP.Separator />
<InputOTP.Group>
{#each cells.slice(2, 4) as cell}
<InputOTP.Slot {cell} />
{/each}
</InputOTP.Group>
{/snippet}
</InputOTP.Root> Controlled
Enter your one-time password.
<script lang="ts">
import * as InputOTP from "$lib/components/ui/input-otp/index.js";
let value = $state("");
</script>
<div class="space-y-2">
<InputOTP.Root maxlength={6} bind:value>
{#snippet children({ cells })}
<InputOTP.Group>
{#each cells.slice(0, 6) as cell (cell)}
<InputOTP.Slot {cell} />
{/each}
</InputOTP.Group>
{/snippet}
</InputOTP.Root>
<div class="text-center text-sm">
{value === "" ? "Enter your one-time password." : `You entered: ${value}`}
</div>
</div> Form
<script lang="ts">
import { toast } from "svelte-sonner";
import { Button } from "$lib/components/ui/button/index.js";
import * as Field from "$lib/components/ui/field/index.js";
import * as InputOTP from "$lib/components/ui/input-otp/index.js";
const pinId = "input-otp-form-pin";
const descriptionId = "input-otp-form-description";
const errorId = "input-otp-form-error";
let pin = $state("");
let errors = $state<{ message: string }[]>([]);
const invalid = $derived(errors.length > 0);
const describedBy = $derived(
`${descriptionId}${invalid ? ` ${errorId}` : ""}`
);
function handleSubmit(event: SubmitEvent) {
event.preventDefault();
if (pin.length !== 6) {
errors = [{ message: "Your one-time password must be 6 characters." }];
toast.error("Please fix the errors in the form.");
return;
}
errors = [];
toast.success(`You submitted ${JSON.stringify({ pin }, null, 2)}`);
}
</script>
<form method="POST" class="w-2/3 space-y-6" onsubmit={handleSubmit}>
<Field.Field data-invalid={invalid ? "true" : undefined}>
<Field.Label for={pinId}>One-Time Password</Field.Label>
<InputOTP.Root
id={pinId}
name="pin"
maxlength={6}
aria-describedby={describedBy}
aria-invalid={invalid ? "true" : undefined}
bind:value={pin}
>
{#snippet children({ cells })}
<InputOTP.Group>
{#each cells as cell (cell)}
<InputOTP.Slot {cell} />
{/each}
</InputOTP.Group>
{/snippet}
</InputOTP.Root>
<Field.Description id={descriptionId}>
Please enter the one-time password sent to your phone.
</Field.Description>
<Field.Error id={errorId} {errors} />
</Field.Field>
<Button type="submit" class="w-full">Submit</Button>
</form>