GODRICH
#css-animation#button#loader#transition#scroll#dashboard#ui#design-trends

9 Login UI Design Patterns (Copy-Paste)

Nine login and signup patterns that compute: fill ratio refills a progress ring, remaining time recounts into an arc every second, and scroll progress alone

Auto-plays · click a tile to jump to its section · all nine in one zip

Only item 09's consent checkbox carries a listener that responds to real user input — a genuine scroll event on the terms panel. The other eight run purely on requestAnimationFrame cycling on a fixed 2000ms period, replaying the same scenario from arrays and formulas with no click or keystroke behind it. There's no real email delivery, no real auth server, no real social-login brand, and no real key or token anywhere — only item 05's six-digit code comes from a seeded pseudo-random function (mulberry32), so every capture reproduces the same digits.

Anatomy of a login/signup UI — what nine screens compute

None of these nine screens hard-codes a percentage, a color, or a layout and leaves it sitting there. The same handful of arithmetic moves — a ratio, a score, a recount — keeps resurfacing under a different name, and once that's visible, all nine read as one idea wearing nine costumes.

Why a login card's ring fills from an averaged ratio instead of one flag

Counting how many characters have landed in the email field and the password field separately, then averaging the two fractions, is what item 01 feeds into the card border's conic-gradient — so a half-typed email and an empty password hold the ring at roughly a quarter full, not zero and not half.

Why a signup-step bar keeps growing between whole steps instead of snapping

Item 02 divides the current step number by STEP_TOTAL (4) for the step boundary, then adds the elapsed fraction inside that step (0-1) on every frame before it touches the bar's length — a bar that only jumped on whole-step boundaries would hide the computed layer entirely.

Why a social-login row recomputes its column count instead of keeping one fixed grid

COUNTS cycles [2, 3, 4, 5, 3] while colsFor(n) decides on every step whether that count fits one row or needs to wrap, so item 03's button spacing stays even whether a scenario shows two providers or five.

Code per screen

Every block below is copied straight out of that item's own index.html, not retyped from memory, and the matching prompt fragment in the zip regenerates the same file when handed to an AI coding tool.

01Login card — a card that computes fill ratio to fill its own progress indicator

Counting how many of the email and password characters are filled works out a completion ratio, and that ratio fills the card border's progress indicator to match — recomputed every 70ms tick (TYPE_MS) as the fields type themselves out.

Login card — a card that computes fill ratio to fill its own progress indicator
Open in a new tab

01 · Login card — a card that computes fill ratio to fill its own progress indicator — Measured area 0.824% · moving frames 19/23Original by GODRICH · SCSS and React ship in the zip · See the password

conic-gradientsetPropertyautocomplete

What the numbers say

Cumulative area is the lowest of the nine at 0.824% (max frame 0.065%, mean intensity 155.2) across 19 of 23 frames — an input field is a structurally small-change surface, which is exactly right for a completion ring.

  var TYPE_MS = 70;
  var CYCLE_MS = 2000;

  function frameFor(t) {
    var emailChars = Math.min(Math.max(Math.floor(t / TYPE_MS), 0), EMAIL_LEN);
    var pwStart = EMAIL_LEN * TYPE_MS;
    var pwChars = t > pwStart
      ? Math.min(Math.max(Math.floor((t - pwStart) / TYPE_MS), 0), SAMPLE_PW_LEN)
      : 0;
    return { emailChars: emailChars, pwChars: pwChars };
  }

  function apply(t) {
    var f = frameFor(t);
    emailEl.value = SAMPLE_EMAIL.slice(0, f.emailChars);
    pwEl.value = PW_DOTS.slice(0, f.pwChars);
    var ratio = ((f.emailChars / EMAIL_LEN) + (f.pwChars / SAMPLE_PW_LEN)) / 2;
    ring.style.setProperty('--ratio', ratio.toFixed(4));
  }

02Signup steps — a stepped form that divides its own progress ratio to set bar length and color

Dividing the current step number by the total step count works out a progress ratio, and that ratio sets both the length and the color depth of the progress bar together — STEP_TOTAL is 4 and each step holds for STEP_MS 500ms.

Signup steps — a stepped form that divides its own progress ratio to set bar length and color
Open in a new tab

02 · Signup steps — a stepped form that divides its own progress ratio to set bar length and color — Measured area 2.294% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

setPropertyaria-valuenowclassList

What the numbers say

