⚠️ Design system CSS not found. Check the path in cms/docs.config.js → designSystemPath, then re-run npm run docgen.

Login

Authentication forms: sign in, sign up, reset, one-time code

Website / Login
Download .md file
Open .md in new tab
On this page
  • Anatomy
  • Basic usage
  • Create account
  • Forgot and reset password
  • Magic link
  • One-time code
  • Social sign-in
  • JavaScript
  • Accessibility
  • Usage rules
  • CSS reference
  • Use in another product

A login is the authentication stack: a header with logo and title, one or more form panes, and trailing text actions. The class is thin by design — it composes Card, the form primitives, Password Toggle and Button rather than restyling them. It ships no JavaScript of its own: switching between panes (sign in, reset, one-time code) is consumer JS toggling , and password reveal is password-toggle.js.

Centering on the page belongs to the consuming layout, not the component. caps its own width and stacks its children; where it sits is the page's decision.


Anatomy

Class Role
Root column — width cap, gap-based stack
Logo, title and subtitle, centered
Logo slot; inherits currentColor
The heading
Supporting line; collapses when empty
One form pane — a page may hold several, one visible at a time
Row between fields and submit: remember-me against forgot-password
Form-level live region for error or success messages
Trailing text actions below a pane

Basic usage

Sign in with email and password. The pane's own gap owns the spacing between groups; inside a group, the label-to-input rhythm is tighter. Wrap the stack in a when the page wants a panel.

Welcome back

Sign in to your account

<div class="card login">
  <div class="login-header">
    <div class="login-logo" role="img" aria-label="By Default logo">
      <!-- inline logo SVG -->
    </div>
    <h1 class="login-title">Welcome back</h1>
    <p class="login-subtitle">Sign in to your account</p>
  </div>
  <form class="login-form" autocomplete="on" novalidate>
    <div class="form-group">
      <label for="login-email">Email</label>
      <input type="email" id="login-email" name="email"
             autocomplete="username" autocapitalize="none" spellcheck="false" required aria-required="true">
    </div>
    <div class="form-group">
      <label for="login-password">Password</label>
      <div class="password-field">
        <input type="password" id="login-password" name="password"
               autocomplete="current-password" autocapitalize="none" spellcheck="false" required aria-required="true">
        <button type="button" class="password-toggle" aria-label="Show password" aria-pressed="false">
          <div class="svg-icn" data-icon="eye-off"><!-- inline eye-off SVG --></div>
          <div class="svg-icn" data-icon="eye"><!-- inline eye SVG --></div>
        </button>
      </div>
    </div>
    <div class="login-meta">
      <div class="form-check">
        <input type="checkbox" id="login-remember" name="remember">
        <label for="login-remember">Remember me</label>
      </div>
      <button type="button" class="button" data-variant="text">Forgot password?</button>
    </div>
    <p class="login-status" id="login-status" data-type="danger" role="alert" aria-live="assertive"></p>
    <button type="submit" class="button" data-full-width>Sign in</button>
  </form>
</div>

On the page the demo heading is an h2; in an application the login title is usually the page's h1. The subtitle collapses when empty, so flow JS can fill it only when there is something to say — an invite instruction, a recovery notice.


Create account

Sign-up swaps the field set: name, email, a new password with a hint. The footer points existing users back to sign in.

Create your account

At least 8 characters.

<div class="form-group">
  <label for="signup-password">Password</label>
  <div class="password-field">
    <input type="password" id="signup-password" name="password"
           autocomplete="new-password" autocapitalize="none" spellcheck="false" required aria-required="true"
           minlength="8" aria-describedby="signup-hint">
    <button type="button" class="password-toggle" aria-label="Show password" aria-pressed="false">
      <div class="svg-icn" data-icon="eye-off"><!-- inline eye-off SVG --></div>
      <div class="svg-icn" data-icon="eye"><!-- inline eye SVG --></div>
    </button>
  </div>
  <p class="form-hint" id="signup-hint">At least 8 characters.</p>
</div>

Every password field gets the reveal toggle — creating a password is where seeing what was typed matters most.


Forgot and reset password

Two panes. The request pane asks for an email and sends a link; the set pane takes the new password twice. On one page they sit as sibling panes and consumer JS switches them with .

Reset your password

We will email you a reset link

The set pane pairs two new-password fields, and carries a hidden autocomplete="username" input that consumer JS fills with the account email from the reset token — that is what lets password managers save the new credential against the right account. With the reveal toggle present the confirm field is optional — a user who can see what they typed doesn't need to type it twice — so a product may drop it and keep only the first field. The example below also shows the error state: and aria-invalid="true" on the input together, with a message wired in through aria-describedby.

Set a new password

Passwords do not match.

