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);
})();
🤖 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で提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。