Cumulative area comes to 2.294% (max frame 0.426%, mean intensity 177.4, the brightest of the nine) over all 23 of 23 frames — a thick bar filling in bold color reads as the most visually intense item in the set even at a modest footprint.

  var STEP_TOTAL = 4;
  var STEP_MS = 500;
  var CYCLE_MS = STEP_TOTAL * STEP_MS;

  function apply(t) {
    var tt = t % CYCLE_MS;
    var idx = Math.min(Math.max(Math.floor(tt / STEP_MS), 0), STEP_TOTAL - 1);
    var current = idx + 1;
    // The step boundary snaps to a whole number (1-4) for aria/label, but the bar itself
    // adds the elapsed fraction inside the step (0-1) so it grows a little every frame — one computed layer (interpolation) reads like a real signup progress bar.
    var within = (tt % STEP_MS) / STEP_MS;
    var ratio = (idx + within) / STEP_TOTAL;
    track.style.setProperty('--ratio', ratio.toFixed(4));
    track.setAttribute('aria-valuenow', String(current));
    steps.forEach(function (el, i) {
      if (i === idx) {
        el.classList.add('is-active');
      } else {
        el.classList.remove('is-active');
      }
    });
  }

03Social login — a button group that recomputes column count from how many providers there are

Counting the provider buttons works out how many columns fit one row, and spacing and size get recalculated so the layout stays even whether providers are added or removed — the button labels are generic placeholders ("Provider A" and the like), with no real brand anywhere.

Social login — a button group that recomputes column count from how many providers there are
Open in a new tab

03 · Social login — a button group that recomputes column count from how many providers there are — Measured area 23.165% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

grid-template-columnsclip-pathclassList

What the numbers say

Cumulative area is the widest of the nine at 23.165% (max frame 20.234%, mean intensity 161.0) across all 23 of 23 frames — a grid that reshuffles between two and five buttons every step is naturally the largest-footprint item here.

  function colsFor(n) {
    return n <= 3 ? n : Math.ceil(n / 2);
  }

  function apply(t) {
    var tt = t % CYCLE_MS;
    var idx = Math.min(Math.max(Math.floor(tt / STEP_MS), 0), COUNTS.length - 1);
    var n = COUNTS[idx];
    var cols = colsFor(n);
    row.style.setProperty('--cols', String(cols));
    hintCount.textContent = String(n);
    btns.forEach(function (btn, i) {
      if (i < n) {
        btn.classList.remove('is-hidden');
      } else {
        btn.classList.add('is-hidden');
      }
    });
    // The stretch remaining until the next count change gets recomputed every frame into a
    // tick bar (one computed layer) — so the grid rearrange never snaps; it signals the next change coming.
    var within = (tt % STEP_MS) / STEP_MS;
    tickBar.style.setProperty('--within', within.toFixed(4));
  }

04Magic link — a sent-confirmation card that recomputes remaining time into a ring's arc length

The remaining valid time in seconds gets recomputed every second, and comparing it against the total valid time redraws a donut ring's arc length to match that ratio — VALID_SEC ships at 300 (five minutes), and the ring's circumference is a fixed 276.46.

Magic link — a sent-confirmation card that recomputes remaining time into a ring's arc length
Open in a new tab

04 · Magic link — a sent-confirmation card that recomputes remaining time into a ring's arc length — Measured area 2.65% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

stroke-dasharraystrokeDashoffsetrequestAnimationFrame

What the numbers say

Cumulative area is 2.65% (max frame 2.535%, mean intensity 153.0) over all 23 of 23 frames — the ring's stroke offset shifts by a small amount on every tick, which is why it ties several other items for full-frame movement despite a modest footprint.

  var VALID_SEC = 300; // variant: 300 → 900
  var CIRCUMFERENCE = 276.46;
  var CYCLE_MS = 2000;

  function ratio(t) {
    return 1 - Math.min(Math.max(t / CYCLE_MS, 0), 1);
  }

  function apply(t) {
    var r = ratio(t);
    var remainingDisplaySec = Math.ceil(r * VALID_SEC);
    ring.style.strokeDashoffset = String(CIRCUMFERENCE * (1 - r));
    timeText.textContent = 'Time left' + ' ' + remainingDisplaySec + 's';
  }

05Two-factor — a code entry that sets its confirm button's state from a fill ratio

Counting how many of the six boxes are filled works out a fill ratio, and only once that ratio reaches full does the confirm button's opacity and clickability change together — the six digits themselves come from mulberry32(42), a seeded generator, never Math.random().

Two-factor — a code entry that sets its confirm button's state from a fill ratio
Open in a new tab

05 · Two-factor — a code entry that sets its confirm button's state from a fill ratio — Measured area 9.225% · moving frames 22/23Original by GODRICH · SCSS and React ship in the zip · See the password

data-indexautocompletesetAttribute

What the numbers say

