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 () => {