Subframe
Components & interactionsPassword strength meter

Credential meter

Four quiet segments track which password requirements are satisfied.

Type a password and toggle its visibility.

Subframe Original · MIT

Add to your project

Component source

Standalone React component. Styles are included.

React only. No additional packages.

Render the default export inside a container with a defined height. Its optional active prop stops movement when false. This example works with React 19; Next.js projects can use it as a client component.

View source code
/**
 * MIT License
 * Copyright (c) 2026 Subframe
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

"use client"
import * as React from "react"
function usePreview(active: boolean) {
  const [reduced, setReduced] = React.useState(true),
    [instant, setInstant] = React.useState(false)
  React.useEffect(() => {
    const q = matchMedia("(prefers-reduced-motion: reduce)")
    const read = () => setReduced(q.matches)
    read()
    q.addEventListener("change", read)
    return () => q.removeEventListener("change", read)
  }, [])
  return {
    still: !active || reduced || instant,
    reduced,
    onKeyDownCapture: () => setInstant(true),
    onPointerDownCapture: () => setInstant(false),
  }
}

export default function Example({ active = true }: { active?: boolean }) {
  const preview = usePreview(active)

  const [value, setValue] = React.useState(""),
    [shown, setShown] = React.useState(false)
  const id = React.useId()
  const rules = [
    { label: "12 characters", met: value.length >= 12 },
    { label: "Mixed case", met: /[a-z]/.test(value) && /[A-Z]/.test(value) },
    { label: "A number", met: /\d/.test(value) },
    { label: "A symbol", met: /[^\w\s]/.test(value) },
  ]
  const count = rules.filter((r) => r.met).length

  return (
    <section
      className="sf-w2-credential-meter"
      data-still={preview.still}
      onKeyDownCapture={preview.onKeyDownCapture}
      onPointerDownCapture={preview.onPointerDownCapture}
    >
      <div className="vault">
        <label htmlFor={id}>New password</label>
        <div className="field">
          <input
            id={id}
            type={shown ? "text" : "password"}
            autoComplete="new-password"
            value={value}
            placeholder="At least 12 characters"
            onChange={(e) => setValue(e.target.value)}
            maxLength={80}
          />
          <button type="button" onClick={() => setShown(!shown)} aria-label={shown ? "Hide password" : "Show password"}>
            {shown ? "Hide" : "Show"}
          </button>
        </div>
        <div
          className="meter"
          role="meter"
          aria-label="Password requirements met"
          aria-valuemin={0}
          aria-valuemax={4}
          aria-valuenow={count}
        >
          {rules.map((r, i) => (
            <i key={r.label} data-met={i < count} />
          ))}
        </div>
        <div className="rules">
          {rules.map((r) => (
            <span key={r.label} data-met={r.met}>
              <b aria-hidden>{r.met ? "✓" : "·"}</b>
              {r.label}
            </span>
          ))}
        </div>
        <div className="foot" role="status">
          {count === 4 ? "All requirements met" : `${count} of 4 requirements met`}
        </div>
      </div>

      <style>{`.sf-w2-credential-meter{position:relative;isolation:isolate;container-type:inline-size;width:100%;height:100%;min-height:280px;overflow:hidden;box-sizing:border-box;font:13px/1.4 Arial,sans-serif;-webkit-font-smoothing:antialiased}.sf-w2-credential-meter *{box-sizing:border-box}.sf-w2-credential-meter button,.sf-w2-credential-meter input,.sf-w2-credential-meter select,.sf-w2-credential-meter textarea{font:inherit}.sf-w2-credential-meter button{cursor:pointer;touch-action:manipulation;min-height:44px;border:0;background:none;color:inherit}.sf-w2-credential-meter button:disabled{opacity:.4;cursor:default}.sf-w2-credential-meter button:active:not(:disabled){transform:scale(.98)}.sf-w2-credential-meter input,.sf-w2-credential-meter textarea{font-size:16px;min-width:0}.sf-w2-credential-meter button:focus-visible,.sf-w2-credential-meter input:focus-visible,.sf-w2-credential-meter select:focus-visible,.sf-w2-credential-meter textarea:focus-visible,.sf-w2-credential-meter [tabindex]:focus-visible{outline:2px solid #3376ef;outline-offset:3px}.sf-w2-credential-meter p{margin:0}.sf-w2-credential-meter .muted{opacity:.6}.sf-w2-credential-meter .sr{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}.sf-w2-credential-meter[data-still=true] *,.sf-w2-credential-meter[data-still=true] *::before,.sf-w2-credential-meter[data-still=true] *::after{animation:none!important;transition:none!important;scroll-behavior:auto!important}@media(prefers-reduced-motion:reduce){.sf-w2-credential-meter *,.sf-w2-credential-meter *::before,.sf-w2-credential-meter *::after{animation:none!important;transition:none!important;scroll-behavior:auto!important}}@media(hover:hover) and (pointer:fine){.sf-w2-credential-meter button:hover:not(:disabled){filter:brightness(.96)}}.sf-w2-credential-meter{background:#121514;color:#f0f3f1;display:grid;place-items:center;padding:24px}.sf-w2-credential-meter .vault{width:min(100%,364px)}.sf-w2-credential-meter .key{font-size:25px;color:#9db9ac}.sf-w2-credential-meter label{display:block;margin:0 0 7px;color:#bbc5bf;font-size:12px}.sf-w2-credential-meter .field{display:flex;border:1px solid #424b45;border-radius:8px;background:#1e2420;overflow:hidden}.sf-w2-credential-meter input{flex:1;width:100%;border:0;background:transparent;color:#f2f7f3;padding:12px}.sf-w2-credential-meter input::placeholder{font-size:13px;color:#858f88}.sf-w2-credential-meter .field button{font-size:11px;padding:0 14px;color:#cbd5ce}.sf-w2-credential-meter .meter{display:flex;gap:5px;margin:17px 0}.sf-w2-credential-meter .meter i{height:3px;flex:1;background:#364039;transition:background .18s}.sf-w2-credential-meter .meter i[data-met=true]{background:#89c8a7}.sf-w2-credential-meter .rules{display:grid;grid-template-columns:1fr 1fr;gap:10px;color:#87938b;font-size:12px}.sf-w2-credential-meter .rules span{display:flex;gap:7px;align-items:center;transition:color .18s}.sf-w2-credential-meter .rules span[data-met=true]{color:#c9e6d7}.sf-w2-credential-meter .rules b{display:grid;place-items:center;width:15px;font-size:13px}.sf-w2-credential-meter .foot{font-size:11px;color:#9aa99f;margin-top:20px;padding-top:14px;border-top:1px solid #303a33}`}</style>
    </section>
  )
}

Where it works well

Show the remaining requirements while a user chooses a credential.

Reduced motion

Requirement states update instantly; the meter has no animated travel.

Things to know

A local policy checklist, not a password strength estimate or authentication system.

More examples