Cumulative area is 9.225% (max frame 9.171%, mean intensity 49.4) over 22 of 23 frames as each of the six boxes fills roughly every 250ms (DIGIT_MS).

  var DIGITS = 6;
  var DIGIT_MS = 250; // 6 * 250 = 1500; the remaining 500ms holds the completed state
  var CYCLE_MS = 2000;

  function mulberry32(seed) {
    return function () {
      seed |= 0; seed = (seed + 0x6D2B79F5) | 0;
      var t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
      t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
      return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
    };
  }

  var rand = mulberry32(42);
  var code = [];
  for (var i = 0; i < DIGITS; i++) code.push(Math.floor(rand() * 10));

  function filled(t) {
    return Math.min(Math.max(Math.floor(t / DIGIT_MS), 0), DIGITS);
  }

06Password reset request — a form that scores email validity to set its button state

Checking the entered email's structure and whether it holds an at-sign and domain works out a validity score, and that score decides both whether the request button enables and what color the hint text turns — the threshold sits at .9.

Password reset request — a form that scores email validity to set its button state
Open in a new tab

06 · Password reset request — a form that scores email validity to set its button state — Measured area 7.964% · moving frames 20/23Original by GODRICH · SCSS and React ship in the zip · See the password

aria-invalidaria-describedbyclassList

What the numbers say

Cumulative area is 7.964% (max frame 7.761%, mean intensity 159.4) over 20 of 23 frames as a placeholder address types itself out character by character.

  var SAMPLE = '[email protected]';
  var TYPE_MS = 90;
  var SCORE_THRESHOLD = .9;
  var CYCLE_MS = 2000;

  function currentText(t) {
    var n = Math.min(Math.max(Math.floor(t / TYPE_MS), 0), SAMPLE.length);
    return SAMPLE.slice(0, n);
  }

  function score(text) {
    var domain = text.split('@')[1] || '';
    var s = 0;
    if (text.indexOf('@') !== -1) s += .4;
    if (/\.[a-z]{2,}$/i.test(domain)) s += .4;
    s += Math.min(text.length / 12, 1) * .2;
    return s;
  }

07Session expired — a notice that recomputes badge intensity from elapsed time since expiry

The time elapsed since the session dropped keeps getting recomputed, and the longer that stretch grows, the deeper the notice badge's color and the re-login button's emphasis both turn — the ramp runs over RAMP_MS 1800ms with a linear curve (INTENSITY_POW 1).

Session expired — a notice that recomputes badge intensity from elapsed time since expiry
Open in a new tab

07 · Session expired — a notice that recomputes badge intensity from elapsed time since expiry — Measured area 4.845% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

getComputedStylegetPropertyValueclassList

What the numbers say

Cumulative area is 4.845% (max frame 0.958%, mean intensity 31.8, the calmest of the nine) over all 23 of 23 frames — the badge only deepens gradually within a pale range, so it moves on nearly every tick without ever reading as bright.

  var INTENSITY_POW = 1; // variant: 1 → 3
  var RAMP_MS = 1800;
  var CYCLE_MS = 2000;

  var badge = document.getElementById('badge');
  var elapsedSeconds = document.getElementById('elapsedSeconds');
  var btn = document.getElementById('reloginBtn');
  if (!badge || !elapsedSeconds || !btn) return;

  function clamp(v, min, max) { return Math.max(min, Math.min(max, v)); }
  function hexToRgb(hex) {
    hex = String(hex).trim().replace('#', '');
    return [
      parseInt(hex.substring(0, 2), 16),
      parseInt(hex.substring(2, 4), 16),
      parseInt(hex.substring(4, 6), 16)
    ];
  }
  function elapsedRatio(t) { return clamp(t / RAMP_MS, 0, 1); }

08Captcha slot — a widget that computes selection match ratio to set border color and button state

Comparing the number of selected tiles against the answer set works out a match ratio, and only once that ratio clears a threshold does the confirm button enable, with the border color shifting to match the ratio too — the fixed pick order deliberately includes one wrong tile.

Captcha slot — a widget that computes selection match ratio to set border color and button state
Open in a new tab

08 · Captcha slot — a widget that computes selection match ratio to set border color and button state — Measured area 4.765% · moving frames 7/23Original by GODRICH · SCSS and React ship in the zip · See the password

data-indexgrid-template-columnsclassList

What the numbers say

