Docs
OTP Field
OTP Field
An accessible and customizable OTP Input component.
import {
OTPField,
OTPFieldGroup,
OTPFieldInput,
OTPFieldSeparator,
OTPFieldSlot,
} from "@repo/tailwindcss/ui/otp-field";
const OtpFieldDemo = () => {
return (
<OTPField maxLength={6}>
<OTPFieldInput />
<OTPFieldGroup>
<OTPFieldSlot index={0} />
<OTPFieldSlot index={1} />
<OTPFieldSlot index={2} />
</OTPFieldGroup>
<OTPFieldSeparator />
<OTPFieldGroup>
<OTPFieldSlot index={3} />
<OTPFieldSlot index={4} />
<OTPFieldSlot index={5} />
</OTPFieldGroup>
</OTPField>
);
};
export default OtpFieldDemo;
Installation
npx shadcn-solid@latest add otp-field
Install the following dependencies:
npm install @corvu/otp-field
Copy and paste the following code into your project:
import { cn } from "@/libs/cn";
import type { DynamicProps, RootProps } from "@corvu/otp-field";
import OTPFieldPrimitive from "@corvu/otp-field";
import type { ComponentProps, ValidComponent } from "solid-js";
import { Show, splitProps } from "solid-js";
export const OTPFieldInput = OTPFieldPrimitive.Input;
type OTPFieldProps<T extends ValidComponent = "div"> = RootProps<T> & {
class?: string;
};
export const OTPField = <T extends ValidComponent = "div">(
props: DynamicProps<T, OTPFieldProps<T>>,
) => {
const [local, rest] = splitProps(props, ["class"]);
return (
<OTPFieldPrimitive
class={cn(
"flex items-center gap-2 has-[:disabled]:opacity-50",
local.class,
)}
{...rest}
/>
);
};
export const OTPFieldGroup = (props: ComponentProps<"div">) => {
const [local, rest] = splitProps(props, ["class"]);
return <div class={cn("flex items-center", local.class)} {...rest} />;
};
export const OTPFieldSeparator = (props: ComponentProps<"div">) => {
return (
// biome-ignore lint/a11y/useAriaPropsForRole: []
<div role="separator" {...props}>
<svg
xmlns="http://www.w3.org/2000/svg"
class="size-4"
viewBox="0 0 15 15"
>
<title>Separator</title>
<path
fill="currentColor"
fill-rule="evenodd"
d="M5 7.5a.5.5 0 0 1 .5-.5h4a.5.5 0 0 1 0 1h-4a.5.5 0 0 1-.5-.5"
clip-rule="evenodd"
/>
</svg>
</div>
);
};
export const OTPFieldSlot = (
props: ComponentProps<"div"> & { index: number },
) => {
const [local, rest] = splitProps(props, ["class", "index"]);
const context = OTPFieldPrimitive.useContext();
const char = () => context.value()[local.index];
const hasFakeCaret = () =>
context.value().length === local.index && context.isInserting();
const isActive = () => context.activeSlots().includes(local.index);
return (
<div
class={cn(
"relative flex size-9 items-center justify-center border-y border-r border-input text-sm shadow-sm transition-shadow first:rounded-l-md first:border-l last:rounded-r-md",
isActive() && "z-10 ring-[1.5px] ring-ring",
local.class,
)}
{...rest}
>
{char()}
<Show when={hasFakeCaret()}>
<div class="pointer-events-none absolute inset-0 flex items-center justify-center">
<div class="h-4 w-px animate-caret-blink bg-foreground" />
</div>
</Show>
</div>
);
};
Update config:
/** @type {import('tailwindcss').Config} */
module.exports = {
theme: {
extend: {
keyframes: {
"accordion-down": {
from: { height: 0 },
to: { height: "var(--kb-accordion-content-height)" }
},
"accordion-up": {
from: { height: "var(--kb-accordion-content-height)" },
to: { height: 0 }
}
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out"
}
}
}
};
Install the following dependencies:
npm install @corvu/otp-field
Copy and paste the following code into your project:
import { cn } from "@/libs/cn";
import type { DynamicProps, RootProps } from "@corvu/otp-field";
import OTPFieldPrimitive from "@corvu/otp-field";
import type { ComponentProps, ValidComponent } from "solid-js";
import { Show, splitProps } from "solid-js";
export const OTPFieldInput = OTPFieldPrimitive.Input;
type OTPFieldProps<T extends ValidComponent = "div"> = RootProps<T> & {
class?: string;
};
export const OTPField = <T extends ValidComponent = "div">(
props: DynamicProps<T, OTPFieldProps<T>>,
) => {
const [local, rest] = splitProps(props, ["class"]);
return (
<OTPFieldPrimitive
class={cn(
"flex items-center gap-2 has-[:disabled]:opacity-50",
local.class,
)}
{...rest}
/>
);
};
export const OTPFieldGroup = (props: ComponentProps<"div">) => {
const [local, rest] = splitProps(props, ["class"]);
return <div class={cn("flex items-center", local.class)} {...rest} />;
};
export const OTPFieldSeparator = (props: ComponentProps<"div">) => {
return (
// biome-ignore lint/a11y/useAriaPropsForRole: []
<div role="separator" {...props}>
<svg
xmlns="http://www.w3.org/2000/svg"
class="size-4"
viewBox="0 0 15 15"
>
<title>Separator</title>
<path
fill="currentColor"
fill-rule="evenodd"
d="M5 7.5a.5.5 0 0 1 .5-.5h4a.5.5 0 0 1 0 1h-4a.5.5 0 0 1-.5-.5"
clip-rule="evenodd"
/>
</svg>
</div>
);
};
export const OTPFieldSlot = (
props: ComponentProps<"div"> & { index: number },
) => {
const [local, rest] = splitProps(props, ["class", "index"]);
const context = OTPFieldPrimitive.useContext();
const char = () => context.value()[local.index];
const hasFakeCaret = () =>
context.value().length === local.index && context.isInserting();
const isActive = () => context.activeSlots().includes(local.index);
return (
<div
class={cn(
"relative flex size-9 items-center justify-center border-y border-r border-input text-sm shadow-sm transition-all first:(rounded-l-md border-l) last:rounded-r-md",
isActive() && "z-10 ring-1.5 ring-ring",
local.class,
)}
{...rest}
>
{char()}
<Show when={hasFakeCaret()}>
<div class="pointer-events-none absolute inset-0 flex items-center justify-center">
<div class="h-4 w-px animate-caret-blink bg-foreground" />
</div>
</Show>
</div>
);
};
Update config:
export default defineConfig({
themes: {
animation: {
keyframes: {
"caret-blink": "{ 0%,70%,100% { opacity: 1 } 20%,50% { opacity: 0 } }"
},
timingFns: {
"caret-blink": "ease-out"
},
durations: {
"caret-blink": "1.25s"
},
counts: {
"caret-blink": "infinite"
}
}
}
});
Usage
import {
OTPField,
OTPFieldGroup,
OTPFieldInput,
OTPFieldSeparator,
OTPFieldSlot
} from "@/components/ui/otp-field";
<OTPField maxLength={6}>
<OTPFieldInput />
<OTPFieldGroup>
<OTPFieldSlot index={0} />
<OTPFieldSlot index={1} />
<OTPFieldSlot index={2} />
</OTPFieldGroup>
<OTPFieldSeparator />
<OTPFieldGroup>
<OTPFieldSlot index={3} />
<OTPFieldSlot index={4} />
<OTPFieldSlot index={5} />
</OTPFieldGroup>
</OTPField>
Exmaples
Pattern
Use the pattern
prop to define a custom pattern for the OTP field.
import {
OTPField,
OTPFieldGroup,
OTPFieldInput,
OTPFieldSlot,
} from "@repo/tailwindcss/ui/otp-field";
const OTPFieldWithPatternDemo = () => {
return (
<OTPField maxLength={6}>
<OTPFieldInput pattern="^[a-zA-Z0-9]*$" />
<OTPFieldGroup>
<OTPFieldSlot index={0} />
<OTPFieldSlot index={1} />
<OTPFieldSlot index={2} />
<OTPFieldSlot index={3} />
<OTPFieldSlot index={4} />
<OTPFieldSlot index={5} />
</OTPFieldGroup>
</OTPField>
);
};
export default OTPFieldWithPatternDemo;
Controlled
You can use the value
and onValueChange
props to control the input value.
import {
OTPField,
OTPFieldGroup,
OTPFieldInput,
OTPFieldSlot,
} from "@repo/tailwindcss/ui/otp-field";
import { Show, createSignal } from "solid-js";
const OtpFieldWithControlledDemo = () => {
const [value, setValue] = createSignal<string>();
return (
<div class="flex flex-col items-center gap-2">
<OTPField maxLength={6} value={value()} onValueChange={setValue}>
<OTPFieldInput />
<OTPFieldGroup>
<OTPFieldSlot index={0} />
<OTPFieldSlot index={1} />
<OTPFieldSlot index={2} />
<OTPFieldSlot index={3} />
<OTPFieldSlot index={4} />
<OTPFieldSlot index={5} />
</OTPFieldGroup>
</OTPField>
<span class="text-center text-sm">
<Show fallback="Enter your one-time password." when={value()}>
You entered: {value()}
</Show>
</span>
</div>
);
};
export default OtpFieldWithControlledDemo;