diff --git a/packages/react/src/otp-field/input/OTPFieldInput.tsx b/packages/react/src/otp-field/input/OTPFieldInput.tsx
index 68c4259f8..d547e0cf4 100644
--- a/packages/react/src/otp-field/input/OTPFieldInput.tsx
+++ b/packages/react/src/otp-field/input/OTPFieldInput.tsx
@@ -101,8 +101,8 @@ export const OTPFieldInput = React.forwardRef(function OTPFieldInput(
autoCorrect: 'off',
spellCheck: 'false',
enterKeyHint: index === length - 1 ? 'done' : 'next',
- // Allow the first slot to accept a full code so browser paste/autofill can target it directly.
- maxLength: index === 0 ? length : 1,
+ // Only the first slot has a max length to avoid password manager bubbles appearing after later inputs.
+ maxLength: index === 0 ? length : undefined,
tabIndex: activeIndex === index ? 0 : -1,
disabled,
form,
diff --git a/packages/react/src/otp-field/root/OTPFieldRoot.test.tsx b/packages/react/src/otp-field/root/OTPFieldRoot.test.tsx
index f561cccfb..7929f71bc 100644
--- a/packages/react/src/otp-field/root/OTPFieldRoot.test.tsx
+++ b/packages/react/src/otp-field/root/OTPFieldRoot.test.tsx
@@ -71,6 +71,9 @@ describe('', () => {
expect(inputs.map((input) => input.value)).toEqual(['1', '2', '3', '4', '5', '6']);
expect(inputs[0]).toHaveAttribute('maxlength', '6');
+ inputs.slice(1).forEach((input) => {
+ expect(input).not.toHaveAttribute('maxlength');
+ });
expect(hiddenInput).toHaveValue('123456');
});
@@ -804,6 +807,16 @@ describe('', () => {
expect(getValues()).toBe('123456');
});
+
+ it('replaces consecutive slots when typing multiple characters into a later input', async () => {
+ await render();
+
+ const inputs = screen.getAllByRole('textbox');
+
+ fireEvent.change(inputs[2], { target: { value: '99' } });
+
+ expect(getValues()).toBe('129956');
+ });
});
describe('pasting', () => {