Cumulative area is 4.765% (max frame 1.083%, mean intensity 71.6) over just 7 of 23 frames, the lowest of the nine — the idle gap between the last pick and an unrelated pause is structurally normal for a tile-selection widget, not a defect.

  var ANSWER = [1, 3, 5, 7];
  var PICK_ORDER = [1, 4, 3, 7, 5]; // deliberately includes the wrong pick 4
  var PICK_MS = 350; // 5 * 350 = 1750
  var MATCH_THRESHOLD = 1; // must reach 100% match to enable
  var CYCLE_MS = 2000;

  function clamp(v, min, max) { return Math.max(min, Math.min(max, v)); }

  var tiles = Array.prototype.slice.call(document.querySelectorAll('.tile'));
  var matchPercent = document.getElementById('matchPercent');
  var submitBtn = document.getElementById('submitBtn');
  if (!tiles.length || !matchPercent || !submitBtn) return;

  function selectedSoFar(t) {
    var count = clamp(Math.floor(t / PICK_MS), 0, PICK_ORDER.length);
    return PICK_ORDER.slice(0, count);
  }

  function matchRatioOf(selected) {
    var hit = 0;
    for (var i = 0; i < ANSWER.length; i++) {
      if (selected.indexOf(ANSWER[i]) !== -1) hit += 1;
    }
    return clamp(hit / ANSWER.length, 0, 1);
  }

09Consent checkbox — a UI that gates activation by measured scroll progress through terms

Measuring how far the terms list has been scrolled works out the point that counts as having read it all, and only past that point does the consent checkbox switch into a clickable state — the threshold ships at .7, and it is the only item among the nine wired to a real scroll listener.

Consent checkbox — a UI that gates activation by measured scroll progress through terms
Open in a new tab

09 · Consent checkbox — a UI that gates activation by measured scroll progress through terms — Measured area 15.528% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

scrollTopaddEventListenersetAttribute

What the numbers say

Cumulative area is 15.528% (max frame 6.348%, mean intensity 144.1) over all 23 of 23 frames — a real scroll position moving the whole terms panel produces one of the largest footprints in the set.

  function onScroll() {
    var maxScroll = panel.scrollHeight - panel.clientHeight;
    var progress = maxScroll > 0 ? clamp(panel.scrollTop / maxScroll, 0, 1) : 1;
    if (progress >= SCROLL_THRESHOLD && !unlocked) {
      unlocked = true;
      checkbox.disabled = false;
      checkbox.setAttribute('aria-disabled', 'false');
      label.textContent = DONE_TEXT;
    } else if (progress < SCROLL_THRESHOLD && unlocked) {
      unlocked = false;
      checkbox.disabled = true;
      checkbox.checked = false;
      checkbox.setAttribute('aria-disabled', 'true');
      label.textContent = lockedText;
    }
  }
  panel.addEventListener('scroll', onScroll);

Where this actually gets used — a team rebuilding login and signup forms

Teams that go looking for these nine screens usually have one specific problem. They want a card like 01 that shows input completeness as a fill ratio, but they're stuck on what that ratio should actually be computed from.

Whoever owns the password-reset screen keeps hitting the same call as 06 — deciding whether to enable the button from email format alone, before anything is actually sent. The next question is how to show format validation and actual send success differently on the same screen.

For whoever builds the terms-consent screen, 09's scroll-progress math is basically the whole feature. How you decide the moment scrolling counts as "reached the end" changes how impatient the button feels to wait for.

Three variants, and how reduced motion changes them

Variant What changes What it trades off
Magic link valid time Item 04's VALID_SEC from 300 to 900 300 seconds (5 minutes) shrinks the ring quickly, favoring security through urgency; 900 seconds (15 minutes) buys room but the ring barely shrinks, dulling the sense that the link is time-limited
Session-expired emphasis curve Item 07's INTENSITY_POW from 1 to 3 An exponent of 1 deepens the badge color gradually and feels less urgent; raising it to 3 keeps the badge nearly unchanged early on, then deepens suddenly near the end for a stronger warning
Consent checkbox scroll threshold Item 09's SCROLL_THRESHOLD from .7 to 1 .7 unlocks the checkbox after skimming 70% of the terms, speeding up signup but weakening the guarantee everything was read; 1 requires scrolling to the very end, which is certain but can feel tedious for long terms

What reduced motion turns off, item by item

prefers-reduced-motion: reduce removes travel, not arithmetic, across all nine screens: item 01 jumps straight to the fully-typed state while the same averaging math still exists underneath, item 05 holds at its all-boxes-empty starting frame with the confirm button still disabled rather than animating through the six fills, and item 09 sets panel.scrollTop to scrollHeight once and still runs the exact same onScroll path a real scroll event would trigger. Validity scores, elapsed-time ramps, and match ratios keep computing either way — only the frame-by-frame travel toward that result gets skipped.

Where values reshape the screen — fill ratio, remaining time, read ratio

