ハンバーガー組み上げメニュー
3本線のハンバーガーアイコンを開くと、バンズ・レタス・パティが上から落ちてきて本物のハンバーガーが組み上がります。閉じると逆再生。ただの3本線に遊び心を加える、ナビゲーションの一捻りです。
ライブデモ
使用例(お題: カフェ MOON BREW)
この技法を「カフェ MOON BREW」というテーマのダミーサイトで実際に使った例です。
HTML
<!-- MOON BREW: カフェのヘッダー。メニューを開くとバーガーが組み上がる遊び -->
<div class="site">
<header class="bar">
<span class="logo">☕ MOON BREW</span>
<div class="ham" data-ham role="button" tabindex="0" aria-label="メニュー(クリックで開閉)">
<svg viewBox="0 0 120 120" aria-hidden="true">
<g class="lines" fill="#3a2417">
<rect x="28" y="34" width="64" height="8" rx="4"/>
<rect x="28" y="54" width="64" height="8" rx="4"/>
<rect x="28" y="74" width="64" height="8" rx="4"/>
</g>
<g class="part" data-part="bun-bottom"><rect x="32" y="84" width="56" height="12" rx="6" fill="#E8A33D"/></g>
<g class="part" data-part="patty"><rect x="30" y="70" width="60" height="12" rx="6" fill="#8B5A2B"/></g>
<g class="part" data-part="lettuce"><path d="M26 56 H94 V64 q-5.6 7 -11.3 0 q-5.6 7 -11.3 0 q-5.6 7 -11.3 0 q-5.6 7 -11.3 0 q-5.6 7 -11.3 0 q-5.6 7 -11.3 0 Z" fill="#7BC86C"/></g>
<g class="part" data-part="bun-top"><path d="M28 58 Q28 28 60 28 Q92 28 92 58 Z" fill="#E8A33D"/></g>
<g class="sesame" fill="#FFF7E6">
<ellipse cx="48" cy="48" rx="2.4" ry="1.5" transform="rotate(-20 48 48)"/>
<ellipse cx="60" cy="42" rx="2.4" ry="1.5"/>
<ellipse cx="72" cy="48" rx="2.4" ry="1.5" transform="rotate(20 72 48)"/>
<ellipse cx="54" cy="53" rx="2.4" ry="1.5" transform="rotate(12 54 53)"/>
<ellipse cx="66" cy="53" rx="2.4" ry="1.5" transform="rotate(-12 66 53)"/>
<ellipse cx="60" cy="38" rx="2.4" ry="1.5"/>
</g>
</svg>
</div>
</header>
<section class="hero">
<p class="eyebrow">SEASONAL MENU</p>
<h1>とろける<br>メープルラテ</h1>
<p class="lead">右上のメニューを開くと、ひと手間でバーガーが組み上がります。</p>
</section>
</div>
CSS
* { box-sizing: border-box; }
html, body { margin: 0; width: 100%; min-height: 100%; background: #F4ECDD; }
.site {
position: relative;
width: 100%; height: 100vh; min-height: 300px; max-height: 100%;
overflow: hidden;
background: radial-gradient(120% 80% at 80% 0%, #fbf4e7 0%, #F0E4CF 100%);
font-family: Georgia, "Hiragino Mincho ProN", serif;
}
.bar {
display: flex; align-items: center; justify-content: space-between;
padding: 14px 22px;
border-bottom: 1px solid rgba(111,78,55,.18);
}
.logo { font-weight: 700; font-size: 18px; color: #3a2417; letter-spacing: .04em; }
.ham { width: 64px; height: 64px; cursor: pointer; outline: none; }
.ham svg { width: 100%; height: 100%; display: block; overflow: visible; }
.part { transform-box: fill-box; transform-origin: center bottom; opacity: 0; }
.sesame { opacity: 0; }
.lines { opacity: 1; }
.hero { padding: 56px 26px; color: #3a2417; }
.eyebrow { margin: 0 0 14px; font-size: 12px; font-weight: 700; letter-spacing: .28em; color: #b07a3c; font-family: system-ui, sans-serif; }
.hero h1 { margin: 0; font-size: clamp(36px, 9vw, 68px); line-height: 1.12; font-weight: 900; }
.lead { margin: 18px 0 0; font-size: 14px; line-height: 1.8; color: #6F4E37; max-width: 30ch; }
JavaScript
// 開閉を WAAPI で精密制御(デモと同じロジック)。線フェード+食材の落下入替。
(() => {
const ham = document.querySelector('[data-ham]');
if (!ham) return;
const lines = ham.querySelector('.lines');
const sesame = ham.querySelector('.sesame');
const parts = [...ham.querySelectorAll('.part')];
const SPRINGY = 'cubic-bezier(0.16, 1, 0.3, 1)';
const drop = [
{ transform: 'translateY(-80px) scaleY(1)', opacity: 0, offset: 0 },
{ opacity: 1, offset: 0.5 },
{ transform: 'translateY(0px) scaleY(0.88)', offset: 0.72 },
{ transform: 'translateY(0px) scaleY(1)', opacity: 1, offset: 1 }
];
const rise = [
{ transform: 'translateY(0px) scaleY(1)', opacity: 1 },
{ transform: 'translateY(-80px) scaleY(1)', opacity: 0 }
];
let open = false, timer = 0;
const openBurger = () => {
lines.animate([{ opacity: 1 }, { opacity: 0 }], { duration: 150, fill: 'forwards' });
parts.forEach((p, idx) =>
p.animate(drop, { duration: 500, delay: idx * 90, easing: SPRINGY, fill: 'forwards' }));
sesame.animate([{ opacity: 0 }, { opacity: 1 }],
{ duration: 200, delay: parts.length * 90 + 260, fill: 'forwards' });
};
const closeBurger = () => {
const n = parts.length;
sesame.animate([{ opacity: 1 }, { opacity: 0 }], { duration: 120, fill: 'forwards' });
parts.forEach((p, idx) => {
const order = n - 1 - idx;
p.animate(rise, { duration: 300, delay: order * 60, easing: 'cubic-bezier(0.4,0,1,1)', fill: 'forwards' });
});
lines.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 200, delay: n * 60 + 120, fill: 'forwards' });
};
const toggle = () => { open = !open; open ? openBurger() : closeBurger(); };
const schedule = () => { timer = setInterval(toggle, 2800); };
const manual = () => { clearInterval(timer); toggle(); schedule(); };
ham.addEventListener('click', manual);
ham.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); manual(); } });
schedule();
})();
実装ガイド
使いどころ
遊び心のあるブランドや飲食系サイトのナビ開閉に。ただの3本線に意外性を持たせ、メニューを開く動作自体を楽しませます。
実装時の注意点
線→×のパスモーフはせず、線のフェードアウト+食材パーツの落下入替で構成します。落下はWAAPI(element.animate)でstaggerと着地のsquashを精密制御。落下順と描画順(重なり)を混同しないこと。クリック/キー操作での切替と自動デモを併存させます。
対応ブラウザ
Web Animations API・SVG・CSS transformは全モダンブラウザで安定動作します。fill:"forwards"で最終状態を保持し、Enter/Spaceのキー操作とaria配慮を入れるとアクセシブルです。対応は実機で確認してください。
よくある失敗
SVGの重なり順(DOM順)と落下発火順を混同するとパーツが前後します。transform-originをfill-boxにしないとsquashが意図しない基準で潰れます。自動タイマーとクリックの競合はクリック時にタイマーをリセットして防ぎます。
応用例
食材を商品やアイコンに差し替える、開閉に効果音、メニュー本体のスライド表示と連動、ブランド配色への変更などに発展できます。
コード
HTML
<!-- ハンバーガー組み上げメニュー:開くと食材が上から落ちて本物のバーガーになる -->
<div class="stage">
<div class="ham" data-ham role="button" tabindex="0" aria-label="メニュー(クリックで開閉)">
<svg viewBox="0 0 120 120" aria-hidden="true">
<!-- 閉状態:3本線アイコン -->
<g class="lines" fill="#1A1A1A">
<rect x="28" y="34" width="64" height="8" rx="4"/>
<rect x="28" y="54" width="64" height="8" rx="4"/>
<rect x="28" y="74" width="64" height="8" rx="4"/>
</g>
<!-- 開状態:食材(落下順 = bun-bottom → patty → lettuce → bun-top) -->
<g class="part" data-part="bun-bottom">
<rect x="32" y="84" width="56" height="12" rx="6" fill="#E8A33D"/>
</g>
<g class="part" data-part="patty">
<rect x="30" y="70" width="60" height="12" rx="6" fill="#8B5A2B"/>
</g>
<g class="part" data-part="lettuce">
<path d="M26 56 H94 V64 q-5.6 7 -11.3 0 q-5.6 7 -11.3 0 q-5.6 7 -11.3 0 q-5.6 7 -11.3 0 q-5.6 7 -11.3 0 q-5.6 7 -11.3 0 Z" fill="#7BC86C"/>
</g>
<g class="part" data-part="bun-top">
<path d="M28 58 Q28 28 60 28 Q92 28 92 58 Z" fill="#E8A33D"/>
</g>
<!-- ゴマ(bun-top着地後に出現) -->
<g class="sesame" fill="#FFF7E6">
<ellipse cx="48" cy="48" rx="2.4" ry="1.5" transform="rotate(-20 48 48)"/>
<ellipse cx="60" cy="42" rx="2.4" ry="1.5"/>
<ellipse cx="72" cy="48" rx="2.4" ry="1.5" transform="rotate(20 72 48)"/>
<ellipse cx="54" cy="53" rx="2.4" ry="1.5" transform="rotate(12 54 53)"/>
<ellipse cx="66" cy="53" rx="2.4" ry="1.5" transform="rotate(-12 66 53)"/>
<ellipse cx="60" cy="38" rx="2.4" ry="1.5"/>
</g>
</svg>
</div>
<p class="cap">MENU</p>
</div>
CSS
:root{
--ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
--ease-inout: cubic-bezier(0.65, 0, 0.35, 1);
}
* { box-sizing: border-box; }
html, body { margin: 0; width: 100%; min-height: 100%; background: #F7F5F0; }
.stage {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100vh;
min-height: 240px;
max-height: 100%;
background: #F7F5F0;
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
}
.ham { width: 160px; height: 160px; cursor: pointer; outline: none; }
.ham svg { width: 100%; height: 100%; display: block; overflow: visible; }
/* 各食材:scaleY squash を下端基準で。初期は非表示(落下前) */
.part {
transform-box: fill-box;
transform-origin: center bottom;
opacity: 0;
}
.sesame { opacity: 0; }
.lines { opacity: 1; }
.cap {
margin: 14px 0 0;
font-size: 12px; font-weight: 700; letter-spacing: .28em;
color: #1A1A1A;
}
JavaScript
// 開閉を WAAPI で精密制御。線→×のモーフはせず「線フェード+食材の落下入替」方式。
(() => {
const ham = document.querySelector('[data-ham]');
if (!ham) return; // null安全
const lines = ham.querySelector('.lines');
const sesame = ham.querySelector('.sesame');
// document順 = 落下順:bun-bottom → patty → lettuce → bun-top
const parts = [...ham.querySelectorAll('.part')];
const SPRINGY = 'cubic-bezier(0.16, 1, 0.3, 1)';
// 落下+着地で scaleY 0.88 に潰して戻す squash
const drop = [
{ transform: 'translateY(-80px) scaleY(1)', opacity: 0, offset: 0 },
{ opacity: 1, offset: 0.5 },
{ transform: 'translateY(0px) scaleY(0.88)', offset: 0.72 },
{ transform: 'translateY(0px) scaleY(1)', opacity: 1, offset: 1 }
];
const rise = [
{ transform: 'translateY(0px) scaleY(1)', opacity: 1 },
{ transform: 'translateY(-80px) scaleY(1)', opacity: 0 }
];
let open = false, timer = 0;
const openBurger = () => {
lines.animate([{ opacity: 1 }, { opacity: 0 }], { duration: 150, fill: 'forwards' });
parts.forEach((p, idx) =>
p.animate(drop, { duration: 500, delay: idx * 90, easing: SPRINGY, fill: 'forwards' }));
sesame.animate([{ opacity: 0 }, { opacity: 1 }],
{ duration: 200, delay: parts.length * 90 + 260, fill: 'forwards' });
};
const closeBurger = () => {
const n = parts.length;
sesame.animate([{ opacity: 1 }, { opacity: 0 }], { duration: 120, fill: 'forwards' });
parts.forEach((p, idx) => {
const order = n - 1 - idx; // bun-top(最後の要素)が最初に離脱
p.animate(rise, { duration: 300, delay: order * 60, easing: 'cubic-bezier(0.4,0,1,1)', fill: 'forwards' });
});
lines.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 200, delay: n * 60 + 120, fill: 'forwards' });
};
const toggle = () => { open = !open; open ? openBurger() : closeBurger(); };
const schedule = () => { timer = setInterval(toggle, 2800); };
// クリック/キーで切替(自動タイマーはリセット)
const manual = () => { clearInterval(timer); toggle(); schedule(); };
ham.addEventListener('click', manual);
ham.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); manual(); } });
schedule();
})();
🤖 AIエージェント用プロンプト
このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「ハンバーガー組み上げメニュー」の効果を追加してください。
# 追加してほしい効果
ハンバーガー組み上げメニュー(マイクロインタラクション)
3本線のハンバーガーアイコンを開くと、バンズ・レタス・パティが上から落ちてきて本物のハンバーガーが組み上がります。閉じると逆再生。ただの3本線に遊び心を加える、ナビゲーションの一捻りです。
# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】
# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- ハンバーガー組み上げメニュー:開くと食材が上から落ちて本物のバーガーになる -->
<div class="stage">
<div class="ham" data-ham role="button" tabindex="0" aria-label="メニュー(クリックで開閉)">
<svg viewBox="0 0 120 120" aria-hidden="true">
<!-- 閉状態:3本線アイコン -->
<g class="lines" fill="#1A1A1A">
<rect x="28" y="34" width="64" height="8" rx="4"/>
<rect x="28" y="54" width="64" height="8" rx="4"/>
<rect x="28" y="74" width="64" height="8" rx="4"/>
</g>
<!-- 開状態:食材(落下順 = bun-bottom → patty → lettuce → bun-top) -->
<g class="part" data-part="bun-bottom">
<rect x="32" y="84" width="56" height="12" rx="6" fill="#E8A33D"/>
</g>
<g class="part" data-part="patty">
<rect x="30" y="70" width="60" height="12" rx="6" fill="#8B5A2B"/>
</g>
<g class="part" data-part="lettuce">
<path d="M26 56 H94 V64 q-5.6 7 -11.3 0 q-5.6 7 -11.3 0 q-5.6 7 -11.3 0 q-5.6 7 -11.3 0 q-5.6 7 -11.3 0 q-5.6 7 -11.3 0 Z" fill="#7BC86C"/>
</g>
<g class="part" data-part="bun-top">
<path d="M28 58 Q28 28 60 28 Q92 28 92 58 Z" fill="#E8A33D"/>
</g>
<!-- ゴマ(bun-top着地後に出現) -->
<g class="sesame" fill="#FFF7E6">
<ellipse cx="48" cy="48" rx="2.4" ry="1.5" transform="rotate(-20 48 48)"/>
<ellipse cx="60" cy="42" rx="2.4" ry="1.5"/>
<ellipse cx="72" cy="48" rx="2.4" ry="1.5" transform="rotate(20 72 48)"/>
<ellipse cx="54" cy="53" rx="2.4" ry="1.5" transform="rotate(12 54 53)"/>
<ellipse cx="66" cy="53" rx="2.4" ry="1.5" transform="rotate(-12 66 53)"/>
<ellipse cx="60" cy="38" rx="2.4" ry="1.5"/>
</g>
</svg>
</div>
<p class="cap">MENU</p>
</div>
【CSS】
:root{
--ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
--ease-inout: cubic-bezier(0.65, 0, 0.35, 1);
}
* { box-sizing: border-box; }
html, body { margin: 0; width: 100%; min-height: 100%; background: #F7F5F0; }
.stage {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100vh;
min-height: 240px;
max-height: 100%;
background: #F7F5F0;
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
}
.ham { width: 160px; height: 160px; cursor: pointer; outline: none; }
.ham svg { width: 100%; height: 100%; display: block; overflow: visible; }
/* 各食材:scaleY squash を下端基準で。初期は非表示(落下前) */
.part {
transform-box: fill-box;
transform-origin: center bottom;
opacity: 0;
}
.sesame { opacity: 0; }
.lines { opacity: 1; }
.cap {
margin: 14px 0 0;
font-size: 12px; font-weight: 700; letter-spacing: .28em;
color: #1A1A1A;
}
【JavaScript】
// 開閉を WAAPI で精密制御。線→×のモーフはせず「線フェード+食材の落下入替」方式。
(() => {
const ham = document.querySelector('[data-ham]');
if (!ham) return; // null安全
const lines = ham.querySelector('.lines');
const sesame = ham.querySelector('.sesame');
// document順 = 落下順:bun-bottom → patty → lettuce → bun-top
const parts = [...ham.querySelectorAll('.part')];
const SPRINGY = 'cubic-bezier(0.16, 1, 0.3, 1)';
// 落下+着地で scaleY 0.88 に潰して戻す squash
const drop = [
{ transform: 'translateY(-80px) scaleY(1)', opacity: 0, offset: 0 },
{ opacity: 1, offset: 0.5 },
{ transform: 'translateY(0px) scaleY(0.88)', offset: 0.72 },
{ transform: 'translateY(0px) scaleY(1)', opacity: 1, offset: 1 }
];
const rise = [
{ transform: 'translateY(0px) scaleY(1)', opacity: 1 },
{ transform: 'translateY(-80px) scaleY(1)', opacity: 0 }
];
let open = false, timer = 0;
const openBurger = () => {
lines.animate([{ opacity: 1 }, { opacity: 0 }], { duration: 150, fill: 'forwards' });
parts.forEach((p, idx) =>
p.animate(drop, { duration: 500, delay: idx * 90, easing: SPRINGY, fill: 'forwards' }));
sesame.animate([{ opacity: 0 }, { opacity: 1 }],
{ duration: 200, delay: parts.length * 90 + 260, fill: 'forwards' });
};
const closeBurger = () => {
const n = parts.length;
sesame.animate([{ opacity: 1 }, { opacity: 0 }], { duration: 120, fill: 'forwards' });
parts.forEach((p, idx) => {
const order = n - 1 - idx; // bun-top(最後の要素)が最初に離脱
p.animate(rise, { duration: 300, delay: order * 60, easing: 'cubic-bezier(0.4,0,1,1)', fill: 'forwards' });
});
lines.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 200, delay: n * 60 + 120, fill: 'forwards' });
};
const toggle = () => { open = !open; open ? openBurger() : closeBurger(); };
const schedule = () => { timer = setInterval(toggle, 2800); };
// クリック/キーで切替(自動タイマーはリセット)
const manual = () => { clearInterval(timer); toggle(); schedule(); };
ham.addEventListener('click', manual);
ham.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); manual(); } });
schedule();
})();
# 外部ライブラリ
なし(追加ライブラリ不要)
# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。