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

Demo

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; }