Open
Description
Expected Behavior
When using the autoFocus
prop on the CustomOtpInput
component, the first OTP input field should automatically receive focus when the component mounts so that the user can immediately start typing the OTP.
Current Behavior
The autoFocus
prop does not work as expected. Even when set to true, the first OTP input field is not automatically focused when the component is rendered. Attempts to manually focus the input via a ref (or using a setTimeout
inside a useEffect
) have also failed to yield the desired result.
Code Example:
<CustomOtpInput
autoFocus
value={otp}
onChange={(value) => setOtp(value)}
onComplete={handleOtpComplete}
/>
The component code
'use client';
import React, { useEffect, useRef } from 'react';
import { OTPInput, SlotProps } from 'input-otp';
import { clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
// Utility to merge class names
function cn(...inputs: any[]) {
return twMerge(clsx(inputs));
}
// Slot component for each OTP digit
function Slot(props: SlotProps) {
return (
<div
className={cn(
'relative w-10 h-14 text-[2rem]',
'flex items-center justify-center',
'transition-all duration-300',
'border-border border-y border-r first:border-l first:rounded-l-md last:rounded-r-md',
'group-hover:border-border/60 group-focus-within:border-border/60',
'outline outline-0 outline-border/70',
{ 'outline-4 outline-border': props.isActive }
)}
>
<div className="group-has-[input[data-input-otp-placeholder-shown]]:opacity-20">
{props.char ?? props.placeholderChar}
</div>
{props.hasFakeCaret && <FakeCaret />}
</div>
);
}
// Component to emulate a blinking caret
function FakeCaret() {
return (
<div className="absolute pointer-events-none inset-0 flex items-center justify-center animate-caret-blink">
<div className="w-px h-8 bg-foreground" />
</div>
);
}
// Component for the dash separator between groups of OTP slots
function FakeDash() {
return (
<div className="flex w-10 justify-center items-center">
<div className="w-3 h-1 rounded-full bg-border" />
</div>
);
}
// Define the props for our OTP input component
interface OtpInputProps {
value: string;
onChange: (value: string) => void;
maxLength?: number;
containerClassName?: string;
autoFocus?: boolean;
onComplete?: () => void;
}
// The custom OTP input component
const CustomOtpInput: React.FC<OtpInputProps> = ({
value,
onChange,
maxLength = 6,
containerClassName = "group flex items-center has-[:disabled]:opacity-30",
autoFocus = true,
onComplete,
}) => {
// Create a ref for the container wrapping the OTPInput
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (autoFocus && containerRef.current) {
// Wait a tick to ensure the inputs are rendered
setTimeout(() => {
// Query for the first <input> element inside the container
const input = containerRef.current!.querySelector('input');
if (input) {
input.focus();
}
}, 0);
}
}, [autoFocus]);
return (
<div ref={containerRef}>
<OTPInput
autoFocus={autoFocus}
onComplete={onComplete}
maxLength={maxLength}
value={value}
onChange={onChange}
containerClassName={containerClassName}
render={({ slots }) => (
<>
<div className="flex">
{slots.slice(0, 3).map((slot, idx) => (
<Slot key={idx} {...slot} />
))}
</div>
<FakeDash />
<div className="flex">
{slots.slice(3).map((slot, idx) => (
<Slot key={idx} {...slot} />
))}
</div>
</>
)}
/>
</div>
);
};
export default CustomOtpInput;
Activity