Better Password Field

Here’s the story: The majority of the users who use our organization’s Portal are not comfortable with computers. Our Portal is invite-only, so we generate randomized temporary passwords for users to complete a first-time login and set a new password + some other partner-specific information (A little wonky, probably, but it works for us).

Every year, we invite ~500 users, and all 500 users must pass through the field of immeasurable confusion known as the password field. The labels are confusing, and their placement leads to errors. The browser auto-fill toggle is also ignored by Google and Edge, resulting in erroneous inputs.

After around 45 emails and phone calls, I decided to go down a ChatGPT and GitHub rabbit hole and “fix” this field. The password strength plugin was also integrated.

Progressive Field Reveal:
The password inputs appear one at a time as the user types:

  • Current Password (6-character minimum)
  • New Password (shows after the first is complete)
  • Re-Enter Password (shows after all password strength checks pass)

Placeholder Text in Input Field:

Self-explanatory, put the field labels where they cannot be misinterpreted.

Autofill Prevention:
Uses hidden “honeypot” inputs and short-term readonly protection to stop Chrome/Edge autofill from inserting saved passwords into the Current Password field.

Password Strength Meter:
Displays a live, color-coded strength indicator with four segments (“Very Weak” → “Strong”) and checklist icons for each condition:

  • Lowercase & Uppercase
  • Number (0–9)
  • Special character [!@#$%^&*]
  • At least 8 characters
  • Not a common password
  • Passwords match :white_check_mark:

Demo

365f1998-296b-49ce-9007-c11efbaf4a18

Javascript

Ensure that you replace the field and component IDs.

TB.render('component_5', function (data) {
  const $ = jQuery;
  const $root  = $(data.ele);
  const $block = $root.find('#field_block_password');
  if (!$block.length) return;

  // Inputs
  const $cur = $block.find('#field_old4PzQ4GNJGV'); // 6-digit current/temp
  const $new = $block.find('#field4PzQ4GNJGV');     // new
  const $con = $block.find('#field_con4PzQ4GNJGV'); // confirm
  if (!$cur.length || !$new.length || !$con.length) return;

  // ---------- Setup / one-time ----------
  // Placeholders & hide tiny inline labels
  $block.find('.small-label').remove();
  $cur.attr({ placeholder: 'Current Password', inputmode: 'numeric' });
  $new.attr({ placeholder: 'New Password' });
  $con.attr({ placeholder: 'Re-Enter New Password' });

  // Autofill discouragement (keep main label visible)
  [$cur, $new, $con].forEach($i =>
    $i.attr({ autocomplete: 'new-password', autocapitalize: 'none', autocorrect: 'off', spellcheck: 'false' })
  );

  // Honeypot (soaks up Chrome/Edge autofill)
  if (!$root.find('#af-bait-user').length) {
    $block.before(
      `<div class="autofill-bait" aria-hidden="true">
         <input id="af-bait-user" type="text" name="username" autocomplete="username">
         <input id="af-bait-pass" type="password" name="password" autocomplete="current-password">
       </div>`
    );
  }

  // Keep current empty until user interacts; prevent autofill writes
  $cur.val('').prop('readOnly', true).one('pointerdown keydown focus', () => $cur.prop('readOnly', false));

  // Layout wrappers (fields left, meter right)
  if (!$block.parent('.pw-field-col').length) $block.wrap('<div class="pw-field-col"></div>');
  const $fieldCol = $block.parent('.pw-field-col');

  // Strength meter (create once)
  let $meterCol = $root.find('.pw-meter-col');
  if (!$meterCol.length) {
    $meterCol = $(`
      <div class="pw-meter-col">
        <div class="tb-strength">
          <div class="tb-strength-header">
            <span>Password Strength:</span>
            <span id="tb-strength-text">Very Weak</span>
          </div>
          <div class="tb-strength-bar" aria-hidden="true">
            <span class="seg"></span><span class="seg"></span><span class="seg"></span><span class="seg"></span>
          </div>
          <ul class="tb-strength-list">
            <li data-cond="case"><i class="far fa-times-circle"></i> 1 lowercase & 1 uppercase</li>
            <li data-cond="num"><i class="far fa-times-circle"></i> 1 number (0–9)</li>
            <li data-cond="spec"><i class="far fa-times-circle"></i> 1 special [!@#$%^&*]</li>
            <li data-cond="len"><i class="far fa-times-circle"></i> At least 8 characters</li>
            <li data-cond="common"><i class="far fa-times-circle"></i> No common words</li>
            <li data-cond="match" class="muted"><i class="far fa-times-circle"></i> Passwords match</li>
          </ul>
        </div>
      </div>
    `);
    $fieldCol.after($meterCol);
  }

  // Step wrappers
  const wrapStep = ($input, id) => ($input.closest('.pw-step').length ? $input.closest('.pw-step') : $input.wrap(`<div class="pw-step" id="${id}"></div>`).parent());
  const $stepCur = wrapStep($cur, 'pw-step-current').addClass('is-visible');
  const $stepNew = wrapStep($new, 'pw-step-new').removeClass('is-visible');
  const $stepCon = wrapStep($con, 'pw-step-confirm').removeClass('is-visible');

  // Cached meter bits
  const $seg = $meterCol.find('.tb-strength-bar .seg');
  const $txt = $meterCol.find('#tb-strength-text');
  const $lis = $meterCol.find('.tb-strength-list li');
  const liFor = (key) => $lis.filter(`[data-cond="${key}"]`);

  // Regex cache
  const reLower = /[a-z]/, reUpper = /[A-Z]/, reNum = /[0-9]/, reSpec = /[!@#$%^&*]/;
  const COMMON = new Set(['password','123456','123456789','qwerty','admin','letmein','welcome']);

  // Small helpers
  const toggleOk = ($li, ok) => {
    // only mutate if state changed
    if (!!$li.hasClass('ok') !== !!ok) $li.toggleClass('ok', !!ok);
    const $icon = $li.children('i');
    $icon.toggleClass('fa-check-circle', !!ok).toggleClass('fa-times-circle', !ok);
  };
  const setSegs = (score) => {
    $seg.each((i, el) => {
      const want = i < score;
      const has = el.classList.contains('on');
      if (want !== has) el.classList.toggle('on', want);
    });
    const label = ['Very Weak','Weak','Fair','Good','Strong'][score];
    if ($txt.text() !== label) $txt.text(label);
  };

  // Compute strength score (0..4) and update checklist/bars (no match here)
  const computeStrength = (pwd) => {
    const hasLower = reLower.test(pwd);
    const hasUpper = reUpper.test(pwd);
    const hasNum   = reNum.test(pwd);
    const hasSpec  = reSpec.test(pwd);
    const okLen    = pwd.length >= 8;
    const notCommon= pwd ? !COMMON.has(pwd.toLowerCase()) : false;

    toggleOk(liFor('case'),   hasLower && hasUpper);
    toggleOk(liFor('num'),    hasNum);
    toggleOk(liFor('spec'),   hasSpec);
    toggleOk(liFor('len'),    okLen);
    toggleOk(liFor('common'), notCommon);

    const score =
      (hasLower && hasUpper ? 1 : 0) +
      (hasNum ? 1 : 0) +
      (hasSpec ? 1 : 0) +
      ((okLen && notCommon) ? 1 : 0);

    setSegs(score);
    return score;
  };

  const updateMatch = () => {
    const match = $new.val() && $new.val() === $con.val();
    toggleOk(liFor('match'), match);
    return match;
  };

  const setVisible = ($el, show) => {
    const has = $el.hasClass('is-visible');
    if (has !== show) $el.toggleClass('is-visible', show);
  };

  const setDisabled = (disabled) => {
    $root.find('.af-form-submit, button[ng-click="resetPass()"]').prop('disabled', !!disabled);
  };

  // Main pipeline — run as needed
  const updateAll = () => {
    const curOK = ($cur.val() || '').length >= 6;
    setVisible($stepNew, curOK);

    const score = computeStrength($new.val());
    setVisible($stepCon, score >= 4);            // reveal confirm when all checks (except match) pass

    const enable = (score >= 4) && updateMatch(); // buttons enabled only when strong + match
    setDisabled(!enable);
  };

  // Events
  $cur.on('input blur', updateAll);
  $new.on('input blur', updateAll);
  $con.on('input blur', updateAll);

  // Autofill guard: briefly watch for unwanted value injection, then stop
  let guardTicks = 0;
  const guard = setInterval(() => {
    guardTicks++;
    if (($cur.val() || '').length > 0 && document.activeElement !== $cur[0]) {
      $cur.val('').trigger('input');
    }
    if (guardTicks > 15 || document.visibilityState === 'hidden') clearInterval(guard);
  }, 250);

  // First paint
  updateAll();
});

CSS

:root {
  --orange:   #E86100;
  --blue:     #0B2341;
  --darkblue: #071526;
  --lightblue:#0F2C51;
  --gray:     #f3f6f9;
  --white:    #fff;
  --textsize: 16px;
  --augray:   #E7E9EC;
  --red:      #f44336;
  --borderred:#d43f3a;
  --darkgreen:#1d7a32;
  --green:    #28a745;
}

/* Honeypot: present for browser, invisible to user */
.autofill-bait{
  position:absolute !important;
  left:-10000px !important;
  width:1px !important;
  height:1px !important;
  opacity:0 !important;
  pointer-events:none !important;
}

/* Two-column layout */
.pw-field-col, .pw-meter-col{ box-sizing:border-box; }
.pw-field-col{ width:58%; display:inline-block; vertical-align:top; padding-right:16px; }
.pw-meter-col{ width:42%; display:inline-block; vertical-align:top; padding-left:16px; }
@media (max-width: 900px){
  .pw-field-col, .pw-meter-col { width:100%; padding:0; display:block; }
  .pw-meter-col { margin-top:12px; }
}

/* Step transitions */
.pw-step{
  overflow:hidden; max-height:0; opacity:0; transform:translateY(-6px);
  transition:max-height 280ms ease, opacity 220ms ease, transform 280ms ease;
  margin-bottom:10px;
}
.pw-step.is-visible{ max-height:120px; opacity:1; transform:translateY(0); }

/* Inputs */
.pw-step input.form-control{
  padding:10px 12px; border:1px solid var(--augray); border-radius:8px;
  transition:box-shadow 180ms ease, border-color 180ms ease; background:#fff;
}
.pw-step input.form-control:focus{
  border-color:var(--lightblue);
  box-shadow:0 0 0 3px color-mix(in srgb, var(--lightblue) 20%, transparent);
}

/* Strength meter */
.tb-strength{ background:var(--gray); border:1px solid var(--augray); border-radius:10px; padding:14px 16px; }
.tb-strength-header{ display:flex; justify-content:space-between; align-items:center; font-size:14px; margin-bottom:8px; color:var(--darkblue); }
#tb-strength-text{ font-weight:600; }
.tb-strength-bar{ display:grid; grid-template-columns:repeat(4,1fr); gap:6px; margin-bottom:10px; }
.tb-strength-bar .seg{
  display:block; height:10px; border-radius:8px; background:#e4e7ec; border:1px solid #d8dbe2;
  transition:background-color 200ms ease, border-color 200ms ease;
}
.tb-strength-bar .seg.on:nth-child(1){ background:#ffb3a8; border-color:#ff8e80; }
.tb-strength-bar .seg.on:nth-child(2){ background:#ffd79a; border-color:#ffc666; }
.tb-strength-bar .seg.on:nth-child(3){ background:#cfe7a3; border-color:#b8db74; }
.tb-strength-bar .seg.on:nth-child(4){ background:#a8e1b2; border-color:#7cd28e; }
.tb-strength-list{ list-style:none; padding:0; margin:0; }
.tb-strength-list li{ display:flex; align-items:center; gap:8px; font-size:13px; line-height:1.4; color:#5b667a; margin:4px 0; }
.tb-strength-list li.muted{ opacity:.9; }
.tb-strength-list li.ok{ color:var(--darkgreen); }
.tb-strength-list li.ok i{ color:var(--green); }

/* Safety: ensure original sub-labels stay hidden */
#field_block_password .small-label{ display:none !important; }

3 Likes