From 8a299d423efb89ef01ae6b053f98551ee7705d0d Mon Sep 17 00:00:00 2001
From: Muhammad Umer Aamir <93091750+umeraamir69@users.noreply.github.com>
Date: Thu, 22 Oct 2026 10:53:28 -0400
Subject: [PATCH 1/4] Set autocomplete on SignIn and SignUp identifier fields.
Email, phone, username, and name fields had no autocomplete tokens, so Safari and password managers would not offer the user's details.
---
.changeset/signin-signup-autocomplete.md | 5 ++
.../ui/src/components/SignIn/SignInStart.tsx | 7 ++-
.../SignIn/__tests__/SignInStart.test.tsx | 50 +++++++++++++++++++
.../ui/src/components/SignUp/SignUpForm.tsx | 5 ++
.../SignUp/__tests__/SignUpStart.test.tsx | 15 ++++++
5 files changed, 81 insertions(+), 1 deletion(-)
create mode 100644 .changeset/signin-signup-autocomplete.md
diff --git a/.changeset/signin-signup-autocomplete.md b/.changeset/signin-signup-autocomplete.md
new file mode 100644
index 00000000000..f40a4920307
--- /dev/null
+++ b/.changeset/signin-signup-autocomplete.md
@@ -0,0 +1,5 @@
+---
+'@clerk/ui': patch
+---
+
+Sign-in and sign-up identifier fields now set standard `autocomplete` tokens so browsers and password managers can offer email, name, and phone.
diff --git a/packages/ui/src/components/SignIn/SignInStart.tsx b/packages/ui/src/components/SignIn/SignInStart.tsx
index 4e3ae9bf6ff..3a42ad78fa4 100644
--- a/packages/ui/src/components/SignIn/SignInStart.tsx
+++ b/packages/ui/src/components/SignIn/SignInStart.tsx
@@ -57,6 +57,11 @@ import {
getSignUpAttributeFromIdentifier,
} from './utils';
+function identifierAutoComplete(identifier: SignInStartIdentifier, webauthn: boolean): string {
+ const token = identifier === 'phone_number' ? 'tel' : identifier === 'email_address' ? 'email' : 'username';
+ return webauthn ? `${token} webauthn` : token;
+}
+
const useAutoFillPasskey = () => {
const [isSupported, setIsSupported] = useState(false);
const { navigate } = useRouter();
@@ -671,7 +676,7 @@ function SignInStartInternal(): JSX.Element {
onActionClicked={switchToNextIdentifier}
{...identifierFieldProps}
autoFocus={shouldAutofocus}
- autoComplete={isWebAuthnAutofillSupported ? 'webauthn' : undefined}
+ autoComplete={identifierAutoComplete(identifierAttribute, isWebAuthnAutofillSupported)}
isLastAuthenticationStrategy={isIdentifierLastAuthenticationStrategy}
/>
diff --git a/packages/ui/src/components/SignIn/__tests__/SignInStart.test.tsx b/packages/ui/src/components/SignIn/__tests__/SignInStart.test.tsx
index 1da263fbd58..365659b65e7 100644
--- a/packages/ui/src/components/SignIn/__tests__/SignInStart.test.tsx
+++ b/packages/ui/src/components/SignIn/__tests__/SignInStart.test.tsx
@@ -76,6 +76,37 @@ describe('SignInStart', () => {
screen.getByText(/email address/i);
});
+ it('sets autocomplete=email on an email-only identifier field', async () => {
+ const { wrapper } = await createFixtures(f => {
+ f.withEmailAddress();
+ });
+
+ render(, { wrapper });
+ expect(screen.getByRole('textbox', { name: /email address/i })).toHaveAttribute('autocomplete', 'email');
+ });
+
+ it('sets autocomplete=username when email and username are both enabled', async () => {
+ const { wrapper } = await createFixtures(f => {
+ f.withEmailAddress();
+ f.withUsername();
+ });
+
+ render(, { wrapper });
+ expect(screen.getByRole('textbox', { name: /email address or username/i })).toHaveAttribute(
+ 'autocomplete',
+ 'username',
+ );
+ });
+
+ it('sets autocomplete=tel on a phone identifier field', async () => {
+ const { wrapper } = await createFixtures(f => {
+ f.withPhoneNumber();
+ });
+
+ render(, { wrapper });
+ expect(screen.getByRole('textbox', { name: /phone number/i })).toHaveAttribute('autocomplete', 'tel');
+ });
+
it('enables login with username', async () => {
const { wrapper } = await createFixtures(f => {
f.withUsername();
@@ -131,6 +162,25 @@ describe('SignInStart', () => {
screen.getByText('Use passkey instead');
});
+ it('puts webauthn after the field name when passkey autofill is on', async () => {
+ const { wrapper } = await createFixtures(f => {
+ f.withEmailAddress();
+ f.withPasskey();
+ f.withPasskeySettings({
+ allow_autofill: true,
+ show_sign_in_button: false,
+ });
+ });
+
+ render(, { wrapper });
+ await waitFor(() => {
+ expect(screen.getByRole('textbox', { name: /email address/i })).toHaveAttribute(
+ 'autocomplete',
+ 'email webauthn',
+ );
+ });
+ });
+
it('enables login with passkey via autofill', async () => {
const { wrapper, fixtures } = await createFixtures(f => {
f.withEmailAddress();
diff --git a/packages/ui/src/components/SignUp/SignUpForm.tsx b/packages/ui/src/components/SignUp/SignUpForm.tsx
index e36a979e464..649b1817c46 100644
--- a/packages/ui/src/components/SignUp/SignUpForm.tsx
+++ b/packages/ui/src/components/SignUp/SignUpForm.tsx
@@ -59,6 +59,7 @@ export const SignUpForm = (props: SignUpFormProps) => {
{...formState.firstName.props}
isRequired={fields.firstName?.required}
isOptional={!fields.firstName?.required}
+ autoComplete='given-name'
/>
)}
{shouldShow('lastName') && (
@@ -66,6 +67,7 @@ export const SignUpForm = (props: SignUpFormProps) => {
{...formState.lastName.props}
isRequired={fields.lastName?.required}
isOptional={!fields.lastName?.required}
+ autoComplete='family-name'
/>
)}
@@ -76,6 +78,7 @@ export const SignUpForm = (props: SignUpFormProps) => {
{...formState.username.props}
isRequired={fields.username?.required}
isOptional={!fields.username?.required}
+ autoComplete='username'
/>
)}
@@ -86,6 +89,7 @@ export const SignUpForm = (props: SignUpFormProps) => {
isRequired={fields.emailAddress?.required}
isOptional={!fields.emailAddress?.required}
isDisabled={fields.emailAddress?.disabled}
+ autoComplete='email'
actionLabel={canToggleEmailPhone ? localizationKeys('signUp.start.actionLink__use_phone') : undefined}
onActionClicked={canToggleEmailPhone ? () => handleEmailPhoneToggle('phoneNumber') : undefined}
/>
@@ -97,6 +101,7 @@ export const SignUpForm = (props: SignUpFormProps) => {
{...formState.phoneNumber.props}
isRequired={fields.phoneNumber?.required}
isOptional={!fields.phoneNumber?.required}
+ autoComplete='tel'
actionLabel={canToggleEmailPhone ? localizationKeys('signUp.start.actionLink__use_email') : undefined}
onActionClicked={canToggleEmailPhone ? () => handleEmailPhoneToggle('emailAddress') : undefined}
/>
diff --git a/packages/ui/src/components/SignUp/__tests__/SignUpStart.test.tsx b/packages/ui/src/components/SignUp/__tests__/SignUpStart.test.tsx
index eb5fb3f4739..f7adfc1457b 100644
--- a/packages/ui/src/components/SignUp/__tests__/SignUpStart.test.tsx
+++ b/packages/ui/src/components/SignUp/__tests__/SignUpStart.test.tsx
@@ -31,6 +31,21 @@ describe('SignUpStart', () => {
screen.getByText('Email address');
});
+ it('sets standard autocomplete tokens on sign-up fields', async () => {
+ const { wrapper } = await createFixtures(f => {
+ f.withEmailAddress({ required: true });
+ f.withPhoneNumber({ required: true });
+ f.withUsername({ required: true });
+ f.withName({ required: true });
+ });
+ render(, { wrapper });
+ expect(screen.getByRole('textbox', { name: 'First name' })).toHaveAttribute('autocomplete', 'given-name');
+ expect(screen.getByRole('textbox', { name: 'Last name' })).toHaveAttribute('autocomplete', 'family-name');
+ expect(screen.getByLabelText('Email address', { selector: 'input' })).toHaveAttribute('autocomplete', 'email');
+ expect(screen.getByLabelText('Phone number', { selector: 'input' })).toHaveAttribute('autocomplete', 'tel');
+ expect(screen.getByRole('textbox', { name: 'Username' })).toHaveAttribute('autocomplete', 'username');
+ });
+
it('enables sign up with phone number', async () => {
const { wrapper } = await createFixtures(f => {
f.withPhoneNumber({ required: true });
From 2c6eaea224b86bbb6f51913d76e1844564a85b67 Mon Sep 17 00:00:00 2001
From: Alex Carpenter
Date: Thu, 22 Oct 2026 15:12:20 -0400
Subject: [PATCH 2/4] refactor(ui): Move sign-in identifier autocomplete into
FirstFactorConfigs
Co-authored-by: Muhammad Umer Aamir <93091750+umeraamir69@users.noreply.github.com>
---
packages/ui/src/common/constants.ts | 6 ++++++
packages/ui/src/components/SignIn/SignInStart.tsx | 11 +++++------
.../components/SignIn/__tests__/SignInStart.test.tsx | 2 +-
3 files changed, 12 insertions(+), 7 deletions(-)
diff --git a/packages/ui/src/common/constants.ts b/packages/ui/src/common/constants.ts
index 4b291268a1e..00a32c56a2b 100644
--- a/packages/ui/src/common/constants.ts
+++ b/packages/ui/src/common/constants.ts
@@ -6,6 +6,7 @@ import { localizationKeys } from '../localization/localizationKeys';
type FirstFactorConfig = {
label: string | LocalizationKey;
type: string;
+ autoComplete: 'email' | 'tel' | 'username';
placeholder: string | LocalizationKey;
action?: string | LocalizationKey;
validLastAuthenticationStrategies: ReadonlySet;
@@ -15,6 +16,7 @@ const FirstFactorConfigs = Object.freeze({
label: localizationKeys('formFieldLabel__emailAddress_username'),
placeholder: localizationKeys('formFieldInputPlaceholder__emailAddress_username'),
type: 'text',
+ autoComplete: 'username',
action: localizationKeys('signIn.start.actionLink__use_email_username'),
validLastAuthenticationStrategies: new Set([
'email_code',
@@ -28,6 +30,7 @@ const FirstFactorConfigs = Object.freeze({
label: localizationKeys('formFieldLabel__emailAddress'),
placeholder: localizationKeys('formFieldInputPlaceholder__emailAddress'),
type: 'email',
+ autoComplete: 'email',
action: localizationKeys('signIn.start.actionLink__use_email'),
validLastAuthenticationStrategies: new Set([
'email_code',
@@ -40,6 +43,7 @@ const FirstFactorConfigs = Object.freeze({
label: localizationKeys('formFieldLabel__phoneNumber'),
placeholder: localizationKeys('formFieldInputPlaceholder__phoneNumber'),
type: 'tel',
+ autoComplete: 'tel',
action: localizationKeys('signIn.start.actionLink__use_phone'),
validLastAuthenticationStrategies: new Set(['phone_code', 'password']),
},
@@ -47,6 +51,7 @@ const FirstFactorConfigs = Object.freeze({
label: localizationKeys('formFieldLabel__username'),
placeholder: localizationKeys('formFieldInputPlaceholder__username'),
type: 'text',
+ autoComplete: 'username',
action: localizationKeys('signIn.start.actionLink__use_username'),
validLastAuthenticationStrategies: new Set(['username', 'password']),
},
@@ -54,6 +59,7 @@ const FirstFactorConfigs = Object.freeze({
label: '',
placeholder: '',
type: 'text',
+ autoComplete: 'username',
action: '',
validLastAuthenticationStrategies: new Set(),
},
diff --git a/packages/ui/src/components/SignIn/SignInStart.tsx b/packages/ui/src/components/SignIn/SignInStart.tsx
index 3a42ad78fa4..d3e9d806bcb 100644
--- a/packages/ui/src/components/SignIn/SignInStart.tsx
+++ b/packages/ui/src/components/SignIn/SignInStart.tsx
@@ -57,11 +57,6 @@ import {
getSignUpAttributeFromIdentifier,
} from './utils';
-function identifierAutoComplete(identifier: SignInStartIdentifier, webauthn: boolean): string {
- const token = identifier === 'phone_number' ? 'tel' : identifier === 'email_address' ? 'email' : 'username';
- return webauthn ? `${token} webauthn` : token;
-}
-
const useAutoFillPasskey = () => {
const [isSupported, setIsSupported] = useState(false);
const { navigate } = useRouter();
@@ -676,7 +671,11 @@ function SignInStartInternal(): JSX.Element {
onActionClicked={switchToNextIdentifier}
{...identifierFieldProps}
autoFocus={shouldAutofocus}
- autoComplete={identifierAutoComplete(identifierAttribute, isWebAuthnAutofillSupported)}
+ autoComplete={
+ isWebAuthnAutofillSupported
+ ? `${currentIdentifier.autoComplete} webauthn`
+ : currentIdentifier.autoComplete
+ }
isLastAuthenticationStrategy={isIdentifierLastAuthenticationStrategy}
/>
diff --git a/packages/ui/src/components/SignIn/__tests__/SignInStart.test.tsx b/packages/ui/src/components/SignIn/__tests__/SignInStart.test.tsx
index 365659b65e7..5e4e648301e 100644
--- a/packages/ui/src/components/SignIn/__tests__/SignInStart.test.tsx
+++ b/packages/ui/src/components/SignIn/__tests__/SignInStart.test.tsx
@@ -113,7 +113,7 @@ describe('SignInStart', () => {
});
render(, { wrapper });
- screen.getByText(/username/i);
+ expect(screen.getByRole('textbox', { name: /username/i })).toHaveAttribute('autocomplete', 'username');
});
it('enables login with phone number', async () => {
From 5f02b5e817e544e7e56a5c1049108eabbd55a9f4 Mon Sep 17 00:00:00 2001
From: Alex Carpenter
Date: Thu, 22 Oct 2026 15:20:30 -0400
Subject: [PATCH 3/4] refactor(ui): Type-check FirstFactorConfigs entries with
satisfies
---
packages/ui/src/common/constants.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/ui/src/common/constants.ts b/packages/ui/src/common/constants.ts
index 00a32c56a2b..15ede217b38 100644
--- a/packages/ui/src/common/constants.ts
+++ b/packages/ui/src/common/constants.ts
@@ -63,7 +63,7 @@ const FirstFactorConfigs = Object.freeze({
action: '',
validLastAuthenticationStrategies: new Set(),
},
-} as Record);
+} satisfies Record);
export type SignInStartIdentifier = 'email_address' | 'username' | 'phone_number' | 'email_address_username';
export const groupIdentifiers = (attributes: Attribute[]): SignInStartIdentifier[] => {
From 8cbeb105da5e385593f911777ce7d25940367360 Mon Sep 17 00:00:00 2001
From: Alex Carpenter
Date: Thu, 22 Oct 2026 15:30:56 -0400
Subject: [PATCH 4/4] test(ui): Assert webauthn autocomplete in the mocked
passkey autofill test
---
.../SignIn/__tests__/SignInStart.test.tsx | 23 ++++---------------
1 file changed, 4 insertions(+), 19 deletions(-)
diff --git a/packages/ui/src/components/SignIn/__tests__/SignInStart.test.tsx b/packages/ui/src/components/SignIn/__tests__/SignInStart.test.tsx
index 5e4e648301e..fe66c6e12db 100644
--- a/packages/ui/src/components/SignIn/__tests__/SignInStart.test.tsx
+++ b/packages/ui/src/components/SignIn/__tests__/SignInStart.test.tsx
@@ -162,25 +162,6 @@ describe('SignInStart', () => {
screen.getByText('Use passkey instead');
});
- it('puts webauthn after the field name when passkey autofill is on', async () => {
- const { wrapper } = await createFixtures(f => {
- f.withEmailAddress();
- f.withPasskey();
- f.withPasskeySettings({
- allow_autofill: true,
- show_sign_in_button: false,
- });
- });
-
- render(, { wrapper });
- await waitFor(() => {
- expect(screen.getByRole('textbox', { name: /email address/i })).toHaveAttribute(
- 'autocomplete',
- 'email webauthn',
- );
- });
- });
-
it('enables login with passkey via autofill', async () => {
const { wrapper, fixtures } = await createFixtures(f => {
f.withEmailAddress();
@@ -202,6 +183,10 @@ describe('SignInStart', () => {
flow: 'autofill',
});
});
+ expect(screen.getByRole('textbox', { name: /email address/i })).toHaveAttribute(
+ 'autocomplete',
+ 'email webauthn',
+ );
});
it('does not display related-origin errors from passkey autofill', async () => {