Navigation

v0.0.4
Application blocks

Sign in

Sign in and sign up cards with providers, fields and a way back.

Signing in is the one screen every product has. This block is the card: a title, the fast ways in, a divider, the fields, and the way to the other card. It is a sheet that is also a form, so its fields keep one distance and it stacks by itself.

Sign in

<form class="sheet form w-full max-w(380) p-xl">  <header class="flex flex-col items-center gap-xs text-center">    <span class="fg-text-strong">      <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">        <path d="M12 3a9 9 0 1 0 0 18a9 9 0 0 0 0-18M12 3a9 9 0 0 0 0 18" />      </svg>    </span>    <h1 class="heading heading-lg">Sign in to Atlas</h1>    <p class="m(0) fg-text-muted">Welcome back. Pick up where you left off.</p>  </header>  <div class="flex flex-col gap-xs">    <button type="button" class="btn">      <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">        <path d="M6 6a2 2 0 1 0 0 .01M18 12a2 2 0 1 0 0 .01M6 18a2 2 0 1 0 0 .01M6 8v8M8 6h4a4 4 0 0 1 4 4v0" />      </svg>Continue with GitHub</button>    <button type="button" class="btn">      <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">        <path d="M14 10a4 4 0 1 0-3.5 4L8 16.5V19h2.5v-2H13l1.5-1.5A4 4 0 0 0 14 10M16.5 7.5h.01" />      </svg>Continue with a passkey</button>  </div>  <div class="divider divider-label" role="separator">or</div>  <label class="field">Email    <input class="input" type="email" name="email" autocomplete="username" placeholder="you@example.com" required>  </label>  <div class="field">    <div class="flex items-baseline justify-between gap-sm">      <label for="signin-password">Password</label>      <a class="link link-quiet text-xs fg-text-muted" href="#">Forgot it?</a>    </div>    <input class="input" id="signin-password" type="password" name="password" autocomplete="current-password" required>  </div>  <label class="check">    <input type="checkbox" class="checkbox" name="remember" checked>Keep me signed in on this device</label>  <button class="btn btn-primary">Sign in</button>  <p class="m(0) text-center fg-text-muted">New here?    <a class="link" href="#">Create an account</a>  </p></form>

Blocks are markup to copy. They take the page's own text color and size; the previews set fg-text-body text-sm on the body. Each block is a size container, so it reflows by its own width: in a wide page, a sidebar's worth of room or a dialog. Center the card on its page with the centered layout.

#What is in it

  • sheet form w-full max-w(380) p-xl. The card fills a phone and stops at 380px. form sets the gap between everything in it.
  • The fast ways first. A provider and a passkey are one click each; the password is the fallback. A divider with "or" parts the two.
  • "Forgot it?" beside the label, not under the field. It is a link-quiet in muted ink at the end of the label's row, so it is found without competing with the button. The label points at its field with for, since the field is no longer inside it.
  • One primary button, the last control, as wide as the fields.
  • The way to the other card under it: one line, one link.

The fields carry autocomplete values, username and current-password, which is what lets a password manager fill them.

#Sign up

The same card with more fields. The names share a row, grid grid-cols(2) gap-sm: two short fields that are one answer. A form-row would not split here, since it waits for 28rem of form and this card has 22. The password's rule is a field-hint tied to the input with aria-describedby, shown before the reader makes the mistake.

Sign up

<form class="sheet form w-full max-w(420) p-xl">  <header class="flex flex-col gap-xs">    <h1 class="heading heading-lg">Create your account</h1>    <p class="m(0) fg-text-muted">Free for personal projects. No card needed.</p>  </header>  <div class="grid grid-cols(2) gap-sm">    <label class="field">First name      <input class="input" name="given-name" autocomplete="given-name" required>    </label>    <label class="field">Last name      <input class="input" name="family-name" autocomplete="family-name" required>    </label>  </div>  <label class="field">Work email    <input class="input" type="email" name="email" autocomplete="email" placeholder="you@company.com" required>  </label>  <label class="field">Password    <input class="input" type="password" name="password" autocomplete="new-password" minlength="12" required aria-describedby="signup-hint">    <span class="field-hint" id="signup-hint">At least 12 characters. A sentence works well.</span>  </label>  <label class="check">    <input type="checkbox" class="checkbox" name="terms" required>    <span>I agree to the      <a class="link" href="#">terms</a>and the      <a class="link" href="#">privacy policy</a>    </span>  </label>  <button class="btn btn-primary">Create account</button>  <p class="m(0) text-center fg-text-muted">Already have one?    <a class="link" href="#">Sign in</a>  </p></form>

Ask only for what the first screen needs. Every field here loses some of the people who started.

#Verify

After a code is sent, one job: type it. An OTP field in the middle, the address the code went to in ink, and a quiet way to send another.

Verify

<form class="sheet form w-full max-w(380) p-xl">  <header class="flex flex-col items-center gap-xs text-center">    <span class="well flex items-center justify-center w(44) h(44) fg-text-strong">      <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">        <path d="M4 6h16a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zM3 7l9 6 9-6" />      </svg>    </span>    <h1 class="heading heading-lg mt-xs">Check your email</h1>    <p class="m(0) fg-text-muted">We sent a six-digit code to      <span class="fg-text-strong">ada@example.com</span>. It works for ten minutes.</p>  </header>  <fieldset class="otp mx-auto" aria-label="Verification code">    <input class="otp-slot" inputmode="numeric" pattern="[0-9]" maxlength="1" autocomplete="one-time-code" aria-label="Digit 1" value="4">    <input class="otp-slot" inputmode="numeric" pattern="[0-9]" maxlength="1" aria-label="Digit 2" value="2">    <input class="otp-slot" inputmode="numeric" pattern="[0-9]" maxlength="1" aria-label="Digit 3">    <span class="otp-separator" aria-hidden="true"></span>    <input class="otp-slot" inputmode="numeric" pattern="[0-9]" maxlength="1" aria-label="Digit 4">    <input class="otp-slot" inputmode="numeric" pattern="[0-9]" maxlength="1" aria-label="Digit 5">    <input class="otp-slot" inputmode="numeric" pattern="[0-9]" maxlength="1" aria-label="Digit 6">  </fieldset>  <button class="btn btn-primary">Verify</button>  <p class="m(0) text-center fg-text-muted">Nothing arrived?    <button type="button" class="link">Send it again</button>  </p></form><script type="module">  import { attach } from '@master/ui/behaviors'  attach()</script>

The otp behavior moves focus from one slot to the next and spreads a pasted code across them; without it each slot is still a field that takes one digit. "Send it again" is a <button> drawn as a link: it does something here, it does not go anywhere.

#Accessibility

  • The card's title is the page's <h1>.
  • Every field has a visible label. A placeholder is an example, not a name.
  • Errors appear with the field they belong to: add a field-error and it shows while the field is invalid; see input.
  • Do not disable the button until the form is valid. A disabled button cannot say what is missing; a submitted form can.
  • Keep "show password" in mind for long passwords: a quiet button inside an input group that switches the field's type.