None of these nine screens has a fixed visual sitting empty until a real user fills it in — the same value that a real login flow would produce is exactly what reshapes the screen here too.

Why the login card's ring holds around a quarter full with an empty password field

Item 01's ratio averages email progress and password progress rather than adding them, so a fully typed email with a still-empty password reads as roughly 50% on the ring instead of jumping straight to full the moment one field finishes — a login form design template with a ring that just snaps to 100% would hide that averaging step entirely.

Why the magic link's remaining time never drifts even if a tab sits in the background

Item 04's ratio(t) recomputes the fraction from scratch on every frame instead of counting down a stored number, which is exactly why the ring and the seconds readout can't drift out of sync with each other even after the browser throttles a backgrounded tab.

Why the consent checkbox can re-lock itself after it was already open

Item 09's onScroll checks both directions on every event — once progress drops back below SCROLL_THRESHOLD after having cleared it, unlocked flips back to false, the checkbox disables again, and the checked state clears, exactly as it would for someone scrolling back up through a genuine set of terms.

What breaks first — an empty email, expiry about to hit, zero correct tiles

A computed layer only earns its keep once the edge cases feeding it are handled on purpose, not accidentally.

Why an email without a real domain suffix caps out at .6, never near the .9 threshold

Item 06's score only awards the at-sign's .4 and the length term's .2 without ever crossing .6 when the domain fails the /\.[a-z]{2,}$/i check — an address like a bare local name safely leaves the request button disabled instead of looking like it might slip through.

Why a session that's been dropped for an hour reads the same as one dropped 1.8 seconds ago

elapsedRatio clamps at RAMP_MS (1800ms), so item 07's badge and re-login button reach full intensity right at that mark and simply hold there — the badge can't tell an hour-old session from a fresh one because the ramp was never designed to run longer than 1.8 seconds of demo time.

Why the deliberately wrong pick never lowers the captcha's match ratio

matchRatioOf only counts how many of the four answer tiles eventually appear in the selection, so picking the wrong tile 4 partway through item 08's sequence costs nothing once every correct tile has also been picked — the logic that keeps that recount honest against a genuinely wrong selection sits behind ydncqy9n in the zip linked from this post.

AI prompt for these screens

Each item ships with a matching prompt fragment naming its constants and its one computed layer, so pasting it into an AI coding tool regenerates the same file this post's demo runs.

What to build: a login card that recounts, every frame, how many characters
are filled in the email and password fields to work out a completion ratio,
then feeds that ratio into the card border's conic-gradient progress
indicator. Use only a placeholder email such as [email protected] — no
real email address, real auth server, or real key anywhere.

Running that fragment through an AI coding tool reproduces item 01's progress ring — the same TYPE_MS, the same averaged ratio, none of it retyped by hand. The full set of nine prompts, matched one-to-one with the nine index.html files, ships inside the zip.

Accessibility across all nine

Every screen checks window.matchMedia('(prefers-reduced-motion: reduce)') once and skips only the part of its own code that would otherwise animate a value into view — the ratio, score, or elapsed time behind that value keeps computing regardless. Browsing login ui design ideas boards full of static screenshots can't show this part: what a screen keeps recomputing after the first paint, not what the first paint looks like. A login form design template that ships as one frozen image hides the same thing.

Questions to check before wiring up a login form

Are these login and signup UI patterns free to use in a commercial project?

Yes — none of the nine loads a third-party library, a UI framework, or an external CDN script, so any of them can go straight into client work with nothing to credit or license.

Why does the two-factor confirm button stay disabled with five of six digits filled?

Item 05's ratio only reaches 1 once filled(t) returns the full DIGITS count (6), and the confirm button's opacity and clickability change together only at that exact point — five filled boxes still leave ratio at 0.833, short of the full ratio the button is waiting for.

Is browsing a login form design codepen collection the same as what's in this zip?

Not quite — a codepen collection freezes at the moment it was captured, while the plain index.html files here keep recomputing their fill ratio, their score, or their elapsed time every time the page opens.

Sources: MDN — prefers-reduced-motion · MDN — requestAnimationFrame · MDN — ARIA attributes

The same recompute-don't-accumulate idea behind item 04's remaining-time ring shows up again in a set of nine login-and-signup card styles built around glassmorphism and split-screen layouts rather than the functional prototypes gathered here, and the fill-ratio math behind items 01 and 05 plays a related role in a set of nine search dropdowns that recompute highlighting and edit distance on every keystroke — one post recomputes what a login or signup screen unlocks, the other recomputes what a search box suggests.

Enter the archive password

The password is inside this article. You will find it as you read.