本文へスキップ
HELLO!
HIGASHIYUKI®.COM
0   /   100
フォーム

パスワードの入力に合わせて強度メーターが伸び、色が変わる

デモ

操作:クリック。 「さわってみる」を押すと自動再生が止まり、マウスで動かせます。

仕様

用途
フォーム
動作
DOM
種類
レシピ
使うもの
DrawSVGPlugin
動作確認
three 0.186.0・gsap 3.15.0
公開日

しくみと調整

打つたびに、長さと満たした条件の数からパスワードの強さを 0〜1 で出し、メーターをその長さまで伸ばします。強さが 0.34・0.62・0.9 を越えると、段階が「弱い → 普通 → 強い → とても強い」と上がり、帯の色が赤 → 橙 → 黄 → 緑に変わって、右の言葉が入れ替わります。満たした条件(8文字以上・大文字・数字・記号)は丸が緑に塗られてチェックが描かれ、「とても強い」になると「次へ進む」のボタンが押せる色になります。

強さは、長さで最大 0.26(12 字まで)、条件 1 つごとに 0.17 を足して出します。長さだけでは「普通」にも届かず、条件を満たすほど大きく伸びるので、何をすれば強くなるかが帯の伸び方で分かります。メーターは打つたびに 0.35 秒の power3.out で、今の長さから新しい長さへ向かい直します。チェックの線は DrawSVGPlugin で 0% から描きます。

日本語入力が効いているときは、変換の途中にも input が届き、未確定のひらがなで強さが揺れます。isComposing が true の間は数えず、compositionend で確定してから数えます。Enter は keydown では受けず、form の送信で受けます。keydown で受けると、変換を確定する Enter でも送ってしまうためです。記号は半角の記号だけを数え、全角の文字や空白は記号と数えません。

段階の色は意味のある色として持ち、欄の枠やボタンは無彩色にします。右の言葉は小さな文字なので、帯と同じ色では白い地に対して薄く、同じ色みで一段濃い色にしています。色だけに頼らないよう、段階の名前は読み上げ用の領域にも書き、満たした条件には「満たしています」を添えます。押せない間のボタンは disabled ではなく aria-disabled にして、焦点を受けられるようにします。

つまみは、長さと条件の配分、段階のしきい値と色、メーターの伸びる時間です。しきい値は HTML の段階の言葉に付けた値で変えられます。右の目のボタンで文字を見せられるようにしておくと、打ち間違いに気づきやすくなります。会員登録、パスワードの変更や再設定のフォームに向いています。

コード

HTML
<form class="password" action="/signup/step3/" method="post">
  <h2 class="password__title">パスワードの設定</h2>
  <p class="password__lead">英字・数字・記号を組み合わせて、8文字以上で設定してください。</p>
  <label class="password__label" for="new-password">パスワード</label>
  <div class="password__field">
    <input class="password__input" id="new-password" name="password" type="password" autocomplete="new-password"
           minlength="8" required spellcheck="false" aria-describedby="password-rules">
    <!-- 押している間(aria-pressed が true)は文字を見せる。名前は変えない -->
    <button class="password__toggle" type="button" aria-pressed="false" aria-label="パスワードを表示">
      <svg viewBox="0 0 24 24" aria-hidden="true">
        <path d="M2.5 12s3.5-6.5 9.5-6.5 9.5 6.5 9.5 6.5-3.5 6.5-9.5 6.5S2.5 12 2.5 12z" />
        <circle cx="12" cy="12" r="3" />
        <path class="password__slash" d="M4 4l16 16" />
      </svg>
    </button>
  </div>
  <!-- 段階は data-min(強さ 0〜1 のしきい値)の小さい順に並べる。帯と言葉の色は style.css -->
  <div class="strength" data-level="-1">
    <div class="strength__track"><span class="strength__bar"></span></div>
    <p class="strength__caption">
      パスワードの強さ
      <span class="strength__words" aria-hidden="true">
        <span class="strength__word" data-min="0">弱い</span>
        <span class="strength__word" data-min="0.34">普通</span>
        <span class="strength__word" data-min="0.62">強い</span>
        <span class="strength__word" data-min="0.9">とても強い</span>
      </span>
    </p>
    <p class="password__hidden" role="status" data-label="パスワードの強さ:"></p>
  </div>
  <!-- data-rule は script.js の条件の名前。満たした条件には、読み上げ用に data-ok の文が付く -->
  <ul class="rules" id="password-rules" aria-label="パスワードの条件" data-ok="(満たしています)">
    <li class="rule" data-rule="length"><span class="rule__dot" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M7 12.4l3.3 3.3 6.7-7" /></svg></span>8文字以上<span class="password__hidden"></span></li>
    <li class="rule" data-rule="upper"><span class="rule__dot" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M7 12.4l3.3 3.3 6.7-7" /></svg></span>大文字を含む<span class="password__hidden"></span></li>
    <li class="rule" data-rule="digit"><span class="rule__dot" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M7 12.4l3.3 3.3 6.7-7" /></svg></span>数字を含む<span class="password__hidden"></span></li>
    <li class="rule" data-rule="symbol"><span class="rule__dot" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M7 12.4l3.3 3.3 6.7-7" /></svg></span>記号を含む<span class="password__hidden"></span></li>
  </ul>
  <button class="password__submit" type="submit" aria-disabled="true">次へ進む</button>
