Dev view
Login Form
The canonical credentials surface — a `<form>` with an identifier field (email or username), a password field with a visibility toggle, a primary submit button, and secondary affordances (forgot-password link, sign-up link). Composes TextInput twice, Button, and Link. Single-page variant is canonical; split-page ("email first, password on next screen") is documented as a decision rather than a separate pattern.
Composition
-
Text Input identifier field (email or username) Use `type="email"` with `autocomplete="email"` for the contemporary canonical login. For dual identifier flows (email or username accepted), use `type="text"` with `autocomplete="username"` and accept any string. Initial focus lands here unless the page is loaded with the field pre-filled by autofill.
-
Text Input password field Use `type="password"` with `autocomplete="current-password"` for the login flow specifically (signup uses `new-password`). The trailing-icon slot hosts the visibility toggle as a real `<button type="button">` — never the password input itself with `type` flipping. The `required` attribute pairs with a visible required indicator.
-
Button primary action (submit) Use `variant="primary"` and `type="submit"`. The button label names the action ("Sign in", "Log in") — never generic "Submit" or "OK". The button must be inside the `<form>` so Enter on either field submits.
-
Link forgot-password recovery Use `variant="inline"` placed near the password field (above it or to the right). Routes to the password-recovery flow. Distinguished from the sign-up link by position and by the verb of its text ("Forgot your password?").
-
Link alternate-flow link (sign up / register) Use `variant="inline"` placed near the submit button (typically below it). Routes to the registration flow when the user has no account. Optional — omit if the product has no public sign-up (B2B SaaS with admin-provisioned accounts).
Framework skeletons
Composition shape only. Each library will diverge in API surface (see the linked component pages for divergence audits).
Web Components
<form name="login" method="post" action="/login"> <ui-text-input type="email" name="email" label="Email" autocomplete="email" required ></ui-text-input>
<ui-text-input type="password" name="password" label="Password" autocomplete="current-password" required has-trailing-icon > <button slot="trailing-icon" type="button" aria-pressed="false" aria-label="Show password" data-password-toggle> <!-- eye icon --> </button> </ui-text-input>
<ui-link href="/forgot-password" variant="inline"> Forgot your password? </ui-link>
<ui-button variant="primary" type="submit">Sign in</ui-button>
<p> New here? <ui-link href="/sign-up" variant="inline">Create an account</ui-link> </p></form>React
<form name="login" method="post" action="/login" onSubmit={handleSubmit}> <TextInput type="email" name="email" label="Email" autoComplete="email" required value={email} onChange={setEmail} errorMessage={errors.email} />
<TextInput type="password" name="password" label="Password" autoComplete="current-password" required value={password} onChange={setPassword} errorMessage={errors.password} trailingIcon={ <Button variant="tertiary" type="button" aria-pressed={showPassword} aria-label={showPassword ? "Hide password" : "Show password"} onClick={() => setShowPassword(p => !p)}> {showPassword ? <EyeOff /> : <Eye />} </Button> } />
<Link href="/forgot-password" variant="inline"> Forgot your password? </Link>
<Button variant="primary" type="submit">Sign in</Button>
<p> New here? <Link href="/sign-up" variant="inline">Create an account</Link> </p></form>Vue
<form name="login" method="post" action="/login" @submit.prevent="onSubmit"> <TextInput type="email" name="email" label="Email" autocomplete="email" required v-model="email" :error-message="errors.email" />
<TextInput type="password" name="password" label="Password" autocomplete="current-password" required v-model="password" :error-message="errors.password" has-trailing-icon > <template #trailing-icon> <button type="button" :aria-pressed="showPassword" :aria-label="showPassword ? 'Hide password' : 'Show password'" @click="showPassword = !showPassword"> <component :is="showPassword ? EyeOff : Eye" /> </button> </template> </TextInput>
<Link href="/forgot-password" variant="inline"> Forgot your password? </Link>
<Button variant="primary" type="submit">Sign in</Button>
<p> New here? <Link href="/sign-up" variant="inline">Create an account</Link> </p></form>Angular (signals)
<form name="login" [formGroup]="form" (ngSubmit)="onSubmit()"> <ui-text-input type="email" formControlName="email" label="Email" autocomplete="email" required [errorMessage]="form.controls.email.errors?.['message']" />
<ui-text-input type="password" formControlName="password" label="Password" autocomplete="current-password" required [errorMessage]="form.controls.password.errors?.['message']" hasTrailingIcon > <button ng-content trailing-icon type="button" [attr.aria-pressed]="showPassword()" [attr.aria-label]="showPassword() ? 'Hide password' : 'Show password'" (click)="showPassword.set(!showPassword())"> <ui-icon [name]="showPassword() ? 'eye-off' : 'eye'" /> </button> </ui-text-input>
<ui-link href="/forgot-password" variant="inline"> Forgot your password? </ui-link>
<ui-button variant="primary" type="submit">Sign in</ui-button>
<p> New here? <ui-link href="/sign-up" variant="inline">Create an account</ui-link> </p></form>Notes
This is the second canonical pattern after Confirmation Flow. Triangulated against APG (textbox + button patterns), HTML spec on `autocomplete`, NIST SP 800-63B (password guidance + reveal- is-allowed), OWASP ASVS 2.1 (authentication), Polaris Account pattern, Material 3 Sign-in Form, and the auth0 / Okta / WorkOS engineering blogs on enumeration-resistant login forms. Library APIs and security guidance evolve; re-validate against current docs and threat-modelling discipline before citing in production reviews. Sign-up Form, Password Recovery Flow, and Magic-Link Login are sibling patterns deferred to follow-up work.