本文へスキップ
HELLO!
HIGASHIYUKI®.COM
0   /   100
ナビゲーション

アコーディオンの質問を押すと答えが開き、別の質問で入れ替わる

デモ

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

仕様

動作
DOM
種類
レシピ
動作確認
three 0.186.0・gsap 3.15.0
公開日

しくみと調整

答えを、はみ出しを隠した枠に入れておき、枠の height を 0 から中身の高さまで伸ばして開きます。下の項目は、ふつうの行送りのまま押し下げられるので、位置の計算は要りません。中身の文は、枠が開き始めてから少し遅れて浮かび上がります。

高さは 0.5 秒の power2.inOut、中身は 0.12 秒後から 0.35 秒で出します。同時に出すと、まだ狭い枠の中で文が動いて読みにくくなります。+ の印は、縦の棒を 0.4 秒かけて 90° 回して − に変え、あわせて丸を文字の色で塗り、明暗を反転します。

高さの行き先は auto で渡せます。GSAP が中身の高さをその場で測って px に直すので、答えの長さが項目ごとに違っても同じ書き方で済みます。ただし測った値はそのまま覚え込まれるため、画面の幅が変わって行数が増えたときは、閉じた形に戻してから測り直させます。

開くのは 1 つだけにしてあります。押すたびに、開いている項目を逆再生し、押した項目を再生します。同じ質問をもう一度押せば閉じます。状態は見た目だけでなく aria-expanded にも書き、質問を見出しの中のボタンにして、答えの領域と結び付けます。

複数を開けるようにするなら、開いている項目を閉じる 1 行を外すだけです。答えが長いときは、伸びる時間を 0.6 秒ほどまでゆるめると、動く量に対して速すぎません。よくある質問・料金表の注記・設定画面の折りたたみに向いています。

コード

HTML
<div class="faq">
  <!-- <details> ではなく、見出しの中のボタンと領域で組む。<details> は open を外した瞬間に
       中身が display:none になるので、閉じていく動きを見せられない。見出しの階層もページに合わせられる -->
  <div class="faq__item">
    <h2 class="faq__head">
      <button class="faq__q" type="button" id="faq-q1" aria-expanded="false" aria-controls="faq-a1">
        無料体験では、何ができますか?
        <span class="faq__icon" aria-hidden="true">
          <span class="faq__bar"></span>
          <span class="faq__bar faq__bar--v"></span>
        </span>
      </button>
    </h2>
    <div class="faq__a" id="faq-a1" role="region" aria-labelledby="faq-q1">
      <p class="faq__inner">14日間、すべての機能を無料でお試しいただけます。<br>カードの登録は不要で、設定画面からいつでも解約できます。</p>
    </div>
  </div>

  <div class="faq__item">
    <h2 class="faq__head">
      <button class="faq__q" type="button" id="faq-q2" aria-expanded="false" aria-controls="faq-a2">
        あとからプランを変更できますか?
        <span class="faq__icon" aria-hidden="true">
          <span class="faq__bar"></span>
          <span class="faq__bar faq__bar--v"></span>
        </span>
      </button>
    </h2>
    <div class="faq__a" id="faq-a2" role="region" aria-labelledby="faq-q2">
      <p class="faq__inner">はい、いつでも変更できます。<br>差額は日割りで計算し、次回のご請求に反映します。</p>
    </div>
  </div>

  <div class="faq__item">
    <h2 class="faq__head">
      <button class="faq__q" type="button" id="faq-q3" aria-expanded="false" aria-controls="faq-a3">
        データは安全に保管されますか?
        <span class="faq__icon" aria-hidden="true">
          <span class="faq__bar"></span>
          <span class="faq__bar faq__bar--v"></span>
        </span>
      </button>
    </h2>
    <div class="faq__a" id="faq-a3" role="region" aria-labelledby="faq-q3">
      <p class="faq__inner">通信時も保存時も、すべてのデータを暗号化しています。<br>第三者機関によるセキュリティ監査も、定期的に受けています。</p>
    </div>
  </div>
</div>
CSS
.faq {
  --ink: #eeece6; /* 文字と線の色 */
  --ground: #111111; /* 地の色。開いている間の棒の色になる */
  --line: rgba(238, 236, 230, 0.14); /* 区切り線 */
  border-top: 1px solid var(--line);
  color: var(--ink);
}

