OTP Field
An accessible and customizable OTP Input component for SolidJS.
import { Show, type VoidComponent } from 'solid-js'
import clsx from 'clsx'
import OtpField from '@corvu/otp-field'
const OtpFieldExample: VoidComponent = () => {
return (
<div class="flex size-full items-center justify-center">
<OtpField maxLength={6} class="flex">
<OtpField.Input aria-label="Verification Code" />
<div class="flex items-center space-x-2">
<Slot index={0} />
<Slot index={1} />
<Slot index={2} />
</div>
<div class="flex size-10 items-center justify-center font-bold text-corvu-text-dark">
-
</div>
<div class="flex items-center space-x-2">
<Slot index={3} />
<Slot index={4} />
<Slot index={5} />
</div>
</OtpField>
</div>
)
}
const Slot = (props: { index: number }) => {
const context = OtpField.useContext()
const char = () => context.value()[props.index]
const showFakeCaret = () =>
context.value().length === props.index && context.isInserting()
return (
<div
class={clsx(
'flex size-10 items-center justify-center rounded-md bg-corvu-100 font-mono text-sm font-bold transition-all',
{
'ring-corvu-text ring-2': context.activeSlots().includes(props.index),
},
)}
>
{char()}
<Show when={showFakeCaret()}>
<div class="pointer-events-none flex items-center justify-center">
<div class="h-4 w-px animate-caret-blink bg-corvu-text duration-1000" />
</div>
</Show>
</div>
)
}
export default OtpFieldExample
import corvuPlugin from '@corvu/tailwind'
/** @type {import('tailwindcss').Config} */
export default {
content: ['./src/**/*.{ts,tsx}'],
theme: {
extend: {
colors: {
corvu: {
bg: '#f3f1fe',
100: '#e6e2fd',
200: '#d4cbfb',
300: '#bcacf6',
400: '#a888f1',
text: '#180f24',
},
},
animation: {
'caret-blink': 'caret-blink 1.25s ease-out infinite',
},
keyframes: {
'caret-blink': {
'0%,70%,100%': { opacity: '1' },
'20%,50%': { opacity: '0' },
},
},
},
},
plugins: [corvuPlugin],
}
import { Show, type VoidComponent } from 'solid-js'
import clsx from 'clsx'
import OtpField from '@corvu/otp-field'
const OtpFieldExample: VoidComponent = () => {
return (
<div class="wrapper">
<OtpField maxLength={6}>
<OtpField.Input aria-label="Verification Code" />
<div class="slot_wrapper">
<Slot index={0} />
<Slot index={1} />
<Slot index={2} />
</div>
<div class="separator">-</div>
<div class="slot_wrapper">
<Slot index={3} />
<Slot index={4} />
<Slot index={5} />
</div>
</OtpField>
</div>
)
}
const Slot = (props: { index: number }) => {
const context = OtpField.useContext()
const char = () => context.value()[props.index]
const showFakeCaret = () =>
context.value().length === props.index && context.isInserting()
return (
<div
class={clsx('slot', {
active_slot: context.activeSlots().includes(props.index),
})}
>
{char()}
<Show when={showFakeCaret()}>
<div class="fake_caret_wrapper">
<div class="fake_caret" />
</div>
</Show>
</div>
)
}
export default OtpFieldExample
.wrapper {
display: flex;
width: 100%;
height: 100%;
align-items: center;
justify-content: center;
}
[data-corvu-otp-field-root] {
display: flex;
}
.slot_wrapper {
display: flex;
align-items: center;
}
.slot_wrapper > * {
margin-right: 0.5rem;
margin-left: 0.5rem;
}
.separator {
display: flex;
width: 2.5rem;
height: 2.5rem;
align-items: center;
justify-content: center;
font-weight: 700;
}
.slot {
display: flex;
width: 2.5rem;
height: 2.5rem;
align-items: center;
justify-content: center;
border-radius: 0.375rem;
background-color: hsl(249, 87%, 94%);
font-size: 0.875rem;
line-height: 1.25rem;
font-weight: 700;
transition-property: all;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
.active_slot {
box-shadow: 0 0 0 2px hsl(267, 40%, 10%);
}
.fake_caret_wrapper {
pointer-events: none;
display: flex;
align-items: center;
justify-content: center;
}
.fake_caret {
height: 1rem;
width: 1px;
animation: caret-blink 1.25s ease-out infinite;
transition-duration: 1000ms;
background-color: hsl(267, 40%, 10%);
}
@keyframes caret-blink {
0%,70%,100% {
opacity: 1;
}
20%,50% {
opacity: 0;
}
}
Features Section titled Features
- Renders as a single input; Supports all default keybindings and is accessible
- Supports Android and iOS copy, paste, and cut
- Automatically removes visual separators when pasting (e.g. 123-456 -> 123456)
- Can retrieve OTP codes from out-of-channel mechanisms, such as SMS, email, or auth apps.
Installation Section titled Installation
npm install @corvu/otp-field
The OTP Field is also included in the main corvu
package under corvu/otp-field
.
Usage Section titled Usage
import OtpField from '@corvu/otp-field' // 'corvu/otp-field'
// or
// import { Root, Input } from '@corvu/otp-field'
The OTP Field primitive renders an invisible input field (<OtpField.Input />
) on top of the visible slots. This allows you to style the input field as you like while keeping the input field accessible. corvu takes care of syncing the selection, focus and hover states between the input and your UI.
Anatomy Section titled Anatomy
<OtpField>
<OtpField.Input />
</OtpField>
Basic Usage Section titled Basic Usage
A simple usage of the OTP Field, using the context to manage the state of the UI:
const OtpFieldExample = () => {
return (
<div>
<OtpField maxLength={6}>
<OtpField.Input aria-label="Verification Code" />
<Slot index={0} />
<Slot index={1} />
<Slot index={2} />
<Slot index={3} />
<Slot index={4} />
<Slot index={5} />
</OtpField>
</div>
)
}
const Slot = (props: { index: number }) => {
const context = OtpField.useContext()
const char = () => context.value()[props.index]
const showFakeCaret = () =>
context.value().length === props.index && context.isInserting()
return (
<div>
{char()}
<Show when={showFakeCaret()}>
<span>|</span>
</Show>
</div>
)
}
Integration with form libraries Section titled Integration with form libraries
The OTP Field can easily be used with form libraries like Modular Forms as you have full control over the input field:
import { createForm, submit } from '@modular-forms/solid'
import OtpField from '@corvu/otp-field'
function OtpForm() {
const [form, { Form, Field }] = createForm<{
otp: string
}>()
return (
<Form
onSubmit={(values) => {
// Submit form
}}
>
<Field name="otp">
{(field, props) => (
<OtpField maxLength={6} onComplete={() => submit(form)}>
<OtpField.Input {...props} />
...
</OtpField>
)}
</Field>
</Form>
)
}
The form automatically submits when the OTP Field is filled because we’re calling submit(form)
inside the onComplete
callback.
Accessibility Section titled Accessibility
The OTP Field renders as a single native input element, which means it supports all standard keybindings and is accessible by default.
It is suggested that you add a label to describe the input:
<OtpField maxLength={6}>
<OtpField.Input aria-label="Verification Code" />
</OtpField>
Limitations Section titled Limitations
To ensure accessibility for screen readers assistive technology, the OTP Field renders a single input field that gets placed invisibly above the slots with position: absolute; inset: 0px;
. The slots are visual only, i.e. mouse and touch interactions are restricted. You cannot select a specific field to edit it with the mouse or drag a selection on touch devices.
Credits Section titled Credits
The implementation of this OTP Field is inspired by Guilherme’s React OTP Input.
API reference Section titled API reference
OTP Field root component. Is the wrapper to position the hidden OTP Field input element and provides the context.
Props
Property | Default | Type/Description |
---|---|---|
maxLength | - | number Max number of chars. Is required. |
value | - | string The value of the OTP Field. |
onValueChange | - | (value: string) => void Callback fired when the OTP Field value changes. |
onComplete | - | (value: string) => void Callback fired when the OTP Field is filled. |
shiftPWManagers | true | boolean Whether to create extra space on the right for password managers. |
contextId | - | string The id of the OTP Field context. Useful if you have nested OTP Fields and want to create components that belong to an OTP Field higher up in the tree. |
as | div | ValidComponent Component to render the dynamic component as. |
Data attributes
Data attributes present on <Root />
components.
Property | Description |
---|---|
data-corvu-otp-field-root | Present on every OTP Field root element. |
The hidden input element for the OTP Field.
Props
Property | Default | Type/Description |
---|---|---|
pattern | '^\\d*$' | string | null Regex pattern for the input. null disables the pattern and allow all chars. |
noScriptCSSFallback | - | string | null Override the styles to apply when JavaScript is disabled. corvu provides a default for this but you're free to define your own styling. null disables the fallback. |
contextId | - | string The id of the OTP Field context. Useful if you have nested OTP Fields and want to create components that belong to an OTP Field higher up in the tree. |
as | input | ValidComponent Component to render the dynamic component as. |
Data attributes
Data attributes present on <Input />
components.
Property | Description |
---|---|
data-corvu-otp-field-input | Present on every OTP Field input element. |
Context which exposes various properties to interact with the OTP Field. Optionally provide a contextId to access a keyed context.
Returns
Property | Type/Description |
---|---|
value | Accessor<string> The value of the OTP Field. |
isFocused | Accessor<boolean> Whether the OTP Field is currently focused. |
isHovered | Accessor<boolean> Whether the OTP Field is currently hovered. |
isInserting | Accessor<boolean> Whether the user is currently inserting a value and a fake caret should be shown. |
maxLength | Accessor<number> The maximum number of chars in the OTP Field. |
activeSlots | Accessor<number[]> The currently active slots in the OTP Field. |
shiftPWManagers | Accessor<boolean> Whether to create extra space on the right for password managers. |
Props that are passed to the Root component children callback.
Props
Property | Type/Description |
---|---|
value | string The value of the OTP Field. |
isFocused | boolean Whether the OTP Field is currently focused. |
isHovered | boolean Whether the OTP Field is currently hovered. |
isInserting | boolean Whether the user is currently inserting a value and a fake caret should be shown. |
maxLength | number The maximum number of chars in the OTP Field. |
activeSlots | number[] The currently active slots in the OTP Field. |
shiftPWManagers | boolean Whether to create extra space on the right for password managers. |
Developed and designed by Jasmin