</form>

CSS
.password {
  --surface: #ffffff;               /* カードの面と欄の地 */
  --ink: #262626;                   /* 文字・フォーカスの枠・押せるボタン */
  --mute: #898989;                  /* 目のアイコンと、満たしていない条件の丸の縁 */
  --mute-small: #6b6b6b;            /* 補足の文字 */
  --rule: rgba(38, 38, 38, 0.12);   /* 欄の枠・メーターの地・フォーカスの輪の外側 */
  --success: #4f8a64;               /* 満たした条件の丸 */
  --disabled: #c8c7c3;              /* まだ押せないボタン */
  /* 段階の色。帯はそのまま、言葉は 16px 未満の文字でも読める濃さにする */
  --weak: #c4553d;   --weak-text: #a8432d;
  --fair: #c07a3a;   --fair-text: #9a5a22;
  --good: #a98b2e;   --good-text: #7f6a1f;
  --strong: #4f8a64; --strong-text: #3f7352;
  max-width: 560px;
  box-sizing: border-box;
  padding: 48px;
  border-radius: 4px;
  background-color: var(--surface);
  color: var(--ink);
}

.password__title { margin: 0; font-size: 24px; font-weight: 700; line-height: 32px; letter-spacing: -0.01em; }
.password__lead { margin: 8px 0 0; color: var(--mute-small); font-size: 14px; line-height: 24px; letter-spacing: 0.04em; }
.password__label { display: block; margin-top: 32px; color: var(--mute-small); font-size: 13px; font-weight: 600; line-height: 16px; letter-spacing: 0.06em; }

/* 欄の枠と、フォーカスの輪(外側に 4px) */
.password__field {
  display: flex;
  align-items: center;
  height: 56px;
  margin-top: 8px;
  box-sizing: border-box;
  border: 1px solid var(--rule);
  border-radius: 4px;
  transition: border-color 0.2s, box-shadow 0.2s;
}
.password__field:focus-within { border-color: var(--ink); box-shadow: 0 0 0 4px var(--rule); }
.password__input { flex: 1; min-width: 0; height: 100%; padding: 0 0 0 16px; border: 0; background: none; color: inherit; font: inherit; font-size: 16px; font-weight: 500; outline: none; }
.password__toggle { display: grid; place-items: center; width: 48px; height: 100%; padding: 0; border: 0; background: none; color: var(--mute); cursor: pointer; }
.password__toggle:focus-visible { outline: 2px solid var(--ink); outline-offset: -8px; border-radius: 8px; }
.password__toggle svg { width: 24px; height: 24px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }

