bpdm/ui
Form

Password Input

Password field with a show/hide toggle and an optional strength meter — sizes and states, in React (@bpdm/ui) and Angular (@bpdm/ng).

The Password Input is a password field with a built-in show/hide toggle and an optional strength meter (a segmented bar plus a label — Weak / Fair / Good / Strong). It renders a real type="password" input, so browser and third-party password managers work as usual. It runs controlled (value + onValueChange) or uncontrolled (defaultValue). In React it's <PasswordInput>; in Angular it's <bpdm-password-input>.

Usage

Drop it in with a placeholder. The eye button toggles visibility; the meter appears once there's input.

sign-up.tsx
import { PasswordInput } from '@bpdm/ui/password-input';

export function SignUp() {
  return <PasswordInput placeholder="Password" />;
}

For a controlled field, pass value and onValueChange:

controlled.tsx
import { useState } from 'react';
import { PasswordInput } from '@bpdm/ui/password-input';

export function Controlled() {
  const [value, setValue] = useState('');
  return <PasswordInput value={value} onValueChange={setValue} placeholder="Password" />;
}
sign-up.component.ts
import { Component } from '@angular/core';
import { BpdmPasswordInput } from '@bpdm/ng';

@Component({
  selector: 'app-sign-up',
  standalone: true,
  imports: [BpdmPasswordInput],
  template: `<bpdm-password-input placeholder="Password" />`,
})
export class SignUpComponent {}

For a controlled field, use two-way binding on value:

controlled.component.ts
import { Component } from '@angular/core';
import { BpdmPasswordInput } from '@bpdm/ng';

@Component({
  selector: 'app-controlled',
  standalone: true,
  imports: [BpdmPasswordInput],
  template: `<bpdm-password-input [(value)]="value" placeholder="Password" />`,
})
export class ControlledComponent {
  value = '';
}

Strength meter

As the value changes, the meter fills based on length and character variety (lower-case, upper-case, digits, symbols) and shows a label for the current level. It's live-announced to screen readers.

strength.tsx
import { PasswordInput } from '@bpdm/ui/password-input';

export function Strength() {
  return <PasswordInput placeholder="Create a password" />;
}
strength.component.ts
import { Component } from '@angular/core';
import { BpdmPasswordInput } from '@bpdm/ng';

@Component({
  selector: 'app-strength',
  standalone: true,
  imports: [BpdmPasswordInput],
  template: `<bpdm-password-input placeholder="Create a password" />`,
})
export class StrengthComponent {}

Levels & labels

levels sets the number of segments; labels overrides the per-level text (its length should match levels). Sensible defaults are provided for 3, 4, and 5 levels. For a custom rule, pass a strength scorer that returns a number from 0 to levels.

Strong

High

levels.tsx
import { PasswordInput } from '@bpdm/ui/password-input';

export function Levels() {
  return (
    <div className="flex w-72 flex-col gap-6">
      <PasswordInput levels={3} placeholder="3 levels" />
      <PasswordInput levels={5} placeholder="5 levels" />
      <PasswordInput levels={3} labels={['Low', 'Mid', 'High']} placeholder="custom labels" />
    </div>
  );
}
levels.component.ts
import { Component } from '@angular/core';
import { BpdmPasswordInput } from '@bpdm/ng';

@Component({
  selector: 'app-levels',
  standalone: true,
  imports: [BpdmPasswordInput],
  template: `
    <div class="flex w-72 flex-col gap-6">
      <bpdm-password-input [levels]="3" placeholder="3 levels" />
      <bpdm-password-input [levels]="5" placeholder="5 levels" />
      <bpdm-password-input [levels]="3" [labels]="['Low', 'Mid', 'High']" placeholder="custom labels" />
    </div>
  `,
})
export class LevelsComponent {}

Hiding the meter

The meter suits sign-up and change-password flows. On a sign-in form, where strength guidance is noise, turn it off with feedback={false}.

sign-in.tsx
import { PasswordInput } from '@bpdm/ui/password-input';

export function SignIn() {
  return <PasswordInput feedback={false} placeholder="Password" />;
}
sign-in.component.ts
import { Component } from '@angular/core';
import { BpdmPasswordInput } from '@bpdm/ng';

@Component({
  selector: 'app-sign-in',
  standalone: true,
  imports: [BpdmPasswordInput],
  template: `<bpdm-password-input [feedback]="false" placeholder="Password" />`,
})
export class SignInComponent {}

Sizes

Three heights — sm, md (default), and lg — to line up with the other fields in a form.

sizes.tsx
import { PasswordInput } from '@bpdm/ui/password-input';