<div class="form-group">
  <label for="setpw-confirm">Confirm password</label>
  <div class="password-field">
    <input type="password" id="setpw-confirm" name="confirm-password"
           autocomplete="new-password" autocapitalize="none" spellcheck="false" required aria-required="true" minlength="8"
           class="is-error" aria-invalid="true" aria-describedby="setpw-confirm-error">
    <button type="button" class="password-toggle" aria-label="Show password" aria-pressed="false">
      <div class="svg-icn" data-icon="eye-off"><!-- inline eye-off SVG --></div>
      <div class="svg-icn" data-icon="eye"><!-- inline eye SVG --></div>
    </button>
  </div>
  <p class="form-error" id="setpw-confirm-error">Passwords do not match.</p>
</div>

Magic link

Passwordless entry: one email field, one action. Pair it with the one-time code pane below when the flow confirms with a code rather than a link.

Sign in without a password

We will email you a sign-in link

<form class="login-form" autocomplete="on" novalidate>
  <div class="form-group">
    <label for="magic-email">Email</label>
    <input type="email" id="magic-email" name="email"
           autocomplete="username" autocapitalize="none" spellcheck="false" required aria-required="true">
  </div>
  <p class="login-status" id="magic-status" data-type="danger" role="alert" aria-live="assertive"></p>
  <button type="submit" class="button" data-full-width>Email me a link</button>
</form>

One-time code

The code field is a plain text input tuned for the moment: inputmode="numeric" raises the number pad on touch keyboards, autocomplete="one-time-code" lets the browser offer the code straight from the message that delivered it, and spellcheck="false" stops the squiggle under a string that was never a word.

Enter your code

Sent to you by email

<div class="form-group">
  <label for="otp-code">One-time code</label>
  <input type="text" id="otp-code" name="code"
         inputmode="numeric" autocomplete="one-time-code" spellcheck="false"
         pattern="[0-9]*" maxlength="6" required aria-required="true">
</div>

Social sign-in

Provider buttons are plain buttons: data-variant="outline" with data-full-width, a brand mark in the standard wrapper, stacked below a labelled Divider. No role class, no token overrides.

Welcome back

or
<div class="divider divider--labelled">or</div>
<button type="button" class="button" data-variant="outline" data-full-width>
  <div class="svg-icn" data-icon="google"><!-- inline Google mark --></div>
  Continue with Google
</button>
<button type="button" class="button" data-variant="outline" data-full-width>
  <div class="svg-icn" data-icon="apple"><!-- inline Apple mark --></div>
  Continue with Apple
</button>

The Google and Apple marks are site-local icons — they live at assets/icons/local/, deliberately outside the shared icon registry, because third-party brand marks are a per-product licensing decision, not a system asset. Consumers copy them from this page's code or author their own. Both ship as monochrome currentColor paths so they follow the theme; a product bound by Google's full sign-in branding rules may substitute the official multicolour G inline — fixed fills work, they will not theme.


JavaScript

The component ships no script of its own. Wiring a login page in a new product takes the stylesheet install (see the appendix below), one shipped module, and three small pieces of your own JavaScript — in the order they come up.

1. Include the password reveal module once per page. The toggle markup carries both state icons; the contract is in Password Toggle.

<script src="/assets/js/password-toggle.js" defer></script>

2. Intercept every submit. Each submitting pane is a real <form>, and the handler prevents the default — a form that falls through to a native submit navigates with the typed values, credentials included, in the URL. Keep the button enabled and mark it while the request runs; write the outcome into the pane's . The status element is already in the DOM and stays in the accessibility tree while empty, so filling it with text is all a reliable announcement needs.

const form = document.querySelector('#sign-in-form');
const status = form.querySelector('.login-status');
const submit = form.querySelector('[type="submit"]');

async function handleSignIn(event) {
  event.preventDefault();
  status.textContent = '';
  submit.classList.add('is-loading');
  try {
    await signIn(new FormData(form)); // your auth call
  } catch (error) {
    status.textContent = 'That email and password did not match. Try again.';
  } finally {
    submit.classList.remove('is-loading');
  }
}

form.addEventListener('submit', handleSignIn);

3. Switch panes with and move focus. Render each flow as a sibling , keep one visible at a time, and land focus where the flow continues.

function showPane(next) {
  document.querySelectorAll('.login-form').forEach(function (pane) {
    pane.classList.toggle('is-hidden', pane !== next);
  });
  const field = next.querySelector('input:not([hidden])');
  if (field) field.focus();
}

4. Fill what the flow knows. On the set-new-password pane, write the account email from the reset token into the hidden autocomplete="username" input — that is what lets password managers pair the new credential with the account. Write flow instructions into ; it collapses when empty, so it costs nothing on panes with nothing to say.

The social sign-in buttons and their brand marks are in Social sign-in — the marks are site-local by design and deliberately not shipped with the system, so copy them from that section's code into your own assets.


Accessibility

  • Every input carries the autocomplete value for its moment — this is what makes password managers and OS keychains work:
Field autocomplete
Email as sign-in identifier (sign in, sign up, reset, magic link) username
Email as a contact detail only email
Full name name
Current password (sign in) current-password
New password (sign up, reset) new-password
One-time code one-time-code

