遊べるUI 中級

モーフィング・メニュー

≡アイコンが弾性変形して×になり、メニュー項目が下から時間差でポップ表示されるハンバーガーメニュー。モバイルナビの開閉に使えます。

#css#js#menu#animation

ライブデモ

コード

HTML
<!-- モーフィング・メニュー: ハンバーガーが弾性変形して×になり、項目が下から時間差でポップ表示 -->
<div class="stage">
  <button class="morph" type="button" aria-haspopup="true" aria-expanded="false" aria-label="メニューを開く">
    <span class="morph__box" aria-hidden="true">
      <span class="morph__line morph__line--top"></span>
      <span class="morph__line morph__line--mid"></span>
      <span class="morph__line morph__line--bot"></span>
    </span>
  </button>
  <nav class="morph-menu" aria-label="メニュー">
    <div class="morph-menu__list">
      <a class="morph-menu__item" href="#" style="--i:0">Home</a>
      <a class="morph-menu__item" href="#" style="--i:1">Works</a>
      <a class="morph-menu__item" href="#" style="--i:2">About</a>
      <a class="morph-menu__item" href="#" style="--i:3">Contact</a>
    </div>
  </nav>
  <p class="hint">クリックしてメニューを開閉</p>
</div>
CSS
* { box-sizing: border-box; }
body {
  margin: 0;
  min-height: 100vh;
  overflow: hidden;
  display: grid;
  place-items: center;
  font-family: "Segoe UI", system-ui, sans-serif;
  background: radial-gradient(circle at 50% 28%, #1b1f3d 0%, #0d0e1a 72%);
  color: #f4f5fb;
}

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

/* ハンバーガーボタン */
.morph {
  --size: 56px;
  width: var(--size);
  height: var(--size);
  border: none;
  border-radius: 18px;
  cursor: pointer;
  display: grid;
  place-items: center;
  background: linear-gradient(145deg, #262b52, #14162b);
  box-shadow: 0 10px 26px rgba(0, 0, 0, .45), inset 0 1px 0 rgba(255, 255, 255, .06);
  animation: morph-breathe 3.4s ease-in-out infinite;
  transition: transform .2s ease;
}
.morph:active { transform: scale(.92); }
.morph:focus-visible { outline: 2px solid #8ec9ff; outline-offset: 3px; }
.stage.is-open .morph {
  animation: none;
  box-shadow: 0 10px 26px rgba(0, 0, 0, .45), 0 0 20px rgba(122, 162, 255, .4);
}

@keyframes morph-breathe {
  0%, 100% { box-shadow: 0 10px 26px rgba(0, 0, 0, .45), 0 0 0 rgba(122, 162, 255, 0); transform: scale(1); }
  50%      { box-shadow: 0 10px 26px rgba(0, 0, 0, .45), 0 0 14px rgba(122, 162, 255, .3); transform: scale(1.02); }
}

.morph__box { position: relative; width: 26px; height: 18px; }

.morph__line {
  position: absolute;
  left: 0;
  width: 100%;
  height: 3px;
  border-radius: 3px;
  background: linear-gradient(90deg, #8ec9ff, #b39dff);
  transition: transform .5s cubic-bezier(.34, 1.56, .64, 1), opacity .3s ease;
}
.morph__line--top { top: 0; }
.morph__line--mid { top: 50%; transform: translateY(-50%); }
.morph__line--bot { bottom: 0; }

.stage.is-open .morph__line--top { transform: translateY(7.5px) rotate(45deg); }
.stage.is-open .morph__line--mid { opacity: 0; transform: translateY(-50%) scaleX(0); }
.stage.is-open .morph__line--bot { transform: translateY(-7.5px) rotate(-45deg); }

/* ポップアップメニュー: grid-template-rows を 0fr→1fr でアコーディオン開閉させる。
   閉時は高さ0でヒント文と重ならず、開時は自然に下の要素を押し下げる(オーバーラップしない)。 */
.morph-menu {
  display: grid;
  grid-template-rows: 0fr;
  width: min(190px, 74vw);
  overflow: hidden;
  transition: grid-template-rows .48s cubic-bezier(.22, 1, .36, 1);
}
.stage.is-open .morph-menu { grid-template-rows: 1fr; }

.morph-menu__list {
  display: flex;
  flex-direction: column;
  gap: 6px;
  min-height: 0;
  padding-top: 14px;
}

.morph-menu__item {
  padding: 9px 16px;
  border-radius: 12px;
  text-align: center;
  text-decoration: none;
  font-weight: 600;
  font-size: 13px;
  letter-spacing: .02em;
  color: #f4f5fb;
  background: rgba(255, 255, 255, .05);
  border: 1px solid rgba(255, 255, 255, .08);
  opacity: 0;
  transform: translateY(12px) scale(.85);
  pointer-events: none;
  transition:
    transform .5s cubic-bezier(.34, 1.56, .64, 1),
    opacity .3s ease,
    background-color .2s ease,
    border-color .2s ease;
  transition-delay: calc(var(--i, 0) * 65ms);
}
.morph-menu__item:hover,
.morph-menu__item:focus-visible {
  background: linear-gradient(90deg, rgba(142, 201, 255, .22), rgba(179, 157, 255, .22));
  border-color: rgba(255, 255, 255, .16);
}

.stage.is-open .morph-menu__item {
  opacity: 1;
  transform: translateY(0) scale(1);
  pointer-events: auto;
}
/* 閉じる時は下から先に消えるよう逆順の時間差をつける */
.stage:not(.is-open) .morph-menu__item {
  transition-delay: calc((3 - var(--i, 0)) * 35ms);
}

.hint {
  margin: 0;
  font-size: 13px;
  color: rgba(244, 245, 251, .5);
}

@media (prefers-reduced-motion: reduce) {
  .morph { animation: none; }
  .morph__line,
  .morph-menu__item { transition-duration: .12s; transition-delay: 0s !important; }
}
JavaScript
// モーフィング・メニュー: クリックで≡が弾性変形して×になり、項目が時間差でポップ表示
(() => {
  const stage = document.querySelector('.stage');
  const btn = document.querySelector('.morph');
  const hint = document.querySelector('.hint');
  if (!stage || !btn) return; // null安全

  let open = false;

  const setOpen = (next) => {
    open = next;
    stage.classList.toggle('is-open', open);
    btn.setAttribute('aria-expanded', String(open));
    btn.setAttribute('aria-label', open ? 'メニューを閉じる' : 'メニューを開く');
    if (hint) hint.textContent = open ? 'もう一度クリックで閉じる' : 'クリックしてメニューを開閉';
  };

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

  // メニュー項目はデモ用のダミーリンクなので遷移させない
  document.querySelectorAll('.morph-menu__item').forEach((a) => {
    a.addEventListener('click', (e) => e.preventDefault());
  });

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

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

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

# 追加してほしい効果
モーフィング・メニュー(遊べるUI)
≡アイコンが弾性変形して×になり、メニュー項目が下から時間差でポップ表示されるハンバーガーメニュー。モバイルナビの開閉に使えます。

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

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- モーフィング・メニュー: ハンバーガーが弾性変形して×になり、項目が下から時間差でポップ表示 -->
<div class="stage">
  <button class="morph" type="button" aria-haspopup="true" aria-expanded="false" aria-label="メニューを開く">
    <span class="morph__box" aria-hidden="true">
      <span class="morph__line morph__line--top"></span>
      <span class="morph__line morph__line--mid"></span>
      <span class="morph__line morph__line--bot"></span>
    </span>
  </button>
  <nav class="morph-menu" aria-label="メニュー">
    <div class="morph-menu__list">
      <a class="morph-menu__item" href="#" style="--i:0">Home</a>
      <a class="morph-menu__item" href="#" style="--i:1">Works</a>
      <a class="morph-menu__item" href="#" style="--i:2">About</a>
      <a class="morph-menu__item" href="#" style="--i:3">Contact</a>
    </div>
  </nav>
  <p class="hint">クリックしてメニューを開閉</p>
</div>

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

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

/* ハンバーガーボタン */
.morph {
  --size: 56px;
  width: var(--size);
  height: var(--size);
  border: none;
  border-radius: 18px;
  cursor: pointer;
  display: grid;
  place-items: center;
  background: linear-gradient(145deg, #262b52, #14162b);
  box-shadow: 0 10px 26px rgba(0, 0, 0, .45), inset 0 1px 0 rgba(255, 255, 255, .06);
  animation: morph-breathe 3.4s ease-in-out infinite;
  transition: transform .2s ease;
}
.morph:active { transform: scale(.92); }
.morph:focus-visible { outline: 2px solid #8ec9ff; outline-offset: 3px; }
.stage.is-open .morph {
  animation: none;
  box-shadow: 0 10px 26px rgba(0, 0, 0, .45), 0 0 20px rgba(122, 162, 255, .4);
}

@keyframes morph-breathe {
  0%, 100% { box-shadow: 0 10px 26px rgba(0, 0, 0, .45), 0 0 0 rgba(122, 162, 255, 0); transform: scale(1); }
  50%      { box-shadow: 0 10px 26px rgba(0, 0, 0, .45), 0 0 14px rgba(122, 162, 255, .3); transform: scale(1.02); }
}

.morph__box { position: relative; width: 26px; height: 18px; }

.morph__line {
  position: absolute;
  left: 0;
  width: 100%;
  height: 3px;
  border-radius: 3px;
  background: linear-gradient(90deg, #8ec9ff, #b39dff);
  transition: transform .5s cubic-bezier(.34, 1.56, .64, 1), opacity .3s ease;
}
.morph__line--top { top: 0; }
.morph__line--mid { top: 50%; transform: translateY(-50%); }
.morph__line--bot { bottom: 0; }

.stage.is-open .morph__line--top { transform: translateY(7.5px) rotate(45deg); }
.stage.is-open .morph__line--mid { opacity: 0; transform: translateY(-50%) scaleX(0); }
.stage.is-open .morph__line--bot { transform: translateY(-7.5px) rotate(-45deg); }

/* ポップアップメニュー: grid-template-rows を 0fr→1fr でアコーディオン開閉させる。
   閉時は高さ0でヒント文と重ならず、開時は自然に下の要素を押し下げる(オーバーラップしない)。 */
.morph-menu {
  display: grid;
  grid-template-rows: 0fr;
  width: min(190px, 74vw);
  overflow: hidden;
  transition: grid-template-rows .48s cubic-bezier(.22, 1, .36, 1);
}
.stage.is-open .morph-menu { grid-template-rows: 1fr; }

.morph-menu__list {
  display: flex;
  flex-direction: column;
  gap: 6px;
  min-height: 0;
  padding-top: 14px;
}

.morph-menu__item {
  padding: 9px 16px;
  border-radius: 12px;
  text-align: center;
  text-decoration: none;
  font-weight: 600;
  font-size: 13px;
  letter-spacing: .02em;
  color: #f4f5fb;
  background: rgba(255, 255, 255, .05);
  border: 1px solid rgba(255, 255, 255, .08);
  opacity: 0;
  transform: translateY(12px) scale(.85);
  pointer-events: none;
  transition:
    transform .5s cubic-bezier(.34, 1.56, .64, 1),
    opacity .3s ease,
    background-color .2s ease,
    border-color .2s ease;
  transition-delay: calc(var(--i, 0) * 65ms);
}
.morph-menu__item:hover,
.morph-menu__item:focus-visible {
  background: linear-gradient(90deg, rgba(142, 201, 255, .22), rgba(179, 157, 255, .22));
  border-color: rgba(255, 255, 255, .16);
}

.stage.is-open .morph-menu__item {
  opacity: 1;
  transform: translateY(0) scale(1);
  pointer-events: auto;
}
/* 閉じる時は下から先に消えるよう逆順の時間差をつける */
.stage:not(.is-open) .morph-menu__item {
  transition-delay: calc((3 - var(--i, 0)) * 35ms);
}

.hint {
  margin: 0;
  font-size: 13px;
  color: rgba(244, 245, 251, .5);
}

@media (prefers-reduced-motion: reduce) {
  .morph { animation: none; }
  .morph__line,
  .morph-menu__item { transition-duration: .12s; transition-delay: 0s !important; }
}

【JavaScript】
// モーフィング・メニュー: クリックで≡が弾性変形して×になり、項目が時間差でポップ表示
(() => {
  const stage = document.querySelector('.stage');
  const btn = document.querySelector('.morph');
  const hint = document.querySelector('.hint');
  if (!stage || !btn) return; // null安全

  let open = false;

  const setOpen = (next) => {
    open = next;
    stage.classList.toggle('is-open', open);
    btn.setAttribute('aria-expanded', String(open));
    btn.setAttribute('aria-label', open ? 'メニューを閉じる' : 'メニューを開く');
    if (hint) hint.textContent = open ? 'もう一度クリックで閉じる' : 'クリックしてメニューを開閉';
  };

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

  // メニュー項目はデモ用のダミーリンクなので遷移させない
  document.querySelectorAll('.morph-menu__item').forEach((a) => {
    a.addEventListener('click', (e) => e.preventDefault());
  });

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

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

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