ボタン 中級

FAB展開メニュー

+のFABをクリックすると共有・編集・削除の3アクションが縦方向へ時間差で展開し、アイコンは×へ回転します。もう一度押す・外側クリック・Escapeで畳めます。

#css#js#button#fab

ライブデモ

コード

HTML
<!-- FAB展開メニュー: +のFABをクリックすると子アクション3つが縦方向に時間差で展開し、+は×へ回転する -->
<div class="stage">
  <div class="fab-wrap">
    <div class="fab-actions">
      <button class="fab-action" type="button" data-i="0" tabindex="-1" aria-label="共有する">
        <svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="18" cy="5" r="2.4"></circle><circle cx="6" cy="12" r="2.4"></circle><circle cx="18" cy="19" r="2.4"></circle><path d="M8.2 10.8 15.8 6.6M8.2 13.2l7.6 4.2"></path></svg>
      </button>
      <button class="fab-action" type="button" data-i="1" tabindex="-1" aria-label="編集する">
        <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 20h4L18.5 9.5a2.1 2.1 0 0 0-3-3L5 17v3Z"></path></svg>
      </button>
      <button class="fab-action" type="button" data-i="2" tabindex="-1" aria-label="削除する">
        <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 7h14M9 7V5a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2m-9 0 1 13a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1l1-13"></path></svg>
      </button>
    </div>
    <button class="fab-main" type="button" aria-expanded="false" aria-label="メニューを開く">
      <span class="fab-main__bar fab-main__bar--h" aria-hidden="true"></span>
      <span class="fab-main__bar fab-main__bar--v" aria-hidden="true"></span>
    </button>
  </div>
  <p class="hint">+を押すとメニューが時間差で展開します。もう一度押す・×へ・Escape・外側クリックで畳めます</p>