On sign-up the email is the future identifier, so it takes username — that is what lets password managers pair it with the adjacent new-password field and save the credential.

  • Set and aria-invalid="true" together, always — the class paints the border and the danger focus ring, the attribute tells assistive technology. One without the other is a half-reported error.
  • Per-field messages use with an id, wired to the input through aria-describedby.
  • The form-level is a live region: role="alert" with aria-live="assertive" for errors, or role="status" with aria-live="polite" for confirmations. A pane that needs both keeps two elements. Keep the element in the DOM and rendered from first render and fill it with text — a region inserted or revealed from display: none at announce time is not reliably announced, which is why the empty state collapses by height rather than by display. Don't also point fields at it with aria-describedby — the live region announces on its own, and a form-level message is not a field description.
  • The one-time code field pairs inputmode="numeric" with a visible label; never replace the label with a placeholder.
  • Labels are always visible <label for> elements. The password toggle keeps its aria-label/aria-pressed contract from Password Toggle.
  • Identifier fields (email as username, one-time code) carry autocapitalize="none" and spellcheck="false" — mobile keyboards otherwise capitalise the first letter and spellcheck marks the address, both of which cause failed sign-ins that look like wrong passwords.
  • Password fields carry the same pair. The reveal toggle flips them to plain text inputs, and at that moment autocapitalize can mangle further typing while browser spellcheck can send the revealed value to a spellcheck service.
  • A pane that sets a password without showing the email — the set-new-password flow — includes a hidden autocomplete="username" input, filled by consumer JS, so password managers know which account the new credential belongs to.
  • When consumer JS switches panes, move focus to the revealed pane's first field or heading — keyboard and screen-reader users otherwise stay on a control that just disappeared.

Usage rules

Do:

  • Compose with when the page wants the form on a panel
  • Let the page layout own the centering — only caps its own width
  • Keep one pane visible at a time; switch siblings with
  • Put trailing text actions ("Back to sign in", "Resend code") in a as with data-variant="text"
  • Give every password field the reveal toggle, including sign-up — creating a password is where seeing it matters most
  • While a submit is in flight, keep the button enabled and mark it — a disabled button drops keyboard focus and goes silent for screen readers
  • Answer a reset request the same way whether or not the account exists — "If an account exists for that address, we've sent a link" — so the form never confirms which emails are registered
  • Write error text that states the fix, not just the failure — "Passwords need at least 8 characters", matching the field's minlength, beats "Invalid password"

Don't:

  • Don't nest a login inside a login, or stack two visible panes
  • Don't add margins inside the stack — the gaps own the spacing
  • Don't invent provider button styles — a social button is with data-variant="outline", data-full-width and a mark
  • Don't put brand marks in the shared icon registry — they are site-local by design
  • Don't block paste in password fields — pasting from a password manager is the desired path, not an abuse case
  • Don't cap password maxlength short or restrict characters — length is the strength that costs users nothing; keep minlength in step with the backend policy so the browser and the server reject the same inputs

CSS reference

This section documents how the component is built. For usage, see the sections above.

Tokens

Token Default Purpose
380px Width cap of the root column — the one-column form measure
88px Height of the header logo slot

Selectors

Selector Purpose
Root column: full width up to the cap, flex column, gap, centered items
Centered logo/title/subtitle stack with gap
Logo slot at token height; inner svg scales to it and inherits currentColor
Heading at , weight from the heading element, document-flow margins removed
Supporting line at in ; :empty collapses it
Full-width pane, flex column with gap
.login-form .form-group Scoped tightening: label-to-input gap, margin-bottom zeroed — the pane gap owns inter-group spacing
.login-form label
.login-form .divider Re-points to — the pane gap already separates it
Wrapping space-between row for remember-me and the forgot link
Form-level live region at , centered; :empty collapses it to zero height while it stays in the accessibility tree; data-type="danger"/"success" pick the status colour
Centered column for trailing text actions

Key rules

  • Spacing is gap-only throughout — no margins inside the stack, per the layout rules
  • tightens by scope rather than introducing a new field class, so the form primitives stay untouched everywhere else
  • data-type on uses the shared feedback axis values (danger, success) resolving to --status-* tokens
  • The component carries no dark-mode CSS — every colour is a semantic token

Use in another product

The design system installs once per product:

npm install github:bydefaultstudio/design-system-dist#semver:^4.7.0

This component's styles ship in design-system.css. Its behaviour ships as dist/js/password-toggle.js — copy it into the product's served assets and include it once per page:

<script src="assets/js/password-toggle.js" defer></script>
On this page
  • Anatomy
  • Basic usage
  • Create account
  • Forgot and reset password
  • Magic link
  • One-time code
  • Social sign-in
  • JavaScript
  • Accessibility
  • Usage rules
  • CSS reference
  • Use in another product
Previous Section Header
Next Cell Input

Was this page helpful?

We use this feedback to improve our documentation.

Thanks for your feedback

Send feedback

© 2026 By Default