export function Sizes() {
  return (
    <div className="flex w-72 flex-col gap-3">
      {(['sm', 'md', 'lg'] as const).map((s) => (
        <PasswordInput key={s} size={s} feedback={false} placeholder={`Size ${s}`} />
      ))}
    </div>
  );
}
sizes.component.ts
import { Component } from '@angular/core';
import { BpdmPasswordInput } from '@bpdm/ng';

@Component({
  selector: 'app-sizes',
  standalone: true,
  imports: [BpdmPasswordInput],
  template: `
    <div class="flex w-72 flex-col gap-3">
      @for (s of sizes; track s) {
        <bpdm-password-input [size]="s" [feedback]="false" [placeholder]="'Size ' + s" />
      }
    </div>
  `,
})
export class SizesComponent {
  sizes = ['sm', 'md', 'lg'] as const;
}

States

Disable the field, or mark it invalid with aria-invalid and pair it with a described error message.

Password is required.

states.tsx
import { PasswordInput } from '@bpdm/ui/password-input';

export function States() {
  return (
    <div className="flex w-72 flex-col gap-4">
      <PasswordInput disabled feedback={false} placeholder="Disabled" />
      <div className="flex flex-col gap-1.5">
        <PasswordInput aria-invalid aria-describedby="pw-err" feedback={false} placeholder="Password" />
        <p id="pw-err" className="text-sm text-destructive">
          Password is required.
        </p>
      </div>
    </div>
  );
}
states.component.ts
import { Component } from '@angular/core';
import { BpdmPasswordInput } from '@bpdm/ng';

@Component({
  selector: 'app-states',
  standalone: true,
  imports: [BpdmPasswordInput],
  template: `
    <div class="flex w-72 flex-col gap-4">
      <bpdm-password-input [disabled]="true" [feedback]="false" placeholder="Disabled" />
      <div class="flex flex-col gap-1.5">
        <bpdm-password-input aria-invalid aria-describedby="pw-err" [feedback]="false" placeholder="Password" />
        <p id="pw-err" class="text-sm text-destructive">Password is required.</p>
      </div>
    </div>
  `,
})
export class StatesComponent {}

API

PasswordInput / bpdm-password-input

PropTypeDefaultDescription
valuestringControlled value. React: pair with onValueChange. Angular: two-way [(value)].
defaultValuestring""Uncontrolled initial value.
onValueChange (React)(value: string) => voidCalled with the value on every change.
feedbackbooleantrueShow the strength meter below the field.
levelsnumber4Number of strength segments.
strength(value: string) => numberCustom scorer returning 0..levels (defaults to a length + variety heuristic).
labelsstring[]Per-level labels (length = levels); defaults provided for 3 / 4 / 5.
sizesm | md | lgmdHeight + text scale.
disabledbooleanfalseDisable the field and the toggle.
namestringNative name for form submission (forwarded to the <input>).
autoCompletestringSet current-password for sign-in, new-password for sign-up.
aria-invalidbooleanfalseInvalid state — destructive border, announced by assistive tech.
aria-label / aria-describedbystringAccessible name / description, forwarded to the <input>.
messages{ show, hide }EnglishOverride the reveal-toggle labels (screen-reader text) — see Internationalization.
idstringField id (for <label> association).
className (React) / class (Angular)stringExtra classes on the outer wrapper.

In React, PasswordInput forwards its ref to the underlying <input> and spreads any other native <input> prop (required, maxLength, data-*, onBlur, …) onto it, so it drops into forms and test harnesses unchanged. In Angular, name, aria-label, and aria-describedby are dedicated inputs on <bpdm-password-input>.

Accessibility

  • The show/hide control is a real <button type="button"> with an aria-label (Show password / Hide password) and aria-pressed reflecting the current state — operable by keyboard and announced by screen readers. The eye glyph is aria-hidden.
  • Revealing switches the field between type="password" and type="text"; either way it's a standard input, so browser and third-party password managers work.
  • The strength bar is decorative (aria-hidden). Its label is aria-live="polite" — so changes are announced — and linked to the field via aria-describedby, so the current strength is read as the field's description (any aria-describedby you pass is preserved and merged).
  • Invalid state is set with aria-invalid — it colours the border and is announced (never colour-only). Pair it with aria-describedby pointing at your error text.
  • Give the field a name — associate a <label> (via htmlFor/id) or add aria-label.

Internationalization

  • The reveal-toggle labels (Show password / Hide password) are translatable via the messages prop, and the strength-meter labels via labels — see Translating labels. Give the field a translated accessible name.
  • RTL-safe: no physical layout (the toggle sits via flex), so the field mirrors under dir="rtl".

On this page