</div>
CSS
* { box-sizing: border-box; }
body {
  margin: 0;
  min-height: 100vh;
  display: grid;
  place-items: center;
  overflow: hidden;
  font-family: "Segoe UI", system-ui, sans-serif;
  background: radial-gradient(circle at 50% 25%, #fff0f0 0%, #fde3e3 50%, #f9d2d2 100%);
}

.stage {
  display: grid;
  place-items: center;
  gap: 26px;
}

.fab-wrap {
  position: relative;
  width: 64px;
  height: 64px;
}

/* 子アクション: 初期状態はメイン直下に重なって不可視。openで上方向へ時間差で展開する */
.fab-action {
  position: absolute;
  left: 0;
  bottom: 0;
  width: 52px;
  height: 52px;
  margin: 6px;
  display: grid;
  place-items: center;
  border: none;
  border-radius: 50%;
  background: #fff;
  color: #e35d6a;
  cursor: pointer;
  box-shadow: 0 8px 18px rgba(227, 93, 106, .28);
  opacity: 0;
  transform: translateY(0) scale(.5);
  transition: transform .38s cubic-bezier(.34, 1.4, .4, 1), opacity .25s ease;
  transition-delay: 0s;
  pointer-events: none;
}

.fab-action svg {
  width: 20px;
  height: 20px;
}
.fab-action svg path,
.fab-action svg circle {
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.fab-wrap.is-open .fab-action {
  opacity: 1;
  pointer-events: auto;
}

/* 各ボタンの到達位置と展開の遅延(時間差)。開く時だけ遅延をつけ、閉じる時は基本ルールの0sで即畳む */
.fab-wrap.is-open .fab-action:nth-child(1) {
  transform: translateY(-76px) scale(1);
  transition-delay: 0s;
}
.fab-wrap.is-open .fab-action:nth-child(2) {
  transform: translateY(-146px) scale(1);
  transition-delay: .06s;
}
.fab-wrap.is-open .fab-action:nth-child(3) {
  transform: translateY(-216px) scale(1);
  transition-delay: .12s;
}

.fab-main {
  position: relative;
  z-index: 1;
  width: 64px;
  height: 64px;
  border: none;
  border-radius: 50%;
  display: grid;
  place-items: center;
  background: linear-gradient(135deg, #f0666f 0%, #e04a56 100%);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  box-shadow: 0 14px 28px rgba(224, 74, 86, .4);
  transition: transform .3s cubic-bezier(.22, 1, .36, 1), box-shadow .35s ease;
  /* 待機時のアイドル: 影がゆっくり呼吸してクリック可能なことを伝える(展開中は停止) */
  animation: fab-idle-breathe 3s ease-in-out infinite;
}

@keyframes fab-idle-breathe {
  0%, 100% { box-shadow: 0 14px 28px rgba(224, 74, 86, .4); }
  50%      { box-shadow: 0 18px 36px rgba(224, 74, 86, .55); }
}

.fab-main__bar {
  position: absolute;
  background: #fff;
  border-radius: 2px;
  transition: transform .35s cubic-bezier(.22, 1, .36, 1);
}
.fab-main__bar--h { width: 22px; height: 3px; }
.fab-main__bar--v { width: 3px; height: 22px; }

.fab-main:hover,
.fab-main:focus-visible {
  transform: scale(1.06);
  animation-play-state: paused;
}

.fab-main:active {
  transform: scale(.94);
}

.fab-main:focus-visible {
  outline: 3px solid rgba(224, 74, 86, .5);
  outline-offset: 4px;
}

.fab-wrap.is-open .fab-main {
  animation: none;
}

/* + を × へ回転(横棒・縦棒をそれぞれ45度回転させ交差させる) */
.fab-wrap.is-open .fab-main__bar--h,
.fab-wrap.is-open .fab-main__bar--v {
  transform: rotate(45deg);
}

.hint {
  margin: 0;
  font-size: 13px;
  color: #8a5058;
  text-align: center;
  max-width: 300px;
}

@media (prefers-reduced-motion: reduce) {
  .fab-main { animation: none; }
  .fab-action { transition-duration: .01ms; }
}
JavaScript
// FAB展開メニュー: クリックでopen/close切替。子アクションのクリック・Escape・外側クリックでも閉じる
(() => {
  const wrap = document.querySelector('.fab-wrap');
  if (!wrap) return; // null安全

  const main = wrap.querySelector('.fab-main');
  const actions = Array.from(wrap.querySelectorAll('.fab-action'));
  if (!main) return;

  let open = false;

  const setOpen = (value) => {
    open = value;
    wrap.classList.toggle('is-open', open);
    main.setAttribute('aria-expanded', String(open));
    main.setAttribute('aria-label', open ? 'メニューを閉じる' : 'メニューを開く');
    actions.forEach((el) => { el.tabIndex = open ? 0 : -1; });
  };

  main.addEventListener('click', () => setOpen(!open));

  actions.forEach((el) => {
    el.addEventListener('click', () => setOpen(false));
  });

  document.addEventListener('keydown', (e) => {
    if (e.key === 'Escape' && open) {
      setOpen(false);
      main.focus();
    }
  });

  document.addEventListener('pointerdown', (e) => {
    if (open && !wrap.contains(e.target)) setOpen(false);
  });

  setOpen(false);
})();

🤖 AIエージェント用プロンプト

このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「FAB展開メニュー」の効果を追加してください。

# 追加してほしい効果
FAB展開メニュー(ボタン)
+のFABをクリックすると共有・編集・削除の3アクションが縦方向へ時間差で展開し、アイコンは×へ回転します。もう一度押す・外側クリック・Escapeで畳めます。

# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- FAB展開メニュー: +のFABをクリックすると子アクション3つが縦方向に時間差で展開し、+は×へ回転する -->
<div class="stage">
  <div class="fab-wrap">
    <div class="fab-actions">
      <button class="fab-action" type="button" data-i="0" tabindex="-1" aria-label="共有する">
        <svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="18" cy="5" r="2.4"></circle><circle cx="6" cy="12" r="2.4"></circle><circle cx="18" cy="19" r="2.4"></circle><path d="M8.2 10.8 15.8 6.6M8.2 13.2l7.6 4.2"></path></svg>
      </button>
      <button class="fab-action" type="button" data-i="1" tabindex="-1" aria-label="編集する">
        <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 20h4L18.5 9.5a2.1 2.1 0 0 0-3-3L5 17v3Z"></path></svg>
      </button>
      <button class="fab-action" type="button" data-i="2" tabindex="-1" aria-label="削除する">
        <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 7h14M9 7V5a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2m-9 0 1 13a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1l1-13"></path></svg>
      </button>
    </div>
    <button class="fab-main" type="button" aria-expanded="false" aria-label="メニューを開く">
      <span class="fab-main__bar fab-main__bar--h" aria-hidden="true"></span>
      <span class="fab-main__bar fab-main__bar--v" aria-hidden="true"></span>
    </button>
  </div>
  <p class="hint">+を押すとメニューが時間差で展開します。もう一度押す・×へ・Escape・外側クリックで畳めます</p>
</div>

【CSS】
* { box-sizing: border-box; }
body {
  margin: 0;
  min-height: 100vh;
  display: grid;
  place-items: center;
  overflow: hidden;
  font-family: "Segoe UI", system-ui, sans-serif;
  background: radial-gradient(circle at 50% 25%, #fff0f0 0%, #fde3e3 50%, #f9d2d2 100%);
}

.stage {
  display: grid;
  place-items: center;
  gap: 26px;
}

.fab-wrap {
  position: relative;
  width: 64px;
  height: 64px;
}

/* 子アクション: 初期状態はメイン直下に重なって不可視。openで上方向へ時間差で展開する */
.fab-action {
  position: absolute;
  left: 0;
  bottom: 0;
  width: 52px;
  height: 52px;
  margin: 6px;
  display: grid;
  place-items: center;
  border: none;
  border-radius: 50%;
  background: #fff;
  color: #e35d6a;
  cursor: pointer;
  box-shadow: 0 8px 18px rgba(227, 93, 106, .28);
  opacity: 0;
  transform: translateY(0) scale(.5);
  transition: transform .38s cubic-bezier(.34, 1.4, .4, 1), opacity .25s ease;
  transition-delay: 0s;
  pointer-events: none;
}

.fab-action svg {
  width: 20px;
  height: 20px;
}
.fab-action svg path,
.fab-action svg circle {
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.fab-wrap.is-open .fab-action {
  opacity: 1;
  pointer-events: auto;
}

/* 各ボタンの到達位置と展開の遅延(時間差)。開く時だけ遅延をつけ、閉じる時は基本ルールの0sで即畳む */
.fab-wrap.is-open .fab-action:nth-child(1) {
  transform: translateY(-76px) scale(1);
  transition-delay: 0s;
}
.fab-wrap.is-open .fab-action:nth-child(2) {
  transform: translateY(-146px) scale(1);
  transition-delay: .06s;
}
.fab-wrap.is-open .fab-action:nth-child(3) {
  transform: translateY(-216px) scale(1);
  transition-delay: .12s;
}

.fab-main {
  position: relative;
  z-index: 1;
  width: 64px;
  height: 64px;
  border: none;
  border-radius: 50%;
  display: grid;
  place-items: center;
  background: linear-gradient(135deg, #f0666f 0%, #e04a56 100%);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  box-shadow: 0 14px 28px rgba(224, 74, 86, .4);
  transition: transform .3s cubic-bezier(.22, 1, .36, 1), box-shadow .35s ease;
  /* 待機時のアイドル: 影がゆっくり呼吸してクリック可能なことを伝える(展開中は停止) */
  animation: fab-idle-breathe 3s ease-in-out infinite;
}

@keyframes fab-idle-breathe {
  0%, 100% { box-shadow: 0 14px 28px rgba(224, 74, 86, .4); }
  50%      { box-shadow: 0 18px 36px rgba(224, 74, 86, .55); }
}

.fab-main__bar {
  position: absolute;
  background: #fff;
  border-radius: 2px;
  transition: transform .35s cubic-bezier(.22, 1, .36, 1);
}
.fab-main__bar--h { width: 22px; height: 3px; }
.fab-main__bar--v { width: 3px; height: 22px; }

.fab-main:hover,
.fab-main:focus-visible {
  transform: scale(1.06);
  animation-play-state: paused;
}

.fab-main:active {
  transform: scale(.94);
}

.fab-main:focus-visible {
  outline: 3px solid rgba(224, 74, 86, .5);
  outline-offset: 4px;
}

.fab-wrap.is-open .fab-main {
  animation: none;
}

/* + を × へ回転(横棒・縦棒をそれぞれ45度回転させ交差させる) */
.fab-wrap.is-open .fab-main__bar--h,
.fab-wrap.is-open .fab-main__bar--v {
  transform: rotate(45deg);
}

.hint {
  margin: 0;
  font-size: 13px;
  color: #8a5058;
  text-align: center;
  max-width: 300px;
}

@media (prefers-reduced-motion: reduce) {
  .fab-main { animation: none; }
  .fab-action { transition-duration: .01ms; }
}

【JavaScript】
// FAB展開メニュー: クリックでopen/close切替。子アクションのクリック・Escape・外側クリックでも閉じる
(() => {
  const wrap = document.querySelector('.fab-wrap');
  if (!wrap) return; // null安全

  const main = wrap.querySelector('.fab-main');
  const actions = Array.from(wrap.querySelectorAll('.fab-action'));
  if (!main) return;

  let open = false;

  const setOpen = (value) => {
    open = value;
    wrap.classList.toggle('is-open', open);
    main.setAttribute('aria-expanded', String(open));
    main.setAttribute('aria-label', open ? 'メニューを閉じる' : 'メニューを開く');
    actions.forEach((el) => { el.tabIndex = open ? 0 : -1; });
  };

  main.addEventListener('click', () => setOpen(!open));

  actions.forEach((el) => {
    el.addEventListener('click', () => setOpen(false));
  });

  document.addEventListener('keydown', (e) => {
    if (e.key === 'Escape' && open) {
      setOpen(false);
      main.focus();
    }
  });

  document.addEventListener('pointerdown', (e) => {
    if (open && !wrap.contains(e.target)) setOpen(false);
  });

  setOpen(false);
})();

# 外部ライブラリ
なし(追加ライブラリ不要)

# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。