},
{
"path": "./dist/css/bootstrap.css",
- "maxSize": "35.5 kB"
+ "maxSize": "35.75 kB"
},
{
"path": "./dist/css/bootstrap.min.css",
- "maxSize": "32.0 kB"
+ "maxSize": "32.25 kB"
},
{
"path": "./dist/js/bootstrap.bundle.js",
- "maxSize": "47.0 kB"
+ "maxSize": "48.5 kB"
},
{
"path": "./dist/js/bootstrap.bundle.min.js",
- "maxSize": "24.5 kB"
+ "maxSize": "25.25 kB"
},
{
"path": "./dist/js/bootstrap.esm.js",
- "maxSize": "33.5 kB"
+ "maxSize": "34.75 kB"
},
{
"path": "./dist/js/bootstrap.esm.min.js",
- "maxSize": "20.0 kB"
+ "maxSize": "21.0 kB"
},
{
"path": "./dist/js/bootstrap.js",
- "maxSize": "34.0 kB"
+ "maxSize": "35.25 kB"
},
{
"path": "./dist/js/bootstrap.min.js",
- "maxSize": "18.25 kB"
+ "maxSize": "19.0 kB"
}
],
"ci": {
export { default as Dialog } from './src/dialog.js'
export { default as Dropdown } from './src/dropdown.js'
export { default as Offcanvas } from './src/offcanvas.js'
+export { default as Strength } from './src/strength.js'
export { default as Popover } from './src/popover.js'
export { default as ScrollSpy } from './src/scrollspy.js'
export { default as Tab } from './src/tab.js'
import Dialog from './src/dialog.js'
import Dropdown from './src/dropdown.js'
import Offcanvas from './src/offcanvas.js'
+import Strength from './src/strength.js'
import Popover from './src/popover.js'
import ScrollSpy from './src/scrollspy.js'
import Tab from './src/tab.js'
Dialog,
Dropdown,
Offcanvas,
+ Strength,
Popover,
ScrollSpy,
Tab,
--- /dev/null
+/**
+ * --------------------------------------------------------------------------
+ * Bootstrap strength.js
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
+ * --------------------------------------------------------------------------
+ */
+
+import BaseComponent from './base-component.js'
+import EventHandler from './dom/event-handler.js'
+import SelectorEngine from './dom/selector-engine.js'
+
+/**
+ * Constants
+ */
+
+const NAME = 'strength'
+const DATA_KEY = 'bs.strength'
+const EVENT_KEY = `.${DATA_KEY}`
+const DATA_API_KEY = '.data-api'
+
+const EVENT_STRENGTH_CHANGE = `strengthChange${EVENT_KEY}`
+
+const SELECTOR_DATA_STRENGTH = '[data-bs-strength]'
+
+const STRENGTH_LEVELS = ['weak', 'fair', 'good', 'strong']
+
+const Default = {
+ input: null, // Selector or element for password input
+ minLength: 8,
+ messages: {
+ weak: 'Weak',
+ fair: 'Fair',
+ good: 'Good',
+ strong: 'Strong'
+ },
+ weights: {
+ minLength: 1,
+ extraLength: 1,
+ lowercase: 1,
+ uppercase: 1,
+ numbers: 1,
+ special: 1,
+ multipleSpecial: 1,
+ longPassword: 1
+ },
+ thresholds: [2, 4, 6], // weak ≤2, fair ≤4, good ≤6, strong >6
+ scorer: null // Custom scoring function (password) => number
+}
+
+const DefaultType = {
+ input: '(string|element|null)',
+ minLength: 'number',
+ messages: 'object',
+ weights: 'object',
+ thresholds: 'array',
+ scorer: '(function|null)'
+}
+
+/**
+ * Class definition
+ */
+
+class Strength extends BaseComponent {
+ constructor(element, config) {
+ super(element, config)
+
+ this._input = this._getInput()
+ this._segments = SelectorEngine.find('.strength-segment', this._element)
+ this._textElement = SelectorEngine.findOne('.strength-text', this._element.parentElement)
+ this._currentStrength = null
+
+ if (this._input) {
+ this._addEventListeners()
+ // Check initial value
+ this._evaluate()
+ }
+ }
+
+ // Getters
+ static get Default() {
+ return Default
+ }
+
+ static get DefaultType() {
+ return DefaultType
+ }
+
+ static get NAME() {
+ return NAME
+ }
+
+ // Public
+ getStrength() {
+ return this._currentStrength
+ }
+
+ evaluate() {
+ this._evaluate()
+ }
+
+ // Private
+ _getInput() {
+ if (this._config.input) {
+ return typeof this._config.input === 'string' ?
+ SelectorEngine.findOne(this._config.input) :
+ this._config.input
+ }
+
+ // Look for preceding password input
+ const parent = this._element.parentElement
+ return SelectorEngine.findOne('input[type="password"]', parent)
+ }
+
+ _addEventListeners() {
+ EventHandler.on(this._input, 'input', () => this._evaluate())
+ EventHandler.on(this._input, 'change', () => this._evaluate())
+ }
+
+ _evaluate() {
+ const password = this._input.value
+ const score = this._calculateScore(password)
+ const strength = this._scoreToStrength(score)
+
+ if (strength !== this._currentStrength) {
+ this._currentStrength = strength
+ this._updateUI(strength, score)
+
+ EventHandler.trigger(this._element, EVENT_STRENGTH_CHANGE, {
+ strength,
+ score,
+ password: password.length > 0 ? '***' : '' // Don't expose actual password
+ })
+ }
+ }
+
+ _calculateScore(password) {
+ if (!password) {
+ return 0
+ }
+
+ // Use custom scorer if provided
+ if (typeof this._config.scorer === 'function') {
+ return this._config.scorer(password)
+ }
+
+ const { weights } = this._config
+ let score = 0
+
+ // Length scoring
+ if (password.length >= this._config.minLength) {
+ score += weights.minLength
+ }
+
+ if (password.length >= this._config.minLength + 4) {
+ score += weights.extraLength
+ }
+
+ // Character variety
+ if (/[a-z]/.test(password)) {
+ score += weights.lowercase
+ }
+
+ if (/[A-Z]/.test(password)) {
+ score += weights.uppercase
+ }
+
+ if (/\d/.test(password)) {
+ score += weights.numbers
+ }
+
+ // Special characters
+ if (/[!@#$%^&*(),.?":{}|<>]/.test(password)) {
+ score += weights.special
+ }
+
+ // Extra points for more special chars or length
+ if (/[!@#$%^&*(),.?":{}|<>].*[!@#$%^&*(),.?":{}|<>]/.test(password)) {
+ score += weights.multipleSpecial
+ }
+
+ if (password.length >= 16) {
+ score += weights.longPassword
+ }
+
+ return score
+ }
+
+ _scoreToStrength(score) {
+ if (score === 0) {
+ return null
+ }
+
+ const [weak, fair, good] = this._config.thresholds
+
+ if (score <= weak) {
+ return 'weak'
+ }
+
+ if (score <= fair) {
+ return 'fair'
+ }
+
+ if (score <= good) {
+ return 'good'
+ }
+
+ return 'strong'
+ }
+
+ _updateUI(strength) {
+ // Update data attribute on element
+ if (strength) {
+ this._element.dataset.bsStrength = strength
+ } else {
+ delete this._element.dataset.bsStrength
+ }
+
+ // Update segmented meter
+ const strengthIndex = strength ? STRENGTH_LEVELS.indexOf(strength) : -1
+
+ for (const [index, segment] of this._segments.entries()) {
+ if (index <= strengthIndex) {
+ segment.classList.add('active')
+ } else {
+ segment.classList.remove('active')
+ }
+ }
+
+ // Update text feedback
+ if (this._textElement) {
+ if (strength && this._config.messages[strength]) {
+ this._textElement.textContent = this._config.messages[strength]
+ this._textElement.dataset.bsStrength = strength
+
+ // Also set the color via inheriting from parent or using CSS variable
+ const colorMap = {
+ weak: 'danger',
+ fair: 'warning',
+ good: 'info',
+ strong: 'success'
+ }
+ this._textElement.style.setProperty('--strength-color', `var(--${colorMap[strength]}-text)`)
+ } else {
+ this._textElement.textContent = ''
+ delete this._textElement.dataset.bsStrength
+ }
+ }
+ }
+}
+
+/**
+ * Data API implementation
+ */
+
+EventHandler.on(document, `DOMContentLoaded${EVENT_KEY}${DATA_API_KEY}`, () => {
+ for (const element of SelectorEngine.find(SELECTOR_DATA_STRENGTH)) {
+ Strength.getOrCreateInstance(element)
+ }
+})
+
+export default Strength
--- /dev/null
+import Strength from '../../src/strength.js'
+import { clearFixture, createEvent, getFixture } from '../helpers/fixture.js'
+
+describe('Strength', () => {
+ let fixtureEl
+
+ beforeAll(() => {
+ fixtureEl = getFixture()
+ })
+
+ afterEach(() => {
+ clearFixture()
+ })
+
+ const getStrengthHtml = (options = {}) => {
+ const { segments = 4, withText = false, inputId = 'password' } = options
+ const segmentHtml = Array.from({ length: segments })
+ .map(() => '<div class="strength-segment"></div>')
+ .join('')
+ const textHtml = withText ? '<span class="strength-text"></span>' : ''
+
+ return `
+ <div>
+ <input type="password" id="${inputId}" class="form-control">
+ <div class="strength" data-bs-strength>
+ ${segmentHtml}
+ </div>
+ ${textHtml}
+ </div>
+ `
+ }
+
+ const getStrengthBarHtml = (inputId = 'password') => {
+ return `
+ <div>
+ <input type="password" id="${inputId}" class="form-control">
+ <div class="strength-bar" data-bs-strength></div>
+ </div>
+ `
+ }
+
+ describe('VERSION', () => {
+ it('should return plugin version', () => {
+ expect(Strength.VERSION).toEqual(jasmine.any(String))
+ })
+ })
+
+ describe('DATA_KEY', () => {
+ it('should return plugin data key', () => {
+ expect(Strength.DATA_KEY).toEqual('bs.strength')
+ })
+ })
+
+ describe('Default', () => {
+ it('should return default config', () => {
+ expect(Strength.Default).toEqual(jasmine.any(Object))
+ expect(Strength.Default.minLength).toEqual(8)
+ expect(Strength.Default.thresholds).toEqual([2, 4, 6])
+ })
+ })
+
+ describe('DefaultType', () => {
+ it('should return default type config', () => {
+ expect(Strength.DefaultType).toEqual(jasmine.any(Object))
+ })
+ })
+
+ describe('constructor', () => {
+ it('should take care of element either passed as a CSS selector or DOM element', () => {
+ fixtureEl.innerHTML = getStrengthHtml()
+
+ const strengthEl = fixtureEl.querySelector('.strength')
+ const strengthBySelector = new Strength('.strength')
+ const strengthByElement = new Strength(strengthEl)
+
+ expect(strengthBySelector._element).toEqual(strengthEl)
+ expect(strengthByElement._element).toEqual(strengthEl)
+ })
+
+ it('should find password input in parent container', () => {
+ fixtureEl.innerHTML = getStrengthHtml()
+
+ const strengthEl = fixtureEl.querySelector('.strength')
+ const passwordInput = fixtureEl.querySelector('input[type="password"]')
+ const strength = new Strength(strengthEl)
+
+ expect(strength._input).toEqual(passwordInput)
+ })
+
+ it('should use custom input selector', () => {
+ fixtureEl.innerHTML = `
+ <div>
+ <input type="password" id="other-password" class="form-control">
+ </div>
+ <div class="strength" data-bs-strength>
+ <div class="strength-segment"></div>
+ <div class="strength-segment"></div>
+ <div class="strength-segment"></div>
+ <div class="strength-segment"></div>
+ </div>
+ `
+
+ const strengthEl = fixtureEl.querySelector('.strength')
+ const otherInput = fixtureEl.querySelector('#other-password')
+ const strength = new Strength(strengthEl, { input: '#other-password' })
+
+ expect(strength._input).toEqual(otherInput)
+ })
+ })
+
+ describe('getStrength', () => {
+ it('should return null initially', () => {
+ fixtureEl.innerHTML = getStrengthHtml()
+
+ const strengthEl = fixtureEl.querySelector('.strength')
+ const strength = new Strength(strengthEl)
+
+ expect(strength.getStrength()).toBeNull()
+ })
+
+ it('should return current strength level', () => {
+ fixtureEl.innerHTML = getStrengthHtml()
+
+ const strengthEl = fixtureEl.querySelector('.strength')
+ const passwordInput = fixtureEl.querySelector('input[type="password"]')
+ const strength = new Strength(strengthEl)
+
+ // Weak password - just meets min length
+ passwordInput.value = 'password'
+ passwordInput.dispatchEvent(createEvent('input'))
+
+ expect(strength.getStrength()).toEqual('weak')
+ })
+ })
+
+ describe('evaluate', () => {
+ it('should manually trigger evaluation', () => {
+ fixtureEl.innerHTML = getStrengthHtml()
+
+ const strengthEl = fixtureEl.querySelector('.strength')
+ const passwordInput = fixtureEl.querySelector('input[type="password"]')
+ const strength = new Strength(strengthEl)
+
+ passwordInput.value = 'password123'
+ strength.evaluate()
+
+ expect(strength.getStrength()).not.toBeNull()
+ })
+ })
+
+ describe('scoring', () => {
+ it('should return weak for short passwords meeting min length', () => {
+ fixtureEl.innerHTML = getStrengthHtml()
+
+ const strengthEl = fixtureEl.querySelector('.strength')
+ const passwordInput = fixtureEl.querySelector('input[type="password"]')
+ const strength = new Strength(strengthEl)
+
+ passwordInput.value = 'password' // 8 chars, lowercase only = score 2
+ passwordInput.dispatchEvent(createEvent('input'))
+
+ expect(strength.getStrength()).toEqual('weak')
+ })
+
+ it('should return fair for passwords with more variety', () => {
+ fixtureEl.innerHTML = getStrengthHtml()
+
+ const strengthEl = fixtureEl.querySelector('.strength')
+ const passwordInput = fixtureEl.querySelector('input[type="password"]')
+ const strength = new Strength(strengthEl)
+
+ passwordInput.value = 'Password1' // 9 chars, lower+upper+number = score 4
+ passwordInput.dispatchEvent(createEvent('input'))
+
+ expect(strength.getStrength()).toEqual('fair')
+ })
+
+ it('should return good for strong passwords', () => {
+ fixtureEl.innerHTML = getStrengthHtml()
+
+ const strengthEl = fixtureEl.querySelector('.strength')
+ const passwordInput = fixtureEl.querySelector('input[type="password"]')
+ const strength = new Strength(strengthEl)
+
+ passwordInput.value = 'Password1!' // lower+upper+number+special+length = score 5-6
+ passwordInput.dispatchEvent(createEvent('input'))
+
+ expect(strength.getStrength()).toEqual('good')
+ })
+
+ it('should return strong for very strong passwords', () => {
+ fixtureEl.innerHTML = getStrengthHtml()
+
+ const strengthEl = fixtureEl.querySelector('.strength')
+ const passwordInput = fixtureEl.querySelector('input[type="password"]')
+ const strength = new Strength(strengthEl)
+
+ // Long password with all criteria
+ passwordInput.value = 'MyStr0ngP@ssword!!'
+ passwordInput.dispatchEvent(createEvent('input'))
+
+ expect(strength.getStrength()).toEqual('strong')
+ })
+
+ it('should return null for empty password', () => {
+ fixtureEl.innerHTML = getStrengthHtml()
+
+ const strengthEl = fixtureEl.querySelector('.strength')
+ const passwordInput = fixtureEl.querySelector('input[type="password"]')
+ const strength = new Strength(strengthEl)
+
+ passwordInput.value = ''
+ passwordInput.dispatchEvent(createEvent('input'))
+
+ expect(strength.getStrength()).toBeNull()
+ })
+ })
+
+ describe('custom scorer', () => {
+ it('should use custom scoring function', () => {
+ fixtureEl.innerHTML = getStrengthHtml()
+
+ const strengthEl = fixtureEl.querySelector('.strength')
+ const passwordInput = fixtureEl.querySelector('input[type="password"]')
+ const customScorer = password => password.length >= 10 ? 8 : 1
+ const strength = new Strength(strengthEl, { scorer: customScorer })
+
+ passwordInput.value = 'short'
+ passwordInput.dispatchEvent(createEvent('input'))
+ expect(strength.getStrength()).toEqual('weak')
+
+ passwordInput.value = 'longenough'
+ passwordInput.dispatchEvent(createEvent('input'))
+ expect(strength.getStrength()).toEqual('strong')
+ })
+ })
+
+ describe('custom thresholds', () => {
+ it('should use custom threshold values', () => {
+ fixtureEl.innerHTML = getStrengthHtml()
+
+ const strengthEl = fixtureEl.querySelector('.strength')
+ const passwordInput = fixtureEl.querySelector('input[type="password"]')
+ const strength = new Strength(strengthEl, { thresholds: [1, 2, 3] })
+
+ // With low thresholds, even weak passwords rate higher
+ passwordInput.value = 'password'
+ passwordInput.dispatchEvent(createEvent('input'))
+
+ // lowercase + minLength = 2, which is now "fair" with [1,2,3]
+ expect(strength.getStrength()).toEqual('fair')
+ })
+ })
+
+ describe('custom minLength', () => {
+ it('should use custom minimum length', () => {
+ fixtureEl.innerHTML = getStrengthHtml()
+
+ const strengthEl = fixtureEl.querySelector('.strength')
+ const passwordInput = fixtureEl.querySelector('input[type="password"]')
+ const strength = new Strength(strengthEl, { minLength: 12 })
+
+ passwordInput.value = 'password' // Only 8 chars, doesn't meet min
+ passwordInput.dispatchEvent(createEvent('input'))
+
+ // Should be weak since it doesn't meet minLength requirement
+ expect(strength.getStrength()).toEqual('weak')
+ })
+ })
+
+ describe('UI updates', () => {
+ it('should set data-bs-strength attribute', () => {
+ fixtureEl.innerHTML = getStrengthHtml()
+
+ const strengthEl = fixtureEl.querySelector('.strength')
+ const passwordInput = fixtureEl.querySelector('input[type="password"]')
+ new Strength(strengthEl) // eslint-disable-line no-new
+
+ passwordInput.value = 'password'
+ passwordInput.dispatchEvent(createEvent('input'))
+
+ expect(strengthEl.dataset.bsStrength).toEqual('weak')
+ })
+
+ it('should remove data-bs-strength attribute when empty', () => {
+ fixtureEl.innerHTML = getStrengthHtml()
+
+ const strengthEl = fixtureEl.querySelector('.strength')
+ const passwordInput = fixtureEl.querySelector('input[type="password"]')
+ new Strength(strengthEl) // eslint-disable-line no-new
+
+ passwordInput.value = 'password'
+ passwordInput.dispatchEvent(createEvent('input'))
+
+ expect(strengthEl.dataset.bsStrength).toEqual('weak')
+
+ passwordInput.value = ''
+ passwordInput.dispatchEvent(createEvent('input'))
+
+ expect(strengthEl.dataset.bsStrength).toBeUndefined()
+ })
+
+ it('should add active class to appropriate segments', () => {
+ fixtureEl.innerHTML = getStrengthHtml()
+
+ const strengthEl = fixtureEl.querySelector('.strength')
+ const passwordInput = fixtureEl.querySelector('input[type="password"]')
+ const segments = strengthEl.querySelectorAll('.strength-segment')
+ new Strength(strengthEl) // eslint-disable-line no-new
+
+ // Weak - 1 segment active
+ passwordInput.value = 'password'
+ passwordInput.dispatchEvent(createEvent('input'))
+
+ expect(segments[0]).toHaveClass('active')
+ expect(segments[1]).not.toHaveClass('active')
+ expect(segments[2]).not.toHaveClass('active')
+ expect(segments[3]).not.toHaveClass('active')
+ })
+
+ it('should update text element with message', () => {
+ fixtureEl.innerHTML = getStrengthHtml({ withText: true })
+
+ const strengthEl = fixtureEl.querySelector('.strength')
+ const passwordInput = fixtureEl.querySelector('input[type="password"]')
+ const textEl = fixtureEl.querySelector('.strength-text')
+ new Strength(strengthEl) // eslint-disable-line no-new
+
+ passwordInput.value = 'password'
+ passwordInput.dispatchEvent(createEvent('input'))
+
+ expect(textEl.textContent).toEqual('Weak')
+ })
+
+ it('should use custom messages', () => {
+ fixtureEl.innerHTML = getStrengthHtml({ withText: true })
+
+ const strengthEl = fixtureEl.querySelector('.strength')
+ const passwordInput = fixtureEl.querySelector('input[type="password"]')
+ const textEl = fixtureEl.querySelector('.strength-text')
+ new Strength(strengthEl, { // eslint-disable-line no-new
+ messages: {
+ weak: 'Too weak!',
+ fair: 'Getting better',
+ good: 'Nice!',
+ strong: 'Excellent!'
+ }
+ })
+
+ passwordInput.value = 'password'
+ passwordInput.dispatchEvent(createEvent('input'))
+
+ expect(textEl.textContent).toEqual('Too weak!')
+ })
+ })
+
+ describe('events', () => {
+ it('should trigger strengthChange event', () => {
+ return new Promise(resolve => {
+ fixtureEl.innerHTML = getStrengthHtml()
+
+ const strengthEl = fixtureEl.querySelector('.strength')
+ const passwordInput = fixtureEl.querySelector('input[type="password"]')
+ new Strength(strengthEl) // eslint-disable-line no-new
+
+ strengthEl.addEventListener('strengthChange.bs.strength', event => {
+ expect(event.strength).toEqual('weak')
+ expect(event.score).toBeGreaterThan(0)
+ resolve()
+ })
+
+ passwordInput.value = 'password'
+ passwordInput.dispatchEvent(createEvent('input'))
+ })
+ })
+
+ it('should not expose actual password in event', () => {
+ return new Promise(resolve => {
+ fixtureEl.innerHTML = getStrengthHtml()
+
+ const strengthEl = fixtureEl.querySelector('.strength')
+ const passwordInput = fixtureEl.querySelector('input[type="password"]')
+ new Strength(strengthEl) // eslint-disable-line no-new
+
+ strengthEl.addEventListener('strengthChange.bs.strength', event => {
+ expect(event.password).toEqual('***')
+ resolve()
+ })
+
+ passwordInput.value = 'password'
+ passwordInput.dispatchEvent(createEvent('input'))
+ })
+ })
+ })
+
+ describe('strength-bar variant', () => {
+ it('should work with strength-bar element', () => {
+ fixtureEl.innerHTML = getStrengthBarHtml()
+
+ const strengthEl = fixtureEl.querySelector('.strength-bar')
+ const passwordInput = fixtureEl.querySelector('input[type="password"]')
+ const strength = new Strength(strengthEl)
+
+ passwordInput.value = 'password'
+ passwordInput.dispatchEvent(createEvent('input'))
+
+ expect(strength.getStrength()).toEqual('weak')
+ expect(strengthEl.dataset.bsStrength).toEqual('weak')
+ })
+ })
+
+ describe('dispose', () => {
+ it('should dispose the instance', () => {
+ fixtureEl.innerHTML = getStrengthHtml()
+
+ const strengthEl = fixtureEl.querySelector('.strength')
+ const strength = new Strength(strengthEl)
+
+ expect(Strength.getInstance(strengthEl)).not.toBeNull()
+
+ strength.dispose()
+
+ expect(Strength.getInstance(strengthEl)).toBeNull()
+ })
+ })
+
+ describe('getInstance', () => {
+ it('should return strength instance', () => {
+ fixtureEl.innerHTML = getStrengthHtml()
+
+ const strengthEl = fixtureEl.querySelector('.strength')
+ const strength = new Strength(strengthEl)
+
+ expect(Strength.getInstance(strengthEl)).toEqual(strength)
+ expect(Strength.getInstance(strengthEl)).toBeInstanceOf(Strength)
+ })
+
+ it('should return null when there is no instance', () => {
+ fixtureEl.innerHTML = '<div></div>'
+
+ const div = fixtureEl.querySelector('div')
+
+ expect(Strength.getInstance(div)).toBeNull()
+ })
+ })
+
+ describe('getOrCreateInstance', () => {
+ it('should return existing instance', () => {
+ fixtureEl.innerHTML = getStrengthHtml()
+
+ const strengthEl = fixtureEl.querySelector('.strength')
+ const strength = new Strength(strengthEl)
+
+ expect(Strength.getOrCreateInstance(strengthEl)).toEqual(strength)
+ expect(Strength.getOrCreateInstance(strengthEl)).toBeInstanceOf(Strength)
+ })
+
+ it('should create new instance when none exists', () => {
+ fixtureEl.innerHTML = getStrengthHtml()
+
+ const strengthEl = fixtureEl.querySelector('.strength')
+
+ expect(Strength.getInstance(strengthEl)).toBeNull()
+ expect(Strength.getOrCreateInstance(strengthEl)).toBeInstanceOf(Strength)
+ })
+ })
+})
--- /dev/null
+@use "../config" as *;
+@use "../variables" as *;
+@use "../mixins/border-radius" as *;
+@use "../mixins/transition" as *;
+@use "form-variables" as *;
+
+// scss-docs-start strength-variables
+$strength-height: .375rem !default;
+$strength-gap: .25rem !default;
+$strength-margin-top: .25rem !default;
+$strength-border-radius: var(--border-radius-pill) !default;
+$strength-bg: var(--bg-2) !default;
+$strength-transition: background-color .2s ease-in-out, width .3s ease-in-out !default;
+
+$strength-weak-color: var(--danger-bg) !default;
+$strength-fair-color: var(--warning-bg) !default;
+$strength-good-color: var(--info-bg) !default;
+$strength-strong-color: var(--success-bg) !default;
+// scss-docs-end strength-variables
+
+@layer forms {
+ // Strength meter container
+ .strength {
+ --strength-height: #{$strength-height};
+ --strength-gap: #{$strength-gap};
+ --strength-bg: #{$strength-bg};
+ --strength-border-radius: #{$strength-border-radius};
+ --strength-color: #{$strength-bg};
+
+ display: flex;
+ gap: var(--strength-gap);
+ width: 100%;
+ margin-top: $strength-margin-top;
+ }
+
+ // Individual strength segments
+ .strength-segment {
+ flex: 1;
+ height: var(--strength-height);
+ background-color: var(--strength-bg);
+ @include border-radius(var(--strength-border-radius));
+ @include transition($strength-transition);
+
+ // Filled state
+ &.active {
+ background-color: var(--strength-color);
+ }
+ }
+
+ // Strength levels - set the color variable
+ .strength[data-bs-strength="weak"] {
+ --strength-color: #{$strength-weak-color};
+ }
+
+ .strength[data-bs-strength="fair"] {
+ --strength-color: #{$strength-fair-color};
+ }
+
+ .strength[data-bs-strength="good"] {
+ --strength-color: #{$strength-good-color};
+ }
+
+ .strength[data-bs-strength="strong"] {
+ --strength-color: #{$strength-strong-color};
+ }
+
+ // Optional text feedback
+ .strength-text {
+ display: block;
+ margin-top: $strength-margin-top;
+ font-size: $small-font-size;
+ color: var(--strength-color, var(--secondary-color));
+ @include transition(color .2s ease-in-out);
+
+ // Hide when empty
+ &:empty {
+ display: none;
+ }
+ }
+
+ // Alternative: Single bar variant (like a progress bar)
+ .strength-bar {
+ --strength-height: #{$strength-height};
+ --strength-bg: #{$strength-bg};
+ --strength-border-radius: #{$strength-border-radius};
+ --strength-color: transparent;
+ --strength-width: 0%;
+
+ width: 100%;
+ height: var(--strength-height);
+ margin-top: $strength-margin-top;
+ overflow: hidden;
+ background-color: var(--strength-bg);
+ @include border-radius(var(--strength-border-radius));
+
+ &::after {
+ display: block;
+ width: var(--strength-width);
+ height: 100%;
+ content: "";
+ background-color: var(--strength-color);
+ @include border-radius(var(--strength-border-radius));
+ @include transition($strength-transition);
+ }
+
+ &[data-bs-strength="weak"] {
+ --strength-color: #{$strength-weak-color};
+ --strength-width: 25%;
+ }
+
+ &[data-bs-strength="fair"] {
+ --strength-color: #{$strength-fair-color};
+ --strength-width: 50%;
+ }
+
+ &[data-bs-strength="good"] {
+ --strength-color: #{$strength-good-color};
+ --strength-width: 75%;
+ }
+
+ &[data-bs-strength="strong"] {
+ --strength-color: #{$strength-strong-color};
+ --strength-width: 100%;
+ }
+ }
+}
@forward "form-range";
@forward "floating-labels";
@forward "input-group";
+@forward "strength";
@forward "validation";
- title: Range
- title: Input group
- title: Floating labels
+ - title: Password strength
- title: Layout
- title: Validation
--- /dev/null
+---
+title: Password strength
+description: Provide visual feedback on password strength with segmented meters or progress bars that update as users type.
+toc: true
+---
+
+## Overview
+
+Strength meters help users create secure passwords by providing real-time feedback on password complexity. Bootstrap's strength component offers:
+
+- **Segmented meter**: Four segments that fill based on strength level
+- **Progress bar variant**: A single bar that grows with strength
+- **Text feedback**: Optional text messages for each strength level
+- **Customizable scoring**: Configure minimum length and scoring criteria
+- **Minimal JavaScript**: Lightweight component following Bootstrap patterns
+
+## Examples
+
+### Segmented
+
+The default strength meter uses four segments that fill progressively as password strength increases. Add `data-bs-strength` to enable the JavaScript behavior.
+
+<Example code={`<div>
+ <label for="password1" class="form-label">Password</label>
+ <input type="password" class="form-control" id="password1" placeholder="Enter password">
+ <div class="strength" data-bs-strength>
+ <div class="strength-segment"></div>
+ <div class="strength-segment"></div>
+ <div class="strength-segment"></div>
+ <div class="strength-segment"></div>
+ </div>
+ </div>`} />
+
+### With text feedback
+
+Add a `.strength-text` element to display strength messages.
+
+<Example code={`<div>
+ <label for="password2" class="form-label">Password</label>
+ <input type="password" class="form-control" id="password2" placeholder="Enter password">
+ <div class="strength" data-bs-strength>
+ <div class="strength-segment"></div>
+ <div class="strength-segment"></div>
+ <div class="strength-segment"></div>
+ <div class="strength-segment"></div>
+ </div>
+ <span class="strength-text"></span>
+ </div>`} />
+
+### Progress bar
+
+Use `.strength-bar` for a single progress bar that grows with password strength.
+
+<Example code={`<div>
+ <label for="password3" class="form-label">Password</label>
+ <input type="password" class="form-control" id="password3" placeholder="Enter password">
+ <div class="strength-bar" data-bs-strength></div>
+ </div>`} />
+
+### Static segmented
+
+You can use the component without JavaScript for static displays by setting the `data-bs-strength` attribute manually.
+
+<Example class="vstack gap-3 fg-3" code={`<div>
+ <div>Weak</div>
+ <div class="strength" data-bs-strength="weak">
+ <div class="strength-segment active"></div>
+ <div class="strength-segment"></div>
+ <div class="strength-segment"></div>
+ <div class="strength-segment"></div>
+ </div>
+ </div>
+ <div>
+ <div>Fair</div>
+ <div class="strength" data-bs-strength="fair">
+ <div class="strength-segment active"></div>
+ <div class="strength-segment active"></div>
+ <div class="strength-segment"></div>
+ <div class="strength-segment"></div>
+ </div>
+ </div>
+ <div>
+ <div>Good</div>
+ <div class="strength" data-bs-strength="good">
+ <div class="strength-segment active"></div>
+ <div class="strength-segment active"></div>
+ <div class="strength-segment active"></div>
+ <div class="strength-segment"></div>
+ </div>
+ </div>
+ <div>
+ <div>Strong</div>
+ <div class="strength" data-bs-strength="strong">
+ <div class="strength-segment active"></div>
+ <div class="strength-segment active"></div>
+ <div class="strength-segment active"></div>
+ <div class="strength-segment active"></div>
+ </div>
+ </div>`} />
+
+### Static progress bar
+
+<Example class="vstack gap-3 fg-3" code={`<div>
+ <div>Weak</div>
+ <div class="strength-bar" data-bs-strength="weak"></div>
+ </div>
+ <div>
+ <div>Fair</div>
+ <div class="strength-bar" data-bs-strength="fair"></div>
+ </div>
+ <div>
+ <div>Good</div>
+ <div class="strength-bar" data-bs-strength="good"></div>
+ </div>
+ <div>
+ <div>Strong</div>
+ <div class="strength-bar" data-bs-strength="strong"></div>
+ </div>`} />
+
+## Strength criteria
+
+The default scoring algorithm evaluates passwords based on:
+
+<BsTable>
+| Criteria | Points |
+| --- | --- |
+| Meets minimum length (8 characters) | +1 |
+| 4+ characters over minimum | +1 |
+| Contains lowercase letters | +1 |
+| Contains uppercase letters | +1 |
+| Contains numbers | +1 |
+| Contains special characters | +1 |
+| Multiple special characters | +1 |
+| 16+ characters | +1 |
+</BsTable>
+
+<BsTable>
+| Level | Points |
+| --- | --- |
+| Weak | 1-2 |
+| Fair | 3-4 |
+| Good | 5-6 |
+| Strong | 7-8 |
+</BsTable>
+
+## Usage
+
+### Via data attributes
+
+Add `data-bs-strength` to automatically initialize. The component will look for a password input in the same parent container.
+
+```html
+<div>
+ <input type="password" class="form-control">
+ <div class="strength" data-bs-strength>
+ <div class="strength-segment"></div>
+ <div class="strength-segment"></div>
+ <div class="strength-segment"></div>
+ <div class="strength-segment"></div>
+ </div>
+</div>
+```
+
+### Via JavaScript
+
+```js
+const element = document.querySelector('.strength')
+const strength = new bootstrap.Strength(element, {
+ input: '#my-password-input',
+ minLength: 10,
+ messages: {
+ weak: 'Too weak',
+ fair: 'Could be better',
+ good: 'Good password',
+ strong: 'Excellent!'
+ }
+})
+```
+
+### Options
+
+<BsTable>
+| Name | Type | Default | Description |
+| --- | --- | --- | --- |
+| `input` | string \| element | `null` | Selector or element for the password input. If not provided, looks for `input[type="password"]` in the parent. |
+| `minLength` | number | `8` | Minimum password length for first strength point. |
+| `messages` | object | `{weak, fair, good, strong}` | Custom messages for each strength level. |
+| `weights` | object | See below | Point values for each scoring criterion. Set to `0` to disable a criterion. |
+| `thresholds` | array | `[2, 4, 6]` | Score boundaries for strength levels: `[weak, fair, good]`. Scores above the last value are "strong". |
+| `scorer` | function | `null` | Custom scoring function `(password) => number`. Overrides built-in scoring when provided. |
+</BsTable>
+
+#### Default weights
+
+```js
+{
+ minLength: 1, // Meets minimum length
+ extraLength: 1, // 4+ characters over minimum
+ lowercase: 1, // Contains lowercase letters
+ uppercase: 1, // Contains uppercase letters
+ numbers: 1, // Contains numbers
+ special: 1, // Contains special characters
+ multipleSpecial: 1, // Multiple special characters
+ longPassword: 1 // 16+ characters
+}
+```
+
+#### Custom weights
+
+```js
+// Disable uppercase requirement, make special chars worth more
+new bootstrap.Strength(element, {
+ weights: {
+ uppercase: 0, // Don't require uppercase
+ special: 2 // Special chars worth 2 points
+ },
+ thresholds: [3, 5, 7] // Adjust thresholds for new max score
+})
+```
+
+#### Custom scorer
+
+```js
+// Fully custom scoring logic
+new bootstrap.Strength(element, {
+ scorer: (password) => {
+ let score = 0
+ if (password.length >= 12) score += 4
+ if (/[!@#$%]/.test(password)) score += 4
+ return score
+ },
+ thresholds: [2, 4, 6]
+})
+```
+
+### Methods
+
+| Method | Description |
+| --- | --- |
+| `getStrength()` | Returns the current strength level (`'weak'`, `'fair'`, `'good'`, `'strong'`, or `null`). |
+| `evaluate()` | Manually trigger a password evaluation. |
+| `dispose()` | Destroys the component instance. |
+
+```js
+const element = document.querySelector('.strength')
+const strength = bootstrap.Strength.getOrCreateInstance(element)
+
+// Get current strength
+console.log(strength.getStrength()) // 'fair'
+```
+
+### Events
+
+| Event | Description |
+| --- | --- |
+| `strengthChange.bs.strength` | Fired when the strength level changes. Includes `strength` and `score` properties. |
+
+```js
+const element = document.querySelector('.strength')
+
+element.addEventListener('strengthChange.bs.strength', event => {
+ console.log('Strength:', event.strength) // 'weak', 'fair', 'good', 'strong'
+ console.log('Score:', event.score) // 0-8
+})
+```
+
+## CSS-only usage
+
+For simple cases or server-rendered content, you can use the component without JavaScript by manually setting the `data-bs-strength` attribute and `.active` classes.
+
+```html
+<!-- Set via server-side logic -->
+<div class="strength" data-bs-strength="good">
+ <div class="strength-segment active"></div>
+ <div class="strength-segment active"></div>
+ <div class="strength-segment active"></div>
+ <div class="strength-segment"></div>
+</div>
+```
+
+## Accessibility
+
+- The strength meter is decorative feedback and should be used alongside clear password requirements text
+- Consider using `aria-describedby` to associate the password input with requirement text
+- Screen reader users benefit from the text feedback option which announces strength changes
+
+## CSS
+
+### Sass variables
+
+<ScssDocs name="strength-variables" file="scss/forms/_strength.scss" />