- Home
- Primitives
- Input OTP
- Examples
Input OTP
Basic input otp example demonstrating core behavior.
Examples
import * as InputOtp from "@solidiom/input-otp"
;<InputOtp.Root
maxLength={6}
pattern="^[0-9]*$"
onComplete={(value) => console.log("Code entered:", value)}
>
<InputOtp.Group>
<InputOtp.Slot index={0} />
<InputOtp.Slot index={1} />
<InputOtp.Slot index={2} />
</InputOtp.Group>
<InputOtp.Group>
<InputOtp.Slot index={3} />
<InputOtp.Slot index={4} />
<InputOtp.Slot index={5} />
</InputOtp.Group>
</InputOtp.Root>Each Slot displays one character from the OTP value. Use Group to visually separate segments. The pattern prop restricts allowed characters. The onComplete callback fires when all slots are filled.
View source
export function Root(props: InputOTPRootProps) {
let inputRef: HTMLInputElement | undefined
const inputId = createStableId("otp-input")
const [internalValue, setInternalValue] = createSignal(props.defaultValue ?? "")
const [isFocused, setIsFocused] = createSignal(false)
const value = () => {
if (props.value !== undefined) {
return props.value() ?? ""
}
return internalValue()
}
const activeIndex = () => {
const len = value().length
return Math.min(len, props.maxLength - 1)
}
const patternRegex = props.pattern ? new RegExp(props.pattern) : undefined
const setValue = (next: string) => {
const truncated = next.slice(0, props.maxLength)
// Validate each character
if (patternRegex) {
const valid = truncated.split("").every((ch) => patternRegex.test(ch))
if (!valid) return
}
if (props.value === undefined) {
setInternalValue(truncated)
}
props.onValueChange?.(truncated)
if (truncated.length === props.maxLength) {
props.onComplete?.(truncated)
}
}
const handleInput = (e: InputEvent) => {
const target = e.target as HTMLInputElement
setValue(target.value)
}
const handleKeyDown = (e: KeyboardEvent) => {
if (props.disabled) {
e.preventDefault()
return
}
// Allow navigation keys
if (
e.key === "Backspace" ||
e.key === "Delete" ||
e.key === "ArrowLeft" ||
e.key === "ArrowRight"
) {
return
}
}
const handlePaste = (e: ClipboardEvent) => {
e.preventDefault()
if (props.disabled) return
const pasted = e.clipboardData?.getData("text/plain") ?? ""
setValue(pasted)
}
const focus = () => {
inputRef?.focus()
}
return (
<InputOTPContext
value={{
value,
maxLength: props.maxLength,
activeIndex,
isFocused,
pattern: patternRegex,
disabled: props.disabled,
focus,
}}
>
<div
class={props.class}
style={props.style}
onClick={focus}
{...applySemanticAttrs({
scope: "input-otp",
part: "root",
disabled: props.disabled,
})}
>
{/* Hidden input drives the actual value */}
<input
ref={inputRef}
id={inputId}
type="text"
inputmode="numeric"
autocomplete="one-time-code"
maxlength={props.maxLength}
value={value()}
disabled={props.disabled}
onInput={handleInput}
onKeyDown={handleKeyDown}
onPaste={handlePaste}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
aria-label="One-time password"
style={{
position: "absolute",
width: "1px",
height: "1px",
padding: "0",
margin: "-1px",
overflow: "hidden",
clip: "rect(0, 0, 0, 0)",
"white-space": "nowrap",
"border-width": "0",
}}
/>
{props.children}
</div>
</InputOTPContext>
)
}