From 9669c0a9a98c294ebcf314165bad43b65953dab0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Kuna?= <1282324+codecalm@users.noreply.github.com> Date: Mon, 14 Sep 2026 20:22:23 +0200 Subject: [PATCH] Add a password strength meter (#3054) --- .changeset/strength.md | 7 + core/js/src/strength.ts | 278 ++++++++++++++++++ core/js/tabler.ts | 2 + core/js/tests/unit/strength.spec.ts | 200 +++++++++++++ core/scss/_core.scss | 1 + core/scss/_variables.scss | 17 ++ core/scss/ui/forms/_strength.scss | 63 ++++ docs/content/ui/forms/strength.mdx | 165 +++++++++++ preview/pages/password-strength.astro | 168 +++++++++++ screenshots/pages/password-strength.astro | 28 ++ shared/components/cards/SignUpCard.astro | 2 + shared/components/forms/FormElements5.astro | 6 + .../modals/ChangePasswordModalContent.astro | 37 +-- shared/data/docs.json | 4 + shared/data/menu.json | 4 + shared/ui/Strength.astro | 42 +++ 16 files changed, 990 insertions(+), 34 deletions(-) create mode 100644 .changeset/strength.md create mode 100644 core/js/src/strength.ts create mode 100644 core/js/tests/unit/strength.spec.ts create mode 100644 core/scss/ui/forms/_strength.scss create mode 100644 docs/content/ui/forms/strength.mdx create mode 100644 preview/pages/password-strength.astro create mode 100644 screenshots/pages/password-strength.astro create mode 100644 shared/ui/Strength.astro diff --git a/.changeset/strength.md b/.changeset/strength.md new file mode 100644 index 000000000..46a28ddcf --- /dev/null +++ b/.changeset/strength.md @@ -0,0 +1,7 @@ +--- +"@tabler/core": minor +"@tabler/preview": minor +"@tabler/docs": minor +--- + +Added a `Strength` password meter with a segmented bar, configurable scoring and `change.bs.strength`. diff --git a/core/js/src/strength.ts b/core/js/src/strength.ts new file mode 100644 index 000000000..1b3970aa4 --- /dev/null +++ b/core/js/src/strength.ts @@ -0,0 +1,278 @@ +/** + * -------------------------------------------------------------------------- + * Tabler strength.ts + * Licensed under MIT (https://github.com/tabler/tabler/blob/dev/LICENSE) + * -------------------------------------------------------------------------- + */ + +import BaseComponent from './bootstrap/base-component' +import EventHandler from './bootstrap/dom/event-handler' +import SelectorEngine from './bootstrap/dom/selector-engine' +import type { ElementSelector } from './bootstrap/types' + +type StrengthLevel = 'weak' | 'fair' | 'good' | 'strong' + +type ComponentConfig = { + /** selector of the password field; without it the meter looks in its own parent */ + input: string | null + minLength: number + messages: Record + weights: Record + /** score bounds: weak up to the first, fair to the second, good to the third */ + thresholds: number[] + /** replaces the built-in scoring; gets the password, returns a number */ + scorer: ((password: string) => number) | null +} + +type ComponentConfigInput = Partial & Record + +/** + * Constants + */ + +const NAME = 'strength' +const DATA_KEY = `bs.${NAME}` +const EVENT_KEY = `.${DATA_KEY}` + +const EVENT_CHANGE = `change${EVENT_KEY}` + +const CLASS_NAME_ACTIVE = 'active' + +const SELECTOR_DATA_STRENGTH = `[data-bs-${NAME}], [data-tblr-${NAME}]` +const SELECTOR_SEGMENT = `.${NAME}-segment` +const SELECTOR_TEXT = `.${NAME}-text` +const SELECTOR_PASSWORD = 'input[type="password"]' + +const LEVELS: StrengthLevel[] = ['weak', 'fair', 'good', 'strong'] + +const Default: ComponentConfig = { + input: null, + minLength: 8, + messages: { + weak: 'Weak', + fair: 'Fair', + good: 'Good', + strong: 'Strong', + }, + weights: { + minLength: 1, + extraLength: 1, + longPassword: 1, + lowercase: 1, + uppercase: 1, + numbers: 1, + special: 1, + multipleSpecial: 1, + }, + thresholds: [2, 4, 6], + scorer: null, +} + +const DefaultType: Record = { + input: '(string|null)', + minLength: 'number', + messages: 'object', + weights: 'object', + thresholds: 'array', + scorer: '(function|null)', +} + +/** + * Class definition + * + * Rates the password typed in a field and fills a segmented meter. The score + * is a hint for the user, never a validation: check the password on the + * server as well. + */ + +class Strength extends BaseComponent { + declare _element: HTMLElement + declare _config: ComponentConfig + _input: HTMLInputElement | null = null + _segments: HTMLElement[] = [] + _text: HTMLElement | null = null + // What the markup put in the label element: shown while the field is empty, + // so a form can keep its password rules there until a level replaces them. + _emptyText = '' + _level: StrengthLevel | null = null + _onInput = (): void => this.evaluate() + + constructor(element: ElementSelector, config?: ComponentConfigInput) { + super(element, config) + + if (!this._element) { + return + } + + this._input = this._getInput() + this._segments = SelectorEngine.find(SELECTOR_SEGMENT, this._element) + this._text = this._getText() + this._emptyText = this._text?.textContent?.trim() ?? '' + + this._setUpAria() + + if (!this._input) { + return + } + + this._input.addEventListener('input', this._onInput) + this.evaluate() + } + + // Getters + static get Default(): ComponentConfig { + return Default + } + + static get DefaultType(): Record { + return DefaultType + } + + static get NAME(): string { + return NAME + } + + get level(): StrengthLevel | null { + return this._level + } + + // Public + evaluate(): void { + if (!this._input) { + return + } + + const password = this._input.value + const score = this._score(password) + const level = this._level_(score) + + if (level === this._level) { + return + } + + this._level = level + this._render(level) + + // The password itself is never part of the payload. + EventHandler.trigger(this._element, EVENT_CHANGE, { strength: level, score }) + } + + dispose(): void { + this._input?.removeEventListener('input', this._onInput) + super.dispose() + } + + // Private + _getInput(): HTMLInputElement | null { + const { input } = this._config + if (input) { + return SelectorEngine.findOne(input) as HTMLInputElement | null + } + + // Without an explicit selector only the meter's own parent is searched, so + // a form with several password fields cannot bind the wrong one. + const parent = this._element.parentElement + const fields = parent ? (SelectorEngine.find(SELECTOR_PASSWORD, parent) as HTMLInputElement[]) : [] + return fields[fields.length - 1] ?? null + } + + _getText(): HTMLElement | null { + const parent = this._element.parentElement + return parent ? SelectorEngine.findOne(SELECTOR_TEXT, parent) : null + } + + _setUpAria(): void { + const element = this._element + element.setAttribute('role', 'progressbar') + element.setAttribute('aria-valuemin', '0') + element.setAttribute('aria-valuemax', String(LEVELS.length)) + element.setAttribute('aria-valuenow', '0') + + if (!element.hasAttribute('aria-label') && !element.hasAttribute('aria-labelledby')) { + element.setAttribute('aria-label', 'Password strength') + } + + // The segments are decoration; the level is read from the meter itself. + for (const segment of this._segments) { + segment.setAttribute('aria-hidden', 'true') + } + + this._text?.setAttribute('aria-live', 'polite') + } + + _score(password: string): number { + if (!password) { + return 0 + } + + const { scorer, weights, minLength } = this._config + if (typeof scorer === 'function') { + return scorer(password) + } + + const rules: [boolean, number | undefined][] = [ + [password.length >= minLength, weights.minLength], + [password.length >= minLength + 4, weights.extraLength], + [password.length >= 16, weights.longPassword], + [/[a-z]/.test(password), weights.lowercase], + [/[A-Z]/.test(password), weights.uppercase], + [/\d/.test(password), weights.numbers], + [/[^\dA-Za-z]/.test(password), weights.special], + [(password.match(/[^\dA-Za-z]/g) ?? []).length > 1, weights.multipleSpecial], + ] + + return rules.reduce((score, [passed, weight]) => score + (passed ? (weight ?? 0) : 0), 0) + } + + // Trailing underscore: `_level` is the field holding the current value. + _level_(score: number): StrengthLevel | null { + if (score <= 0) { + return null + } + + const index = this._config.thresholds.findIndex((threshold) => score <= threshold) + return LEVELS[index === -1 ? LEVELS.length - 1 : index] ?? null + } + + _render(level: StrengthLevel | null): void { + const element = this._element + const index = level ? LEVELS.indexOf(level) : -1 + + // Emptied rather than removed: `data-bs-strength` is also the attribute + // the data API looks for, and an empty value matches no level in CSS. + element.dataset.bsStrength = level ?? '' + + element.setAttribute('aria-valuenow', String(index + 1)) + // No level means no spoken value: `aria-valuenow` of 0 says it already, + // and an invented word here could not be translated through `messages`. + if (level) { + element.setAttribute('aria-valuetext', this._config.messages[level] ?? level) + } else { + element.removeAttribute('aria-valuetext') + } + + // The segments are filled in proportion, so a meter with three or five of + // them works as well as the usual four. + const filled = level ? Math.ceil(((index + 1) / LEVELS.length) * this._segments.length) : 0 + for (const [position, segment] of this._segments.entries()) { + segment.classList.toggle(CLASS_NAME_ACTIVE, position < filled) + } + + if (this._text) { + this._text.textContent = level ? (this._config.messages[level] ?? '') : this._emptyText + this._text.dataset.bsStrength = level ?? '' + } + } +} + +/** + * Data API implementation + */ + +// js-docs-start strength-init +for (const element of SelectorEngine.find(SELECTOR_DATA_STRENGTH)) { + Strength.getOrCreateInstance(element) +} +// js-docs-end strength-init + +export default Strength diff --git a/core/js/tabler.ts b/core/js/tabler.ts index 9717511c6..48ec34c79 100644 --- a/core/js/tabler.ts +++ b/core/js/tabler.ts @@ -10,6 +10,7 @@ import './src/tab' import './src/toast' import './src/sortable' import './src/sparkline' +import './src/strength' // Re-export everything from bootstrap.ts (single source of truth) export * from './src/bootstrap' @@ -20,4 +21,5 @@ export { default as CountUp } from './src/countup' export { default as InputMask } from './src/input-mask' export { default as Sortable } from './src/sortable' export { default as Sparkline } from './src/sparkline' +export { default as Strength } from './src/strength' export { default as SwitchIcon } from './src/switch-icon' diff --git a/core/js/tests/unit/strength.spec.ts b/core/js/tests/unit/strength.spec.ts new file mode 100644 index 000000000..8380d68f9 --- /dev/null +++ b/core/js/tests/unit/strength.spec.ts @@ -0,0 +1,200 @@ +import { describe, it, expect, beforeAll, beforeEach, afterEach, vi } from 'vitest' +import { clearFixture, getFixture } from '../helpers/fixture' +import Strength from '../../src/strength' + +const METER = ` +
+ +
+ + + + +
+ +
` + +describe('Strength', () => { + let fixtureEl: HTMLElement + + beforeAll(() => { + fixtureEl = getFixture() + }) + + beforeEach(() => { + fixtureEl.innerHTML = METER + }) + + afterEach(() => { + clearFixture() + }) + + const meter = (): HTMLElement => fixtureEl.querySelector('.strength')! + const input = (): HTMLInputElement => fixtureEl.querySelector('input')! + const text = (): HTMLElement => fixtureEl.querySelector('.strength-text')! + const filled = (): number => fixtureEl.querySelectorAll('.strength-segment.active').length + + const type = (value: string): void => { + input().value = value + input().dispatchEvent(new Event('input')) + } + + describe('NAME', () => { + it('should return plugin name', () => { + expect(Strength.NAME).toBe('strength') + }) + }) + + describe('constructor', () => { + it('should bind the password field in its own parent and start empty', () => { + const instance = new Strength(meter()) + + expect(instance._input).toBe(input()) + expect(instance.level).toBeNull() + expect(filled()).toBe(0) + expect(text().textContent).toBe('') + }) + + it('should read an existing value right away', () => { + input().value = 'Sup3rSecret!Pass' + new Strength(meter()) + + expect(meter().dataset.bsStrength).toBe('strong') + }) + + it('should take the field from the input option', () => { + fixtureEl.innerHTML = `
` + + const instance = new Strength(meter()) + + expect(instance._input).toBe(fixtureEl.querySelector('#remote')) + }) + }) + + describe('levels', () => { + it('should climb from weak to strong as the password improves', () => { + new Strength(meter()) + + type('abc') + expect(meter().dataset.bsStrength).toBe('weak') + expect(text().textContent).toBe('Weak') + expect(filled()).toBe(1) + + type('abcdefghijkl') + expect(meter().dataset.bsStrength).toBe('fair') + expect(filled()).toBe(2) + + type('Abcdefgh1!') + expect(meter().dataset.bsStrength).toBe('good') + expect(filled()).toBe(3) + + type('Abcdefgh1!?x') + expect(meter().dataset.bsStrength).toBe('strong') + expect(filled()).toBe(4) + }) + + it('should put the markup text back when the field is emptied', () => { + fixtureEl.innerHTML = METER.replace('', 'At least 8 characters') + + new Strength(meter()) + expect(text().textContent).toBe('At least 8 characters') + + type('abc') + expect(text().textContent).toBe('Weak') + + type('') + expect(text().textContent).toBe('At least 8 characters') + }) + + it('should keep the trigger attribute when the field is emptied', () => { + new Strength(meter()) + type('Abcdefgh1!?x') + type('') + + expect(meter().dataset.bsStrength).toBe('') + expect(text().textContent).toBe('') + expect(filled()).toBe(0) + }) + + it('should fire change.bs.strength once per level, without the password', () => { + const spy = vi.fn() + meter().addEventListener('change.bs.strength', spy) + new Strength(meter()) + + type('abc') + type('abcd') + + expect(spy).toHaveBeenCalledTimes(1) + const event = spy.mock.calls[0][0] as CustomEvent & { strength: string; score: number; password?: string } + expect(event.strength).toBe('weak') + expect(event.score).toBe(1) + expect(event.password).toBeUndefined() + }) + }) + + describe('config', () => { + it('should use a custom scorer and messages', () => { + new Strength(meter(), { scorer: () => 9, messages: { weak: 'Słabe', fair: 'Średnie', good: 'Dobre', strong: 'Mocne' } }) + + type('x') + + expect(meter().dataset.bsStrength).toBe('strong') + expect(text().textContent).toBe('Mocne') + }) + + it('should respect custom thresholds', () => { + new Strength(meter(), { thresholds: [10, 20, 30] }) + + type('Abcdefgh1!?x') + + expect(meter().dataset.bsStrength).toBe('weak') + }) + + it('should fill a meter with a different number of segments', () => { + fixtureEl.innerHTML = `
` + + new Strength(meter()) + type('abc') + + expect(filled()).toBe(1) + }) + }) + + describe('accessibility', () => { + it('should describe the meter and announce the label politely', () => { + new Strength(meter()) + + expect(meter().getAttribute('role')).toBe('progressbar') + expect(meter().getAttribute('aria-valuemax')).toBe('4') + expect(meter().getAttribute('aria-label')).toBe('Password strength') + expect(text().getAttribute('aria-live')).toBe('polite') + expect(fixtureEl.querySelector('.strength-segment')!.getAttribute('aria-hidden')).toBe('true') + + type('Abcdefgh1!?x') + expect(meter().getAttribute('aria-valuenow')).toBe('4') + expect(meter().getAttribute('aria-valuetext')).toBe('Strong') + + type('') + expect(meter().getAttribute('aria-valuenow')).toBe('0') + expect(meter().hasAttribute('aria-valuetext')).toBe(false) + }) + + it('should keep an aria-label from the markup', () => { + meter().setAttribute('aria-label', 'Siła hasła') + new Strength(meter()) + + expect(meter().getAttribute('aria-label')).toBe('Siła hasła') + }) + }) + + describe('dispose', () => { + it('should stop listening and remove the instance', () => { + const instance = new Strength(meter()) + instance.dispose() + type('Abcdefgh1!?x') + + expect(meter().dataset.bsStrength).toBe('') + expect(Strength.getInstance(meter())).toBeNull() + }) + }) +}) diff --git a/core/scss/_core.scss b/core/scss/_core.scss index 9111a85bd..c239acc9e 100644 --- a/core/scss/_core.scss +++ b/core/scss/_core.scss @@ -67,6 +67,7 @@ @forward 'ui/forms/form-custom'; @forward 'ui/forms/form-check'; @forward 'ui/forms/validation'; +@forward 'ui/forms/strength'; @forward 'ui/legend'; @forward 'ui/lists'; @forward 'ui/loaders'; diff --git a/core/scss/_variables.scss b/core/scss/_variables.scss index 7e891e2fd..d4b1a733f 100644 --- a/core/scss/_variables.scss +++ b/core/scss/_variables.scss @@ -1873,6 +1873,23 @@ $progressbg-padding-x: 0.5rem !default; $progressbg-value-padding-start: 2rem !default; $progress-separated-ring-width: 2px !default; $progress-steps-gap: 0.25rem !default; + +// Password strength +// scss-docs-start strength-variables +$strength-height: 0.25rem !default; +$strength-gap: 0.25rem !default; +$strength-bg: var(--border-color) !default; +$strength-border-radius: var(--border-radius-pill) !default; +$strength-text-font-size: $small-font-size !default; +$strength-margin-top: 0.5rem !default; +$strength-text-margin-top: 0.25rem !default; +$strength-levels: ( + 'weak': var(--danger), + 'fair': var(--warning), + 'good': var(--info), + 'strong': var(--success), +) !default; +// scss-docs-end strength-variables $progress-steps-item-height: 0.25rem !default; // scss-docs-end progress-variables diff --git a/core/scss/ui/forms/_strength.scss b/core/scss/ui/forms/_strength.scss new file mode 100644 index 000000000..65a5611e9 --- /dev/null +++ b/core/scss/ui/forms/_strength.scss @@ -0,0 +1,63 @@ +/* +Password strength meter + */ +@use '../../config' as *; + +.strength { + --strength-height: #{$strength-height}; + --strength-gap: #{$strength-gap}; + --strength-bg: #{$strength-bg}; + --strength-border-radius: #{$strength-border-radius}; + --strength-color: #{$strength-bg}; + --strength-margin-top: #{$strength-margin-top}; + + display: flex; + gap: var(--strength-gap); + width: 100%; + + // Spaced like a form hint, so the meter sits under the control it rates + // without a utility class at every call site. + .form-control + &, + .form-select + &, + .input-group + &, + .form-hint + & { + margin-top: var(--strength-margin-top); + } +} + +.strength-segment { + flex: 1; + height: var(--strength-height); + background: var(--strength-bg); + border-radius: var(--strength-border-radius); + transition: background-color 0.2s ease-in-out; + + &.active { + background: var(--strength-color); + } + + @media (prefers-reduced-motion: reduce) { + transition: none; + } +} + +.strength + .strength-text { + margin-top: #{$strength-text-margin-top}; +} + +.strength-text { + font-size: $strength-text-font-size; + color: var(--strength-color, var(--secondary)); + + // An empty label must not reserve a line before the user types. + &:empty { + display: none; + } +} + +@each $level, $color in $strength-levels { + .strength[data-bs-strength='#{$level}'], + .strength-text[data-bs-strength='#{$level}'] { + --strength-color: #{$color}; + } +} diff --git a/docs/content/ui/forms/strength.mdx b/docs/content/ui/forms/strength.mdx new file mode 100644 index 000000000..e279063d9 --- /dev/null +++ b/docs/content/ui/forms/strength.mdx @@ -0,0 +1,165 @@ +--- +title: Password strength +seoTitle: Password strength meter +order: 13 +summary: A strength meter rates the password as the user types and fills a segmented bar, so weak passwords are visible before the form is sent. +description: Show how strong a password is with a segmented meter that rates it as the user types, with configurable scoring and labels. +added-in: '1.6.0' +related: [/ui/forms/elements, /ui/forms/validation] +--- +import Example from '@components/Example.astro'; +import CodeDocs from '@components/CodeDocs.astro'; + +## Overview + +A password field says nothing about the password it holds. The strength meter rates what the user typed and fills a segmented bar, from weak to strong, with a short label under it. + +The meter is a hint for the user, not a validation. Check the password on the server as well, and never let the meter decide whether a form can be sent. + + +
+ + +
+ + + + +
+
+
+
+ +## Usage + +Write the meter as a `strength` element with one `strength-segment` per bar, and add `data-bs-strength`. Tabler finds it on page load and rates the password field that sits in the same parent element. Add a `strength-text` next to the meter for the label; it stays hidden until the user types. + +A meter that follows a control gets its own top margin, so it needs no margin utility. + +Text written inside `strength-text` shows while the field is empty and comes back when it is cleared, so the password rules can stand there until a level takes over. + + +
+ + +
+ + + + +
+
Use 8 to 20 characters, with letters and numbers.
+
+
+ +```html + +
+ + + + +
+
+``` + +### Pointing at a field + +A form with several password fields, or a meter that does not sit next to its input, needs `data-bs-input` with the selector of the field. + +```html +
…
+``` + +### Number of segments + +The segments are filled in proportion to the level, so a meter can have any number of them. Four segments give one per level; with three, two levels share the last segment. + + +
+ + +
+ + + +
+
+
+ +## Options + +Pass options as data attributes on the meter, or in the config object. + +| Option | Default | Description | +| --- | --- | --- | +| `input` | none | Selector of the password field. Without it the meter uses the password field in its own parent. | +| `minLength` | `8` | The length the built-in scoring counts as long enough. | +| `messages` | `Weak`, `Fair`, `Good`, `Strong` | The label for each level. | +| `weights` | `1` each | Points for each rule of the built-in scoring. | +| `thresholds` | `[2, 4, 6]` | Score bounds: weak up to the first, fair to the second, good to the third, strong above it. | +| `scorer` | none | A function that takes the password and returns a number, replacing the built-in scoring. Config object only. | + +The built-in scoring gives a point each for: reaching `minLength`, four characters more, sixteen characters, a lowercase letter, an uppercase letter, a digit, a special character, and a second special character. + +```html +
…
+``` + +## JavaScript + +Tabler ships a `Strength` component that works like the Bootstrap components: it is created once per element and stored on it. On page load Tabler creates one for every element with `data-bs-strength`. This is the code that runs: + + + +Create the component yourself for a form added later, or to pass options the attributes cannot carry: + +```js +const meter = tabler.Strength.getOrCreateInstance(document.getElementById('meter'), { + input: '#password-new', + scorer: (password) => zxcvbn(password).score * 2, +}); +``` + +Every change of level fires `change.bs.strength` on the meter. The event carries the level and the score, never the password: + +```js +document.getElementById('meter').addEventListener('change.bs.strength', (event) => { + console.log(event.strength, event.score); +}); +``` + +| Method | Description | +| --- | --- | +| `evaluate()` | Rates the field again, for example after the value was set from code. | +| `level` | Getter. The current level, or `null` when the field is empty. | +| `dispose()` | Stops listening and removes the component from the element. | +| `getInstance(element)` | Static. Returns the component for the element, or `null`. | +| `getOrCreateInstance(element, config)` | Static. Returns the component for the element and creates it when needed. | + +## Accessibility + +- **Name the meter.** Write an `aria-label` (or `aria-labelledby`) on it, as every example here does. Tabler falls back to the English `Password strength` only when the markup names nothing, so a page in another language should always carry its own label. +- The meter is a `progressbar`. Tabler keeps `aria-valuenow` on it and puts the current level in `aria-valuetext`. An empty field drops `aria-valuetext` and leaves the value at zero, so nothing untranslated is ever spoken. +- The segments are marked `aria-hidden`, so a screen reader reads the level once instead of counting bars. +- The label under the meter is announced politely, and only when the level changes, so typing does not flood a screen reader. +- Colour alone does not carry the result: the label says the same thing in words, and the number of filled segments repeats it. +- A meter says how strong the password is, not which rules a form enforces. Put the rules in `strength-text`, where they show until the first letter, or in a `form-hint` when they have to stay visible the whole time. + +## Translations + +Every word the meter shows comes from your markup or your config, so there is nothing to patch in Tabler to run it in another language. + +Set the level labels with `data-bs-messages` and name the meter with an `aria-label` in the same language: + +```html +
…
+``` + +The labels are also what a screen reader announces through `aria-valuetext`, so translating `messages` covers the spoken output as well. + +## Variables + +Use these SCSS variables to customize the meter. The default values are: + + diff --git a/preview/pages/password-strength.astro b/preview/pages/password-strength.astro new file mode 100644 index 000000000..853d74447 --- /dev/null +++ b/preview/pages/password-strength.astro @@ -0,0 +1,168 @@ +--- +import DefaultLayout from '@shared/layouts/DefaultLayout.astro' +import CaptureScript from '@shared/components/CaptureScript.astro' +import Card from '@ui/Card.astro' +import CardBody from '@ui/CardBody.astro' +import CardTitle from '@ui/CardTitle.astro' +import CardSubtitle from '@ui/CardSubtitle.astro' +import FormGroup from '@ui/FormGroup.astro' +import InputGroup from '@ui/InputGroup.astro' +import Strength from '@ui/Strength.astro' +import Badge from '@ui/Badge.astro' +import Button from '@ui/Button.astro' +import DocsLink from '@ui/DocsLink.astro' +--- + + + + +
+
+ + + Basic + Add data-bs-strength to a strength element next to the field. A strength-text shows the label. + + + + + + +
+ +
+ + + Rules until the first letter + Text written inside strength-text shows while the field is empty, so the password rules can stand there until a level takes over. + + + Use 8 to 20 characters, with letters and numbers. + + + +
+ +
+ + + Without a label + The strength-text is optional. Left out, the meter is the only feedback. + + + + + + +
+ +
+ + + Number of segments + Segments fill in proportion, so a meter can have three, four or more of them. + + + + + + + + + + +
+ +
+ + + Own labels + Pass data-bs-messages as JSON to rename the levels. + + + + + + +
+ +
+ + + Stricter scoring + Raise data-bs-min-length and data-bs-thresholds to demand more before a password counts as strong. + + + + + + +
+ +
+ + + Pointing at a field + With several password fields, name the one to rate with data-bs-input. + + + + + + + + + +
+ +
+ + + Reacting to the level + Every change of level fires change.bs.strength with the level and the score. + + + + +
+ +
+ + + + + +
+
+
+ +
+ + + Without a field + Without a password field in its parent and without data-bs-input, the meter stays empty. + + + +
+
+
diff --git a/screenshots/pages/password-strength.astro b/screenshots/pages/password-strength.astro new file mode 100644 index 000000000..e81f5d75e --- /dev/null +++ b/screenshots/pages/password-strength.astro @@ -0,0 +1,28 @@ +--- +// Password strength meter in its natural place: the password field of a sign-up +// card. The field carries a value, so tabler.js rates it on load and the meter +// shows a filled state with its label instead of an empty bar. +import ScreenshotLayout from '../layouts/ScreenshotLayout.astro' +import Card from '@ui/Card.astro' +import CardBody from '@ui/CardBody.astro' +import CardTitle from '@ui/CardTitle.astro' +import FormGroup from '@ui/FormGroup.astro' +import Strength from '@ui/Strength.astro' +import Button from '@ui/Button.astro' +--- + + + + + Create your account + + + + + + Use 8 to 20 characters, with letters and numbers. + +