DEMO
右上のボタンでメニューを開くと、画面全体を覆う一覧が上から降りてきて、項目が1つずつ現れます。
CODE
<div class="demo-stage fsm-stage">
<div class="fsm-site" id="fsm-site">
<header class="fsm-head">
<span class="fsm-logo">KAPPY</span>
<button type="button" class="fsm-toggle" id="fsm-toggle" aria-expanded="false" aria-controls="fsm-nav" aria-label="メニューを開く"><span class="fsm-lines" aria-hidden="true"><i></i><i></i><i></i></span></button>
</header>
<div class="fsm-body" id="fsm-body">
<h3>フルスクリーンメニュー</h3>
<p>右上のボタンでメニューを開くと、画面全体を覆う一覧が上から降りてきて、項目が1つずつ現れます。</p>
<p class="fsm-status" id="fsm-status" role="status" aria-live="polite"></p>
</div>
<nav class="fsm-nav" id="fsm-nav" aria-label="サイトメニュー">
<ul>
<li><a href="#works">WORKS</a></li>
<li><a href="#about">ABOUT</a></li>
<li><a href="#journal">JOURNAL</a></li>
<li><a href="#contact">CONTACT</a></li>
</ul>
</nav>
</div>
</div>
.fsm-stage { display:flex; align-items:center; justify-content:center; padding:1.5rem; background:#0B0B10; min-height:340px; }
.fsm-site { position:relative; width:100%; max-width:380px; height:300px; border-radius:14px; overflow:hidden; background:#F7F5F2; color:#1A1A1A; }
.fsm-head { position:relative; z-index:3; display:flex; align-items:center; justify-content:space-between; padding:.6rem .6rem .6rem 1rem; }
.fsm-logo { font-family:'Oswald',sans-serif; font-size:.9rem; letter-spacing:.2em; }
.fsm-toggle { width:44px; height:44px; border:0; border-radius:50%; background:transparent; color:inherit; cursor:pointer; }
.fsm-toggle:focus-visible { outline:2px solid #E91B89; outline-offset:2px; }
.fsm-logo, .fsm-toggle { transition: color .2s ease .25s; }
.fsm-site.is-open .fsm-logo, .fsm-site.is-open .fsm-toggle { color:#fff; transition-delay:.15s; }
.fsm-lines { position:relative; display:block; width:20px; height:14px; margin:0 auto; }
.fsm-lines i { position:absolute; left:0; width:100%; height:2px; border-radius:1px; background:currentColor; transition: transform .3s ease, opacity .2s ease; }
.fsm-lines i:nth-child(1) { top:0; }
.fsm-lines i:nth-child(2) { top:6px; }
.fsm-lines i:nth-child(3) { top:12px; }
.fsm-site.is-open .fsm-lines i:nth-child(1) { transform:translateY(6px) rotate(45deg); }
.fsm-site.is-open .fsm-lines i:nth-child(2) { opacity:0; }
.fsm-site.is-open .fsm-lines i:nth-child(3) { transform:translateY(-6px) rotate(-45deg); }
.fsm-body { padding:.4rem 1rem; }
.fsm-body h3 { margin:0 0 .4rem; font-size:1.05rem; }
.fsm-body p { margin:0; color:#5A5A5A; font-size:.82rem; line-height:1.7; }
.fsm-body .fsm-status { margin-top:.6rem; min-height:1.2em; color:#1A1A1A; font-size:.78rem; }
.fsm-nav { position:absolute; inset:0; z-index:2; display:flex; align-items:center; padding:3.5rem 1.4rem 1.4rem; background:#1A1A1A; visibility:hidden; clip-path:inset(0 0 100% 0);
transition: clip-path .45s cubic-bezier(.77,0,.18,1), visibility 0s linear .45s; }
.fsm-site.is-open .fsm-nav { visibility:visible; clip-path:inset(0); transition: clip-path .45s cubic-bezier(.77,0,.18,1), visibility 0s; }
.fsm-nav ul { list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:.2rem; }
.fsm-nav a { display:inline-block; padding:.25rem 0; color:#fff; font-family:'Oswald',sans-serif; font-size:1.7rem; letter-spacing:.08em; text-decoration:none; opacity:0; transform:translateY(16px); transition: opacity .3s ease, transform .3s ease, color .2s ease; }
.fsm-nav a:hover, .fsm-nav a:focus-visible { color:#F472B6; }
.fsm-nav a:focus-visible { outline:2px solid #F472B6; outline-offset:4px; }
.fsm-site.is-open .fsm-nav a { opacity:1; transform:none; transition-delay:calc(.25s + var(--d, 0s)), calc(.25s + var(--d, 0s)), 0s; }
.fsm-nav li:nth-child(2) a { --d:.06s; }
.fsm-nav li:nth-child(3) a { --d:.12s; }
.fsm-nav li:nth-child(4) a { --d:.18s; }
@media (prefers-reduced-motion: reduce) {
.fsm-nav, .fsm-nav a, .fsm-lines i, .fsm-logo, .fsm-toggle { transition-duration:.01ms !important; transition-delay:0s !important; }
}
(function(){
var site = document.getElementById('fsm-site');
var btn = document.getElementById('fsm-toggle');
var nav = document.getElementById('fsm-nav');
var body = document.getElementById('fsm-body');
var status = document.getElementById('fsm-status');
if (!site || !btn || !nav || !body || !status) return;
var links = Array.prototype.slice.call(nav.querySelectorAll('a'));
function set(open){
site.classList.toggle('is-open', open);
btn.setAttribute('aria-expanded', open ? 'true' : 'false');
btn.setAttribute('aria-label', open ? 'メニューを閉じる' : 'メニューを開く');
body.inert = open;
(open ? links[0] : btn).focus({ preventScroll: true });
}
btn.addEventListener('click', function(){ set(!site.classList.contains('is-open')); });
site.addEventListener('mousedown', function(e){
if (site.classList.contains('is-open') && !e.target.closest('a, button')) e.preventDefault();
});
site.addEventListener('keydown', function(e){
if (!site.classList.contains('is-open')) return;
if (e.key === 'Escape') { e.preventDefault(); set(false); return; }
if (e.key !== 'Tab') return;
var last = links[links.length - 1];
if (e.shiftKey && document.activeElement === btn) { e.preventDefault(); last.focus(); }
else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); btn.focus(); }
});
links.forEach(function(a){
a.addEventListener('click', function(e){
e.preventDefault();
set(false);
status.textContent = '「' + a.textContent + '」を選びました(デモのため移動しません)';
});
});
})();
AI PROMPT
メニューボタンを押すと、画面全体を覆う黒い一覧が上から降りてきて、項目が少しずつ遅れて浮かび上がる全画面メニューを作って。メニュー(nav・aria-label 付き)は position:absolute; inset:0 で(デモでは枠の)全体を覆い、閉じている時は clip-path: inset(0 0 100% 0) と visibility:hidden、開いた時は clip-path: inset(0) と visibility:visible にして、clip-path を .45秒・cubic-bezier(.77,0,.18,1) で動かす(visibility は、開く時はすぐ、閉じる時は .45秒遅らせて切り替え、閉じている間は読み上げやフォーカスの対象から外す)。項目のリンクは opacity:0・translateY(16px) から、開いた時に .25秒遅れて浮かび上がらせ、2つ目以降は nth-child で .06秒ずつ遅らせる(遅れは opacity と transform だけに付け、ホバー時の color の変化は遅らせない)。メニューボタンは3本線で、開くと上下の線を transform: translateY(±6px) rotate(±45度) で真ん中へ寄せて×印にし、真ん中の線を消す(top は動かさない)。ボタンはメニューより手前(z-index を高く)に置いて閉じる時にも押せるようにし、ロゴとボタンの文字色は黒い面がヘッダーに届く・離れるのに合わせて遅らせて切り替え(開く時 .15秒・閉じる時 .25秒)、aria-expanded・aria-controls を付け、aria-label を「メニューを開く」「メニューを閉じる」で切り替える。開いたら最初のリンクへフォーカスを移し、後ろの本文は inert にする。Tab と Shift+Tab ではボタンとリンクの間だけを回り、開いている間は、メニューの背景やロゴを押してもフォーカスが外れないよう、リンクとボタン以外の mousedown は preventDefault する。Esc で閉じてボタンへフォーカスを戻す。デモでは枠の中に作るため position:absolute を使う(サイト全体に使う時は画面に固定する)。@media (prefers-reduced-motion: reduce) では transition と遅れを無効化して、すぐに開閉する。 Acceptance criteria: works with prefers-reduced-motion (animation disabled), zero console errors, zero layout shift
AIエージェントでの実装ガイド → Claude Code でWebアニメーションを実装する
BOOKS · PR · AMAZON
※当サイトはAmazonアソシエイトプログラムの参加者です。リンクから商品をご購入いただくと、当方に紹介料が発生します。
学びを止めない · AMAZONサブスク
※当サイトはAmazonアソシエイトプログラムの参加者です。リンクから商品をご購入いただくと、当方に紹介料が発生します。
CREATOR TOOLS · スクール(PR)
※本サイトはアフィリエイトリンクを含みます。リンク経由のご購入・お申し込みで運営者が紹介料を受け取る場合があります。
メニューボタンを押すと、画面全体をメニューの一覧で覆う形のナビゲーションです。項目を大きな文字で見せられるため、ページ数の少ないブランドサイトやポートフォリオでよく使われます。
当サイトの Hamburger Menu は3本線がなめらかに×印へ変形するアイコン、Slide Drawer は画面の右端からスライドして現れるドロワーメニュー、Curtain Reveal は左右のパネルが開いて中身が現れるカーテン演出です。本ページはアイコンの変形と全画面の一覧を組み合わせ、開いている間のフォーカスの扱いまでを含めた別テーマです。
画面全体を覆う分、開いている間は後ろの内容に触れられない状態をきちんと作ることが大切です。閉じるボタンを常に押せる位置に置き、Esc でも閉じられるようにしておくと、どの操作方法でも迷わずに使えます。
閉じている時に visibility:hidden も付けるのはなぜですか?
clip-path で見えなくしただけでは、リンクはページの中に残っていて、Tab キーで見えないリンクへフォーカスが移ったり、読み上げソフトが読んだりしてしまいます。visibility:hidden にすると、見えないだけでなく操作や読み上げの対象からも外れます。閉じる時は clip-path の動きが終わってから切り替わるよう、visibility にだけ遅れを付けています。
開いている間、フォーカスをメニューの中にとどめるのはなぜですか?
全画面のメニューは後ろの本文を完全に覆うため、Tab キーで後ろの見えないリンクへフォーカスが移ると、今どこを操作しているのか分からなくなります。本実装は後ろの本文を inert にして操作の対象から外し、メニューボタンと最後のリンクの間で Tab を回り込ませています。Esc で閉じた時は、メニューボタンへフォーカスを戻します。
コードは無料で使えますか?
はい。Motion LabのコードスニペットとAIプロンプトは、クレジット表記なしで自由に使えます。コピーしてそのままプロジェクトに利用できます。