/* メーター。段階(data-level)で帯の色を移す */
.strength { --level: var(--weak); }
.strength[data-level="1"] { --level: var(--fair); }
.strength[data-level="2"] { --level: var(--good); }
.strength[data-level="3"] { --level: var(--strong); }
.strength__track { height: 8px; margin-top: 24px; overflow: hidden; border-radius: 8px; background-color: var(--rule); }
.strength__bar { display: block; width: 0; height: 100%; border-radius: inherit; background-color: var(--level); transition: background-color 0.25s; }
.strength__caption { display: flex; align-items: center; justify-content: space-between; margin: 8px 0 0; color: var(--mute-small); font-size: 14px; line-height: 24px; letter-spacing: 0.04em; }
/* 段階の言葉は同じ場所に重ね、JS が入れ替える */
.strength__words { display: grid; justify-items: end; font-weight: 700; }
.strength__word { grid-area: 1 / 1; white-space: nowrap; visibility: hidden; opacity: 0; }
.strength__word:nth-child(1) { color: var(--weak-text); }
.strength__word:nth-child(2) { color: var(--fair-text); }
.strength__word:nth-child(3) { color: var(--good-text); }
.strength__word:nth-child(4) { color: var(--strong-text); }

/* 条件の一覧(2 列)。満たしたら丸を塗り、チェックの線は JS が描く */
.rules { display: grid; grid-template-columns: 1fr 1fr; gap: 16px 24px; margin: 24px 0 0; padding: 0; list-style: none; }
.rule { display: flex; align-items: center; gap: 12px; color: var(--mute-small); font-size: 14px; line-height: 24px; letter-spacing: 0.04em; transition: color 0.2s; }
.rule.is-ok { color: var(--ink); }
.rule__dot { display: grid; flex: none; width: 20px; height: 20px; box-sizing: border-box; border: 1px solid var(--mute); border-radius: 50%; transition: background-color 0.2s, border-color 0.2s; }
.rule.is-ok .rule__dot { border-color: var(--success); background-color: var(--success); }
.rule__dot svg { width: 20px; height: 20px; margin: -1px; overflow: visible; fill: none; stroke: #ffffff; stroke-width: 2.8; stroke-linecap: round; stroke-linejoin: round; }

.password__submit {
  width: 100%;
  height: 48px;
  margin-top: 32px;
  border: 0;
  border-radius: 999px;
  background-color: var(--ink);
  color: #ffffff;
  font: inherit;
  font-size: 15px;
  font-weight: 600;
  letter-spacing: 0.04em;
  cursor: pointer;
  transition: background-color 0.3s;
}
.password__submit[aria-disabled="true"] { background-color: var(--disabled); cursor: not-allowed; }
.password__submit:focus-visible { outline: 2px solid var(--ink); outline-offset: 4px; }

/* 読み上げだけに届ける文(見た目からは消す) */
.password__hidden { position: absolute; width: 1px; height: 1px; margin: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }

@media (max-width: 480px) {
  .password { padding: 32px 24px; }
}

@media (prefers-reduced-motion: reduce) {
  .password__field, .strength__bar, .rule, .rule__dot, .password__submit { transition: none; }
}

JavaScript
import { gsap } from "gsap";
import { DrawSVGPlugin } from "gsap/DrawSVGPlugin";

gsap.registerPlugin(DrawSVGPlugin);

const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const t = (seconds) => (reduceMotion ? 0 : seconds); // 動きを減らす設定では、補間せずに切り替える
// 条件。記号は半角の記号だけを数える(全角の文字や空白を記号と数えない)
const RULES = {
  length: (s) => [...s].length >= 8,
  upper: (s) => /[A-Z]/.test(s),
  digit: (s) => /\d/.test(s),
  symbol: (s) => /[!-/:-@[-`{-~]/.test(s),
};
// 強さ(0〜1)。長さで少しずつ伸び(12 字まで)、条件を 1 つ満たすごとに大きく伸びる
const score = (s, passed) => (s ? Math.min(1, 0.06 + (Math.min([...s].length, 12) / 12) * 0.26 + passed * 0.17) : 0);

document.querySelectorAll(".password").forEach((form) => {
  const input = form.querySelector(".password__input");
  const toggle = form.querySelector(".password__toggle");
  const slash = toggle.querySelector(".password__slash");
  const meter = form.querySelector(".strength");
  const bar = meter.querySelector(".strength__bar");
  const words = [...meter.querySelectorAll(".strength__word")];
  const status = meter.querySelector("[role='status']");
  const list = form.querySelector(".rules");
  const rules = [...list.querySelectorAll(".rule")];
  const marks = rules.map((rule) => rule.querySelector("path"));
  const submit = form.querySelector(".password__submit");
  const strongest = words.length - 1;
  let level = -1;

  gsap.set(marks, { drawSVG: "0%" });

  function update() {
    const value = input.value;
    const passed = rules.map((rule) => RULES[rule.dataset.rule](value));
    const s = score(value, passed.filter(Boolean).length);
    const next = value ? words.findLastIndex((word) => s >= Number(word.dataset.min)) : -1;
    // 打つたびに、今の長さから新しい長さへ向かい直す(overwrite で前のトゥイーンの width を止める)
    gsap.to(bar, { width: `${s * 100}%`, duration: t(0.35), ease: "power3.out", overwrite: "auto" });
    if (next !== level) {
      // 段階が変わったときだけ、言葉を入れ替えて帯の色を替える(色は CSS が data-level で移す)
      if (level >= 0) gsap.to(words[level], { autoAlpha: 0, y: -8, duration: t(0.2), ease: "power2.in", overwrite: true });
      if (next >= 0) gsap.fromTo(words[next], { autoAlpha: 0, y: 8 }, { autoAlpha: 1, y: 0, duration: t(0.3), ease: "power2.out", overwrite: true });
      meter.dataset.level = next;
      status.textContent = next >= 0 ? status.dataset.label + words[next].textContent : "";
      level = next;
    }
    rules.forEach((rule, i) => {
      if (rule.classList.contains("is-ok") === passed[i]) return;
      rule.classList.toggle("is-ok", passed[i]); // 丸の塗りと文字の色は CSS
      rule.lastElementChild.textContent = passed[i] ? list.dataset.ok : "";
      gsap.to(marks[i], { drawSVG: passed[i] ? "100%" : "0%", duration: t(passed[i] ? 0.3 : 0.2), delay: passed[i] ? t(0.05) : 0, ease: "power2.out", overwrite: true });
    });
    // いちばん強い段階になるまで、ボタンは押せない色のまま(焦点は受けられるよう disabled にはしない)
    submit.setAttribute("aria-disabled", String(level < strongest));
  }

  // 日本語入力の変換中は数えない。未確定の文字(ひらがな)で強さが揺れないよう、確定してから数える
  input.addEventListener("input", (event) => {
    if (!event.isComposing) update();
  });
  input.addEventListener("compositionend", update);

  // Enter は keydown で受けず、form の送信で受ける(keydown で受けると、変換を確定する Enter でも送ってしまう)。
  // いちばん強い段階になるまでは送らず、欄へ戻す
  form.addEventListener("submit", (event) => {
    if (level === strongest) return;
    event.preventDefault();
    input.focus();
  });

  // 表示の切り替え。押している間は文字を見せ、目の斜線を消す
  toggle.addEventListener("click", () => {
    const show = input.type === "password";
    input.type = show ? "text" : "password";
    toggle.setAttribute("aria-pressed", String(show));
    gsap.to(slash, { drawSVG: show ? "0%" : "100%", duration: t(0.25), ease: "power2.out", overwrite: true });
  });

  update(); // 自動入力や、戻ってきたページに値が残っていたときも数える
});

インタラクションデザインについて、
お気軽にご相談ください。

ここに並べているのは、Web サイトの中で動く UI のサンプルです。新しく作るサイトに組み込むことも、いま公開中のサイトに、ボタンやヒーロー、スクロールの演出だけを足すこともできます。設計から実装までの進め方はWeb サイト制作、端末ごとの見え方や表示速度の考え方はレスポンシブウェブサイトのページにまとめています。映像として作る動きをお探しの場合はモーショングラフィックスをご覧ください。