---
title: "ViraStack Password: An Accessible Password Hook for React"
description: "Show / hide, NIST-style strength, confirmation, the same rules on the server and a leaked password check, in one headless React hook."
url: https://omergulcicek.com/blog/virastack-password-accessible-password-hook
author: "Ömer Gülçiçek"
published: 2026-08-02
language: en
tags: ["ViraStack","React"]
---

# ViraStack Password: An Accessible Password Hook for React

A "show / hide" button on a password field looks like a few lines of `useState`. Then the list grows: a screen reader has to hear the button's state, the password should hide on submit so a password manager saves the right value, and someone typing with Caps Lock on should be told. On top of that you want to rate the password, check the confirmation and run the same rules on the server.

**ViraStack Password** (`@virastack/password`) puts all of that into one headless React hook: it hands you props for the input and the button, and the markup stays yours.

## What it does

- **Accessible visibility:** Adds `aria-pressed` to the button, hides the password on submit and reports Caps Lock.
- **NIST-style strength:** The default policy asks for 15–64 characters and no composition rules; the level grows with length. It catches well-known passwords, repetition and personal information.
- **Confirmation:** `matchWith` checks that a second field repeats the first.
- **Same rules on the server:** `@virastack/password/server` runs the same policy in a Server Action, and can check Have I Been Pwned for leaked passwords.
- **Zero dependencies:** The hook is about 3 KB.

## Try it

The fields use the default policy. The lines under them are what the hook returns: the level, the error keys and whether the two fields match.

## Install

```bash
npm install @virastack/password
```

## Usage

For a plain show / hide toggle, call the hook with no arguments:

```tsx title="password-field.tsx"
import { useViraPassword } from "@virastack/password"

export function PasswordField() {
  const { inputProps, btnProps } = useViraPassword()

  return (
    <div className="relative">
      <input {...inputProps} placeholder="Your password" />
      <button {...btnProps} />
    </div>
  )
}
```

In a sign-up form, bind it to React Hook Form, one entry per field:

```tsx title="signup-form.tsx"
import { definePolicy } from "@virastack/password/strength"

const policy = definePolicy() // NIST: 15–64 characters

const { password, confirm } = useViraPassword(form, {
  password: { policy, forbidden: ["email"] },
  confirm: { matchWith: "password" }
})

<input {...password.inputProps} />
<p {...password.strengthProps}>{password.strength?.level}</p>
<input {...confirm.inputProps} />
```

## How strength is measured

Length sets the level, not character classes. Real results with the default policy:

```ts
import { definePolicy, evaluate } from "@virastack/password/strength"

const policy = definePolicy()

evaluate("Summer2026!", policy)
// { level: "weak", valid: false, issues: ["password.tooShort", "password.common"] }

evaluate("correct horse battery staple", policy)
// { level: "weak", valid: false, issues: ["password.common"] }

evaluate("cold coffee on a quiet morning", policy)
// { level: "strong", valid: true, issues: [] }
```

"Summer2026!" has every character class and is still weak: short, and a pattern everyone uses. The famous "correct horse battery staple" is long enough but too well known. A long lowercase phrase you can remember is strong, which is NIST's point.

Server validation, the leaked password check, every option and the error keys are in the [virastack.com/password](https://virastack.com/password) docs, and the source is in [github.com/virastack/form](https://github.com/virastack/form/tree/main/packages/password). For masked fields in the same form, see [ViraStack Mask](https://omergulcicek.com/blog/virastack-mask-react-hook-form-input-masks).
