ハンバーガー組み上げメニュー

3本線のハンバーガーアイコンを開くと、バンズ・レタス・パティが上から落ちてきて本物のハンバーガーが組み上がります。閉じると逆再生。ただの3本線に遊び心を加える、ナビゲーションの一捻りです。

#hamburger-menu#navigation#playful

ライブデモ

使用例(お題: カフェ 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で提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。