busy-office-ui
Menu

Pattern: login

Who uses it: every user, before anything else — the first screen and, for anyone who forgets a password, a recurring one. What "done" looks like: signed in, or told exactly why not without leaking which part was wrong. The one screen every ERP app has and the one where getting the details wrong locks real users out. Composed entirely from existing components, zero new CSS: .bo-widget (the card primitive) centers the form on the canvas, fields are plain .bo-form-fields with the correct autocomplete tokens (username / current-password — this is what lets password managers fill the form), and errors use the same validation-summary pattern as any other form. Try it: submit the empty form. Not for re-authentication mid-task — when a workspace needs a step-up credential check without abandoning the screen the user was on (confirming a destructive action, re-entering a password after a session timeout), a full-page navigation throws away that context; use a dialog carrying the same two fields instead. This page is for the one navigation every session starts with, not an interruption inside one.

Sign in to busy-office

The details that matter on a login screen

  • autocomplete="username" / "current-password" — the standardized tokens password managers key on. Omitting them (or using off) breaks credential autofill, which hurts the users who rely on managers for strong unique passwords.
  • Server-error recovery uses the same summary box — on a failed sign-in your server re-renders the form with the data-validation-summary-box alert visible and a message like "The email or password you entered is incorrect." Say which action failed, never which field was wrong (that's credential enumeration).
  • No premature lockout messaging — if you rate-limit or lock accounts, tell the user how to recover ("try again in 15 minutes" / "reset your password"), in the same alert, as text — not a dead end.
  • The password field never blocks paste — paste-blocking breaks password managers; nothing here does it and nothing should.
  • One column, one card — .bo-widget with max-inline-size centered on the canvas; no new "login layout" CSS exists because none is needed.

Markup

<!-- inline-size: 100% matters: .bo-widget is a size container
     (container-type: inline-size), so outside a .bo-widget-grid it needs an
     explicit width — max-inline-size alone collapses it. -->
<div class="bo-widget" style="inline-size: 100%; max-inline-size: 24rem; margin-inline: auto">
  <div class="bo-widget__header"><span class="bo-widget__title">Sign in</span></div>
  <div class="bo-widget__body">
    <form method="post" action="/session" data-validation-summary class="bo-stack bo-stack--tight">
      <div class="bo-alert bo-alert--danger" data-validation-summary-box hidden role="alert">
        <p>There is a problem</p>
        <ul></ul>
      </div>
      <div class="bo-form-field">
        <label class="bo-form-field__label" for="user">Email or username</label>
        <input class="bo-input" id="user" name="username" autocomplete="username" required />
      </div>
      <div class="bo-form-field">
        <label class="bo-form-field__label" for="pass">Password</label>
        <input class="bo-input" id="pass" name="password" type="password"
            autocomplete="current-password" required />
      </div>
      <div class="bo-form-actions">
        <button class="bo-btn" type="submit">Sign in</button>
      </div>
    </form>
  </div>
</div>

<script type="module">
  import { initValidationSummary } from "@busy-office/ui/js";
  initValidationSummary(); // client-side empty-field summary; server errors
                            // re-render the same alert box with their message
</script>

Anatomy

  1. Brand block — your mark; the framework ships no logo component, it is your <img>.
  2. Form — a real <form> posting to your endpoint; native validation first, server validation authoritative.
  3. Error region — .bo-alert--danger above the fields, rendered by the SERVER on failure (not JS), so it survives a hard reload.
  4. Secondary links — reset password, SSO — a cluster below the submit.

Data contract

RequestReturns / on failure
POST /loginredirect to the app on success
POST /login (bad credentials)401 → the same form re-rendered with the alert and the USERNAME preserved; never the password
GET /login?next=…the form, carrying the post-login destination in a hidden field

States

StateWhat ships
Idlesubmit enabled; autofocus the first empty field only
Invalid credentialsone generic message — never "no such user" (account enumeration)
Account lockeda distinct message with the unlock path; do NOT reuse the invalid-credentials copy
Session expiredarrive with an info alert explaining why, and the original destination preserved
Submittingbutton disabled + aria-busy; the form is the only thing on screen, so no skeleton
No JSthe live demo form carries novalidate, which turns off the browser's own constraint validation — with no initValidationSummary() to call checkValidity() either, Submit fires with empty required fields unvalidated and the summary box never appears; the canonical form in the Markup section below omits novalidate, so a real deployment of it would fall back to native browser validation bubbles instead of losing validation entirely

Components used

complexity 1 of 4

↑↓ move Enter open Esc close