# PasswordInput

> Password field built on InputGroup with a show/hide toggle (aria-pressed), an optional strength meter and an optional requirement checklist.

Source: https://docs.nasaqui.com/components/password-input

## Install

```bash
npx shadcn@latest add https://docs.nasaqui.com/r/password-input.json
```

A password field with an eye button that shows or hides the text, and an optional strength meter under it.
It is an [`InputGroup`](https://docs.nasaqui.com/components/input-group), so height, border, focus ring and invalid state match every
other field, and it works inside [`Field`](https://docs.nasaqui.com/components/field) for the label, description and error.

## When to use

- Sign in, sign up, change password, and any secret the person types and may want to check.
- Set `showStrength` when the person is choosing a new password.

## When not to use

- One-time codes: use [`OtpInput`](https://docs.nasaqui.com/components/otp-input).
- API keys or tokens you display read-only: use `CopyField` from [`CopyButton`](https://docs.nasaqui.com/components/copy-button).
- Plain text: use `Input` from [`Field`](https://docs.nasaqui.com/components/field).

## Import

```tsx
import { PasswordInput } from "@fadymondy/nasaq/web";
// inside this monorepo: "@nasaq/web"
```

## Quick start

```tsx
import { Field, FieldLabel, PasswordInput } from "@fadymondy/nasaq/web";

export function SignInPassword() {
  return (
    <Field>
      <FieldLabel>Password</FieldLabel>
      <PasswordInput autoComplete="current-password" />
    </Field>
  );
}
```

## Anatomy

```
PasswordInput                     data-slot="password-input"
├─ InputGroup
│  ├─ InputGroupInput             type="password" | "text"
│  └─ InputGroupAddon (end)
│     └─ Button                   data-slot="password-input-toggle", aria-pressed
├─ strength (only with showStrength)   data-slot="password-input-strength", data-score="0".."4"
│  ├─ Meter                       data-slot="meter"
│  └─ level word                  aria-live="polite"
└─ ul (only with rules)           data-slot="password-input-rules", aria-label="Password requirements"
   └─ li × n                      data-met when met, Check or Minus icon, sr-only "met" / "not met"
```

## API

**PasswordInput**: every `InputGroupInput` prop except `type` (so `autoComplete`, `name`, `value`, `defaultValue`,
`onChange`, `disabled`, `required`, `ltr` and `ref` all pass through to the `<input>`), plus:

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `visible` | `boolean` | | Controlled visibility. |
| `defaultVisible` | `boolean` | `false` | Initial visibility when uncontrolled. |
| `onVisibleChange` | `(visible: boolean) => void` | | Called when the toggle is pressed. |
| `toggleLabel` | `string` | "Show password" / "إظهار كلمة المرور" | Accessible name of the toggle. It does not change with state; `aria-pressed` does. |
| `showStrength` | `boolean` | `false` | Show the strength meter and level word. |
| `score` | `number` | estimator | Strength from 0 to 4. Rounded and clamped. Omit to use `estimatePasswordStrength`. |
| `strengthLabel` | `string` | "Password strength" / "قوة كلمة المرور" | Name of the meter. |
| `strengthLevels` | `[string, string, string, string, string]` | English or Arabic words | Words for scores 0 to 4. |
| `rules` | `boolean \| PasswordPolicy \| readonly PasswordRule[]` | | Show a requirement checklist. `true` is 12 characters plus upper, lower, digit and symbol; a policy changes that; an array is shown as is. |
| `ruleLabels` | `Record<string, string>` | English or Arabic | Text per rule id. Needed for custom ids. |
| `className` | `string` | | Class for the outer wrapper. |
| `inputClassName` | `string` | | Class for the `<input>`. |

**estimatePasswordStrength(password: string): PasswordScore**: a small estimate from length and character classes.
Returns 0 to 4. `PasswordScore` is `0 | 1 | 2 | 3 | 4`. It is a hint, not a policy.

| Score | Rule |
| --- | --- |
| 0 | Empty, shorter than 6, or three or fewer distinct characters |
| 1 | 6 or more characters |
| 2 | 8 or more characters with two classes |
| 3 | 10 or more characters with three classes |
| 4 | 12 or more with four classes, or 16 or more with three |

Classes are lower case, upper case, digit, symbol, and uncased letters such as Arabic.

**computePasswordRules(password, policy?): PasswordRule[]**: the `length` rule (with `min`) then one rule per class in
`policy.require` (default upper, lower, digit, symbol). `PasswordPolicy` is `{ minLength?: number; require?: ("upper" |
"lower" | "digit" | "symbol")[] }`, minimum 12 by default. `PasswordRule` is `{ id; met: boolean; min?: number }`.

**computeRuleScore(rules): PasswordScore**: 0 until `length` passes, then 1 plus one per other rule met, capped at 4.
Pass it as `score` so the meter follows your policy instead of the estimate.

**passwordMeetsPolicy(rules): boolean**: true when every rule is met. Use it to block submit.

## Examples

**Sign up with the estimator**

```tsx
import { Field, FieldDescription, FieldLabel, PasswordInput } from "@fadymondy/nasaq/web";

export function NewPassword() {
  return (
    <Field>
      <FieldLabel>New password</FieldLabel>
      <PasswordInput autoComplete="new-password" showStrength />
      <FieldDescription>At least 8 characters.</FieldDescription>
    </Field>
  );
}
```

**Your own estimator**

```tsx
import { PasswordInput } from "@fadymondy/nasaq/web";
import { useState } from "react";

// zxcvbn(value).score is already 0 to 4.
declare function zxcvbnScore(value: string): number;

export function Custom() {
  const [value, setValue] = useState("");
  return <PasswordInput showStrength value={value} score={zxcvbnScore(value)} onChange={(e) => setValue(e.target.value)} />;
}
```

**A policy checklist**

```tsx
import { PasswordInput, computePasswordRules, computeRuleScore, passwordMeetsPolicy } from "@fadymondy/nasaq/web";
import { useState } from "react";

export function Policy() {
  const [value, setValue] = useState("");
  const rules = computePasswordRules(value, { minLength: 10 });
  return (
    <form onSubmit={(e) => passwordMeetsPolicy(rules) || e.preventDefault()}>
      <PasswordInput autoComplete="new-password" showStrength score={computeRuleScore(rules)} rules={rules} value={value} onChange={(e) => setValue(e.target.value)} />
    </form>
  );
}
```

**Arabic**

```tsx
import { Field, FieldLabel, PasswordInput } from "@fadymondy/nasaq/web";

export function ArabicPassword() {
  return (
    <Field>
      <FieldLabel>كلمة مرور جديدة</FieldLabel>
      <PasswordInput autoComplete="new-password" showStrength placeholder="أدخل كلمة المرور" />
    </Field>
  );
}
```

## Accessibility

| Key | Action |
| --- | --- |
| Tab | Moves from the input to the toggle. |
| Enter / Space on the toggle | Shows or hides the password. |

- The toggle is a `button` with a constant `aria-label` and `aria-pressed` for the state, so a screen reader says
  "Show password, toggle button, pressed" and never a contradicting label.
- The meter is `role="meter"` with `aria-valuenow` 0 to 4 and an `aria-label`. The level word sits in an
  `aria-live="polite"` region, so it is spoken when it changes.
- Caller must localise: `toggleLabel`, `strengthLabel` and `strengthLevels` when not using English or Arabic.
- The checklist is a labelled list. Each item ends with a visually hidden ", met" or ", not met", so the state is not
  carried by the tick or the colour alone. It is not live: re-reading on every key would be noisy. Show the unmet rule
  in the field error on submit.
- Set `autoComplete` so password managers work: `current-password` to sign in, `new-password` to create one.
- The input sets `autoCapitalize="none"`, `autoCorrect="off"` and `spellCheck={false}`.

## RTL & i18n

- The toggle sits at the inline end: the right in English, the left in Arabic. The meter fills from the inline start.
- The value keeps the input's natural direction and is not mirrored. Pass `ltr` to pin the typed value to
  left-to-right in an Arabic form, which keeps symbols in order.
- Built-in English and Arabic strings follow the Nasaq locale.
- The estimator counts code points, so Arabic letters and emoji are one character each.

## Styling & tokens

- Border, radius, focus ring: `--nq-*` tokens through `InputGroup`. Meter fill uses the danger, warning, info and success tokens.
- Target `[data-slot="password-input-strength"][data-score="3"]` to style by score.
- `aria-invalid` turns the border to the danger colour. Extend with `className` and `inputClassName`.

## Do / Don't

- Do set `autoComplete` on every password field.
- Do use the meter only when creating a password.
- Don't rely on the estimator as a security rule: enforce the policy on the server.
- Don't put a strength meter on a sign-in form.

## Related

- [`InputGroup`](https://docs.nasaqui.com/components/input-group)
- [`Field`](https://docs.nasaqui.com/components/field)
- [`Progress`](https://docs.nasaqui.com/components/progress) (Meter)
- [`OtpInput`](https://docs.nasaqui.com/components/otp-input)

## Lab

https://docs.nasaqui.com/?path=/docs/components-forms-password-input--docs

## Code

### React

```tsx
import { Field, FieldLabel, PasswordInput } from "@fadymondy/nasaq/web";

export function SignInPassword() {
  return (
    <Field>
      <FieldLabel>Password</FieldLabel>
      <PasswordInput autoComplete="current-password" />
    </Field>
  );
}
```

### shadcn

```tsx
import { Field, FieldLabel } from "@/components/ui/field";
import { PasswordInput } from "@/components/ui/password-input";

export function SignInPassword() {
  return (
    <Field>
      <FieldLabel>Password</FieldLabel>
      <PasswordInput autoComplete="current-password" />
    </Field>
  );
}
```

### Vue

```vue
<script setup lang="ts">
import { NqField, NqFieldLabel, NqPasswordInput } from "@fadymondy/nasaq/vue";
</script>

<template>
  <NqField>
    <NqFieldLabel>Password</NqFieldLabel>
    <NqPasswordInput autocomplete="current-password" />
  </NqField>
</template>
```

### Blade

```blade
<div class="flex w-full max-w-sm flex-col gap-1.5">
    <label for="new-password" class="text-label text-foreground">New password</label>
    <x-nq::password-input id="new-password" name="password" autocomplete="new-password" show-strength :rules="true" />
</div>
```

### HTML + Alpine

```html
<div class="flex w-full max-w-sm flex-col gap-1.5">
    <label for="new-password" class="text-label text-foreground">New password</label>
    <div data-slot="password-input" x-data="nqPasswordInput('', JSON.parse('{\u0022visible\u0022:false,\u0022score\u0022:null,\u0022rules\u0022:{\u0022minLength\u0022:12,\u0022require\u0022:[\u0022upper\u0022,\u0022lower\u0022,\u0022digit\u0022,\u0022symbol\u0022]},\u0022levels\u0022:[\u0022Very weak\u0022,\u0022Weak\u0022,\u0022Fair\u0022,\u0022Good\u0022,\u0022Strong\u0022]}'))" x-modelable="value"
    class="flex w-full flex-col gap-2">
    <div role="group" data-slot="input-group"
    class="group/input-group flex h-control min-h-[var(--nq-touch-min,0px)] w-full min-w-0 items-center overflow-hidden rounded-control border border-input bg-card text-body text-foreground transition-colors duration-150 ease-nq focus-within:border-nq-focus focus-within:outline-1 focus-within:outline-nq-focus has-[[data-invalid]]:border-nq-danger has-[[aria-invalid=true]]:border-nq-danger has-[input:disabled]:cursor-not-allowed has-[input:disabled]:opacity-50"><input data-slot="input-group-input"     x-model="value" x-bind:type="shown ? &#039;text&#039; : &#039;password&#039;" type="password" value="" autocapitalize="none" autocorrect="off" spellcheck="false" id="new-password" name="password" autocomplete="new-password" class="h-full min-w-0 flex-1 border-0 bg-transparent px-3 text-body text-foreground outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed pointer-coarse:text-[16px]" />
        <div data-slot="input-group-addon" data-align="end"
    class="flex h-full shrink-0 items-center gap-1.5 text-body-sm text-muted-foreground [&_svg]:size-4 order-last ps-1 pe-1.5"><button data-slot="password-input-toggle"
     type="button"                         x-bind="toggle" aria-label="Show password" aria-pressed="false" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border border-transparent font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 text-foreground hover:bg-nq-hover size-control-sm p-0">
        <svg x-show="shown" x-cloak="1" style="display: none" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49"/>
  <path d="M14.084 14.158a3 3 0 0 1-4.242-4.242"/>
  <path d="M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143"/>
  <path d="m2 2 20 20"/>
</svg>                <svg x-show="!shown" x-cloak="1" style="" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0"/>
  <circle cx="12" cy="12" r="3"/>
</svg></button></div></div>
            <div data-slot="password-input-strength" data-score="0" class="flex flex-col gap-1 opacity-60" x-bind="strengthBox">
            <div data-slot="meter" role="meter" aria-label="Password strength" aria-valuemin="0" aria-valuemax="4" aria-valuenow="0"
                data-tone="danger" x-bind="meter" class="flex w-full flex-col gap-1.5">
                <div data-slot="meter-track" class="relative block h-1 w-full overflow-hidden rounded-full bg-nq-surface-soft">
                    <div data-slot="meter-indicator" style="inset-inline-start:0;width:0%" x-bind="meterFill"
                        class="block h-full rounded-full transition-[width] duration-300 ease-nq motion-reduce:transition-none bg-nq-danger"></div>
                </div>
            </div>
            <div class="flex items-baseline justify-between gap-3 text-caption text-muted-foreground">
                <span>Password strength</span>
                <span aria-live="polite" class="text-foreground" x-text="levelText()"></span>
            </div>
        </div>
                <ul data-slot="password-input-rules" aria-label="Password requirements" class="grid gap-1 sm:grid-cols-2">
                            <li  x-bind="rule('length')"
                    class="flex items-center gap-1.5 text-caption transition-colors duration-150 ease-nq text-muted-foreground">
                    <svg x-show="met('length')" x-cloak="1" style="display: none" aria-hidden="true" class="size-3.5 shrink-0" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M20 6 9 17l-5-5"/>
</svg>                    <svg x-show="!met('length')" x-cloak="1" style="" aria-hidden="true" class="size-3.5 shrink-0" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M5 12h14"/>
</svg>                    <span>At least 12 characters</span>
                    <span class="sr-only" x-text="met('length') ? ', met' : ', not met'">, not met</span>
                </li>
                            <li  x-bind="rule('upper')"
                    class="flex items-center gap-1.5 text-caption transition-colors duration-150 ease-nq text-muted-foreground">
                    <svg x-show="met('upper')" x-cloak="1" style="display: none" aria-hidden="true" class="size-3.5 shrink-0" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M20 6 9 17l-5-5"/>
</svg>                    <svg x-show="!met('upper')" x-cloak="1" style="" aria-hidden="true" class="size-3.5 shrink-0" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M5 12h14"/>
</svg>                    <span>An uppercase letter</span>
                    <span class="sr-only" x-text="met('upper') ? ', met' : ', not met'">, not met</span>
                </li>
                            <li  x-bind="rule('lower')"
                    class="flex items-center gap-1.5 text-caption transition-colors duration-150 ease-nq text-muted-foreground">
                    <svg x-show="met('lower')" x-cloak="1" style="display: none" aria-hidden="true" class="size-3.5 shrink-0" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M20 6 9 17l-5-5"/>
</svg>                    <svg x-show="!met('lower')" x-cloak="1" style="" aria-hidden="true" class="size-3.5 shrink-0" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M5 12h14"/>
</svg>                    <span>A lowercase letter</span>
                    <span class="sr-only" x-text="met('lower') ? ', met' : ', not met'">, not met</span>
                </li>
                            <li  x-bind="rule('digit')"
                    class="flex items-center gap-1.5 text-caption transition-colors duration-150 ease-nq text-muted-foreground">
                    <svg x-show="met('digit')" x-cloak="1" style="display: none" aria-hidden="true" class="size-3.5 shrink-0" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M20 6 9 17l-5-5"/>
</svg>                    <svg x-show="!met('digit')" x-cloak="1" style="" aria-hidden="true" class="size-3.5 shrink-0" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M5 12h14"/>
</svg>                    <span>A number</span>
                    <span class="sr-only" x-text="met('digit') ? ', met' : ', not met'">, not met</span>
                </li>
                            <li  x-bind="rule('symbol')"
                    class="flex items-center gap-1.5 text-caption transition-colors duration-150 ease-nq text-muted-foreground">
                    <svg x-show="met('symbol')" x-cloak="1" style="display: none" aria-hidden="true" class="size-3.5 shrink-0" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M20 6 9 17l-5-5"/>
</svg>                    <svg x-show="!met('symbol')" x-cloak="1" style="" aria-hidden="true" class="size-3.5 shrink-0" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M5 12h14"/>
</svg>                    <span>A symbol</span>
                    <span class="sr-only" x-text="met('symbol') ? ', met' : ', not met'">, not met</span>
                </li>
                    </ul>
    </div>
</div>
```