.faq__item { border-bottom: 1px solid var(--line); }
.faq__head { margin: 0; }

.faq__q {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  width: 100%;
  padding: 24px 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  font-size: 20px; /* font: inherit の後に書く(先だと打ち消される) */
  font-weight: 700;
  line-height: 1.5;
  letter-spacing: 0.04em;
  text-align: left;
  cursor: pointer;
}

.faq__q:focus-visible { outline: 2px solid var(--ink); outline-offset: 4px; }

/* 丸の線は box-shadow の inset で引く。border だと、線の太さぶん中の棒がずれる */
.faq__icon {
  position: relative;
  flex: none;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  box-shadow: inset 0 0 0 1px var(--ink);
}

/* + は横棒と縦棒。縦棒を 90° 回すと横棒に重なって − になる */
.faq__bar {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 14px;
  height: 2px;
  margin: -1px 0 0 -7px;
  background-color: var(--ink);
}

.faq__bar--v { width: 2px; height: 14px; margin: -7px 0 0 -1px; }

/* 高さを 0 から中身の高さまで伸ばす枠。はみ出しを隠さないと、閉じていても答えが見える */
.faq__a {
  height: 0;
  overflow: hidden;
}

.faq__inner {
  margin: 0;
  padding: 0 64px 32px 0;
  font-size: 18px;
  line-height: 32px;
  letter-spacing: 0.04em;
}
JavaScript
import { gsap } from "gsap";

const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const CLEAR = "rgba(0, 0, 0, 0)"; // 閉じている間の丸の中(塗らない)

document.querySelectorAll(".faq").forEach((faq) => {
  const items = [...faq.querySelectorAll(".faq__item")];
  const styles = getComputedStyle(faq);
  // 色は CSS が正。GSAP は var(--name) のままでは補間できないので、値にして渡す
  const ink = styles.getPropertyValue("--ink").trim();
  const ground = styles.getPropertyValue("--ground").trim();

  const toggles = items.map((item) => {
    const answer = item.querySelector(".faq__a");
    const inner = item.querySelector(".faq__inner");
    const icon = item.querySelector(".faq__icon");
    const bars = item.querySelectorAll(".faq__bar");

    return gsap
      .timeline({ paused: true })
      // "auto" で渡してよい。GSAP が中身の高さを測り、px に直して補間する
      .fromTo(answer, { height: 0 }, { height: "auto", duration: 0.5, ease: "power2.inOut" }, 0)
      .fromTo(inner, { autoAlpha: 0, y: -8 }, { autoAlpha: 1, y: 0, duration: 0.35, ease: "power2.out" }, 0.12)
      .fromTo(bars[1], { rotation: 0 }, { rotation: 90, duration: 0.4, ease: "power2.inOut" }, 0)
      // 開いた項目は丸を塗り、棒を地の色にして明暗を反転する(色を足すより目立つ)
      .fromTo(icon, { backgroundColor: CLEAR }, { backgroundColor: ink, duration: 0.3, ease: "none" }, 0)
      .fromTo(bars, { backgroundColor: ink }, { backgroundColor: ground, duration: 0.3, ease: "none" }, 0);
  });

  // 動きを減らす設定では、ほぼ一瞬で切り替える
  if (reduceMotion) toggles.forEach((toggle) => toggle.timeScale(20));

  let current = -1; // 開いている項目(-1 は無し)
  items.forEach((item, i) => {
    item.querySelector(".faq__q").addEventListener("click", () => {
      current = current === i ? -1 : i; // 開いている項目をもう一度押したら閉じる
      toggles.forEach((toggle, k) => (k === current ? toggle.play() : toggle.reverse()));
      items.forEach((it, k) => {
        it.querySelector(".faq__q").setAttribute("aria-expanded", String(k === current));
      });
    });
  });

  // 幅が変われば答えの高さも変わる。いったん閉じた形に戻してから、測り直させる
  window.addEventListener("resize", () => {
    toggles.forEach((toggle) => {
      const progress = toggle.progress();
      toggle.progress(0).invalidate().progress(progress);
    });
  });
});

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

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