モーフィング・メニュー
≡アイコンが弾性変形して×になり、メニュー項目が下から時間差でポップ表示されるハンバーガーメニュー。モバイルナビの開閉に使えます。
ライブデモ
コード
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で提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。