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

  1. 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.

  2. 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.

  3. 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.

  4. 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?").

  5. 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.