06ハンバーガーがXに折れるフルスクリーンオーバーレイ
3本の線が開いてX字に折りたたまれ、画面全体を覆う板の上へリンクが1つずつ押し上がります。板が開いている間、キーボードの焦点は板の中にだけ留まります。
使いどころモバイルやポートフォリオのメインメニュー
rotatetranslateYaria-modal

<main class="stage">
<div class="fm is-demo">
<div class="fm__page" aria-hidden="true">
<i class="fm__block fm__block--a"></i>
<i class="fm__block fm__block--b"></i>
<i class="fm__rule"></i>
<i class="fm__rule fm__rule--s"></i>
<i class="fm__rule fm__rule--m"></i>
</div>
<header class="fm__bar">
<span class="fm__brand">工房</span>
<button class="fm__btn" type="button" aria-controls="fm-menu">
<span class="fm__burger" aria-hidden="true"><span></span><span></span><span></span></span>
</button>
</header>
<div class="fm__overlay" id="fm-menu" role="dialog" aria-modal="true" aria-label="メインメニュー">
<a class="fm__link" style="--i: 0" href="#">作品
<svg viewBox="0 0 256 256" width="18" height="18" aria-hidden="true"><g fill="currentColor"><path d="M192 64v104L88 64Z" opacity=".2"/><path d="M192 56H88a8 8 0 0 0-5.66 13.66L128.69 116l-70.35 70.34a8 8 0 0 0 11.32 11.32L140 127.31l46.34 46.35A8 8 0 0 0 200 168V64a8 8 0 0 0-8-8m-8 92.69l-38.34-38.34L107.31 72H184Z"/></g></svg>
</a>
<a class="fm__link" style="--i: 1" href="#">紹介
<svg viewBox="0 0 256 256" width="18" height="18" aria-hidden="true"><g fill="currentColor"><path d="M192 64v104L88 64Z" opacity=".2"/><path d="M192 56H88a8 8 0 0 0-5.66 13.66L128.69 116l-70.35 70.34a8 8 0 0 0 11.32 11.32L140 127.31l46.34 46.35A8 8 0 0 0 200 168V64a8 8 0 0 0-8-8m-8 92.69l-38.34-38.34L107.31 72H184Z"/></g></svg>
</a>
<a class="fm__link" style="--i: 2" href="#">記録
<svg viewBox="0 0 256 256" width="18" height="18" aria-hidden="true"><g fill="currentColor"><path d="M192 64v104L88 64Z" opacity=".2"/><path d="M192 56H88a8 8 0 0 0-5.66 13.66L128.69 116l-70.35 70.34a8 8 0 0 0 11.32 11.32L140 127.31l46.34 46.35A8 8 0 0 0 200 168V64a8 8 0 0 0-8-8m-8 92.69l-38.34-38.34L107.31 72H184Z"/></g></svg>
</a>
<a class="fm__link" style="--i: 3" href="#">お問い合わせ
<svg viewBox="0 0 256 256" width="18" height="18" aria-hidden="true"><g fill="currentColor"><path d="M192 64v104L88 64Z" opacity=".2"/><path d="M192 56H88a8 8 0 0 0-5.66 13.66L128.69 116l-70.35 70.34a8 8 0 0 0 11.32 11.32L140 127.31l46.34 46.35A8 8 0 0 0 200 168V64a8 8 0 0 0-8-8m-8 92.69l-38.34-38.34L107.31 72H184Z"/></g></svg>
</a>
</div>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-loop; // 격자 미리보기 루프 한 바퀴 2초
$easing: $ease-pop; // 햄버거→X 모프와 오버레이 등장의 탄성
$color: $subject-cream; // 잉크 무대 위 브랜드·막대·큰 링크
@include stage($stage-ink);
.fm, .fm * { box-sizing: border-box; }
.fm {
position: relative;
width: min(440px, 94vw);
height: clamp(168px, 84vh, 264px);
overflow: hidden;
border-radius: $r-card;
background: rgba(255, 247, 230, .04);
}
// ── 평소 화면: 상단 바 + 텍스트 없는 장식 조각 ──
.fm__bar {
position: relative;
z-index: 3;
display: flex;
align-items: center;
justify-content: space-between;
padding: $sp-3 $sp-4;
}
.fm__brand { color: $color; font-size: 16px; line-height: 22px; font-weight: 800; transition: opacity 200ms linear; }
.fm.is-open .fm__brand { opacity: 0; } // 열렸을 때는 X 닫기 버튼만 남는다
.fm__btn {
display: grid;
place-items: center;
width: 40px;
height: 40px;
margin: 0;
padding: 0;
border: 0;
border-radius: $r-control;
background: rgba(255, 247, 230, .1);
cursor: pointer;
}
.fm__btn:focus-visible { outline: 2px solid $color; outline-offset: 2px; }
// 햄버거는 아이콘이 아니라 막대 3개 span — 그 자체가 X 로 모프한다
.fm__burger { display: grid; gap: $sp-1; }
.fm__burger span {
width: 20px;
height: 2px;
border-radius: $r-pill;
background: $color;
transition: transform 300ms $ease-pop, opacity 160ms linear;
}
.fm.is-open .fm__burger span:nth-child(1) { transform: translateY(6px) rotate(45deg); }
.fm.is-open .fm__burger span:nth-child(2) { transform: scaleX(0); opacity: 0; }
.fm.is-open .fm__burger span:nth-child(3) { transform: translateY(-6px) rotate(-45deg); }
// 페이지 장식 — 블록 두 개와 회색 막대 세 줄
.fm__page { display: flex; flex-direction: column; gap: $sp-2; padding: 0 $sp-4 $sp-3; }
.fm__block { border-radius: $r-sm; background: rgba(255, 247, 230, .1); }
.fm__block--a { height: clamp(40px, 20vh, 84px); }
.fm__block--b { height: clamp(20px, 10vh, 44px); background: rgba(255, 247, 230, .06); }
.fm__rule { height: 8px; border-radius: $r-xs; background: rgba(255, 247, 230, .14); }
.fm__rule--s { width: 64%; }
.fm__rule--m { width: 44%; }
// ── 풀스크린 오버레이 ── 겹침 크로스페이드가 아니라 새 레이어 등장(scale 1.06→1 + opacity).
// 닫힌 상태는 visibility: hidden + inert(JS) 로 포커스·탐색에서도 밖으로 내보낸다.
.fm__overlay {
position: absolute;
inset: 0;
z-index: 2;
display: flex;
flex-direction: column;
justify-content: center;
gap: $sp-3;
padding: 0 $sp-8;
background: #241f2d; // 잉크 무대보다 살짝 밝은 오버레이
visibility: hidden;
opacity: 0;
transform: scale(1.06);
transition: opacity 260ms $ease-out, transform 320ms $ease-pop, visibility 0s linear 320ms;
}
.fm.is-open .fm__overlay {
visibility: visible;
opacity: 1;
transform: scale(1);
transition: opacity 260ms $ease-out, transform 320ms $ease-pop, visibility 0s;
}
.fm__link {
display: inline-flex;
align-items: center;
gap: $sp-2;
color: $color;
font-size: 24px;
line-height: 32px;
font-weight: 800;
text-decoration: none;
opacity: 0;
transform: translateY(18px);
transition: opacity 260ms $ease-out, transform 300ms $ease-pop;
}
.fm__link svg { opacity: .6; }
.fm__link:focus-visible { outline: 2px solid $color; outline-offset: 4px; border-radius: $r-xs; }
// 열릴 때만 시차 등장(--i × 80ms). 닫힘은 지연 없이 한꺼번에 — 퇴장은 짧게.
.fm.is-open .fm__link { opacity: 1; transform: translateY(0); transition-delay: calc(var(--i) * 80ms); }
// ── 격자 미리보기 루프 ──
// 0~7% 닫힘 → 8% 모프+오버레이 등장 → 12~48% 링크 시차 등장(등장이 주인공) →
// 72% 유지 → 76% 닫힘(짧게, 막대 역모프) → 84%부터 닫힌 채 대기.
.fm.is-demo .fm__overlay {
visibility: visible;
animation-name: fm-ov;
animation-duration: $duration;
animation-timing-function: $easing;
animation-iteration-count: infinite;
}
.fm.is-demo .fm__burger span,
.fm.is-demo .fm__link {
animation-duration: $duration;
animation-timing-function: $easing;
animation-iteration-count: infinite;
}
.fm.is-demo .fm__burger span:nth-child(1) { animation-name: fm-b1; }
.fm.is-demo .fm__burger span:nth-child(2) { animation-name: fm-b2; }
.fm.is-demo .fm__burger span:nth-child(3) { animation-name: fm-b3; }
.fm.is-demo .fm__link:nth-child(1) { animation-name: fm-l1; }
.fm.is-demo .fm__link:nth-child(2) { animation-name: fm-l2; }
.fm.is-demo .fm__link:nth-child(3) { animation-name: fm-l3; }
.fm.is-demo .fm__link:nth-child(4) { animation-name: fm-l4; }
@keyframes fm-ov {
0%, 8.3% { opacity: 0; transform: scale(1.06); }
8.4%, 74% { opacity: 1; transform: scale(1); }
80%, 100% { opacity: 0; transform: scale(1); }
}
@keyframes fm-b1 {
0%, 8.3% { transform: translateY(0) rotate(0deg); }
18%, 78% { transform: translateY(6px) rotate(45deg); }
88%, 100% { transform: translateY(0) rotate(0deg); }
}
@keyframes fm-b2 {
0%, 8.3% { opacity: 1; transform: scaleX(1); }
18%, 78% { opacity: 0; transform: scaleX(0); }
88%, 100% { opacity: 1; transform: scaleX(1); }
}
@keyframes fm-b3 {
0%, 8.3% { transform: translateY(0) rotate(0deg); }
18%, 78% { transform: translateY(-6px) rotate(-45deg); }
88%, 100% { transform: translateY(0) rotate(0deg); }
}
@keyframes fm-l1 { 0%, 12% { opacity: 0; transform: translateY(18px); } 24%, 100% { opacity: 1; transform: translateY(0); } }
@keyframes fm-l2 { 0%, 20% { opacity: 0; transform: translateY(18px); } 32%, 100% { opacity: 1; transform: translateY(0); } }
@keyframes fm-l3 { 0%, 28% { opacity: 0; transform: translateY(18px); } 40%, 100% { opacity: 1; transform: translateY(0); } }
@keyframes fm-l4 { 0%, 36% { opacity: 0; transform: translateY(18px); } 48%, 100% { opacity: 1; transform: translateY(0); } }
@media (max-width: 340px) {
.fm__bar { padding: $sp-2 $sp-3; }
.fm__brand { font-size: 14px; line-height: 20px; }
.fm__btn { width: 36px; height: 36px; }
.fm__page { gap: $sp-1; padding: 0 $sp-3 $sp-2; }
.fm__rule { height: 6px; }
.fm__overlay { gap: $sp-2; padding: 0 $sp-5; }
.fm__link { font-size: 19px; line-height: 26px; }
}
@media (prefers-reduced-motion: reduce) {
.fm.is-demo .fm__overlay,
.fm.is-demo .fm__burger span,
.fm.is-demo .fm__link { animation: none !important; }
// 정적 상태에서 주인공(열린 메뉴)이 보이도록 연 채 둔다
.fm.is-demo .fm__overlay { visibility: visible !important; opacity: 1 !important; transform: none !important; }
.fm.is-demo .fm__link { opacity: 1 !important; transform: none !important; }
.fm.is-demo .fm__burger span:nth-child(1) { transform: translateY(6px) rotate(45deg) !important; }
.fm.is-demo .fm__burger span:nth-child(2) { opacity: 0 !important; transform: scaleX(0) !important; }
.fm.is-demo .fm__burger span:nth-child(3) { transform: translateY(-6px) rotate(-45deg) !important; }
.fm.is-demo .fm__brand { opacity: 0 !important; }
.fm__burger span, .fm__overlay, .fm__link, .fm__brand { transition: none !important; transition-delay: 0ms !important; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.fm, .fm * {
box-sizing: border-box;
}
.fm {
position: relative;
width: min(440px, 94vw);
height: clamp(168px, 84vh, 264px);
overflow: hidden;
border-radius: 16px;
background: rgba(255, 247, 230, 0.04);
}
.fm__bar {
position: relative;
z-index: 3;
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
}
.fm__brand {
color: #fff7e6;
font-size: 16px;
line-height: 22px;
font-weight: 800;
transition: opacity 200ms linear;
}
.fm.is-open .fm__brand {
opacity: 0;
}
.fm__btn {
display: grid;
place-items: center;
width: 40px;
height: 40px;
margin: 0;
padding: 0;
border: 0;
border-radius: 12px;
background: rgba(255, 247, 230, 0.1);
cursor: pointer;
}
.fm__btn:focus-visible {
outline: 2px solid #fff7e6;
outline-offset: 2px;
}
.fm__burger {
display: grid;
gap: 4px;
}
.fm__burger span {
width: 20px;
height: 2px;
border-radius: 999px;
background: #fff7e6;
transition: transform 300ms cubic-bezier(0.2, 1.4, 0.4, 1), opacity 160ms linear;
}
.fm.is-open .fm__burger span:nth-child(1) {
transform: translateY(6px) rotate(45deg);
}
.fm.is-open .fm__burger span:nth-child(2) {
transform: scaleX(0);
opacity: 0;
}
.fm.is-open .fm__burger span:nth-child(3) {
transform: translateY(-6px) rotate(-45deg);
}
.fm__page {
display: flex;
flex-direction: column;
gap: 8px;
padding: 0 16px 12px;
}
.fm__block {
border-radius: 8px;
background: rgba(255, 247, 230, 0.1);
}
.fm__block--a {
height: clamp(40px, 20vh, 84px);
}
.fm__block--b {
height: clamp(20px, 10vh, 44px);
background: rgba(255, 247, 230, 0.06);
}
.fm__rule {
height: 8px;
border-radius: 4px;
background: rgba(255, 247, 230, 0.14);
}
.fm__rule--s {
width: 64%;
}
.fm__rule--m {
width: 44%;
}
.fm__overlay {
position: absolute;
inset: 0;
z-index: 2;
display: flex;
flex-direction: column;
justify-content: center;
gap: 12px;
padding: 0 32px;
background: #241f2d;
visibility: hidden;
opacity: 0;
transform: scale(1.06);
transition: opacity 260ms ease-out, transform 320ms cubic-bezier(0.2, 1.4, 0.4, 1), visibility 0s linear 320ms;
}
.fm.is-open .fm__overlay {
visibility: visible;
opacity: 1;
transform: scale(1);
transition: opacity 260ms ease-out, transform 320ms cubic-bezier(0.2, 1.4, 0.4, 1), visibility 0s;
}
.fm__link {
display: inline-flex;
align-items: center;
gap: 8px;
color: #fff7e6;
font-size: 24px;
line-height: 32px;
font-weight: 800;
text-decoration: none;
opacity: 0;
transform: translateY(18px);
transition: opacity 260ms ease-out, transform 300ms cubic-bezier(0.2, 1.4, 0.4, 1);
}
.fm__link svg {
opacity: 0.6;
}
.fm__link:focus-visible {
outline: 2px solid #fff7e6;
outline-offset: 4px;
border-radius: 4px;
}
.fm.is-open .fm__link {
opacity: 1;
transform: translateY(0);
transition-delay: calc(var(--i) * 80ms);
}
.fm.is-demo .fm__overlay {
visibility: visible;
animation-name: fm-ov;
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 1.4, 0.4, 1);
animation-iteration-count: infinite;
}
.fm.is-demo .fm__burger span,
.fm.is-demo .fm__link {
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 1.4, 0.4, 1);
animation-iteration-count: infinite;
}
.fm.is-demo .fm__burger span:nth-child(1) {
animation-name: fm-b1;
}
.fm.is-demo .fm__burger span:nth-child(2) {
animation-name: fm-b2;
}
.fm.is-demo .fm__burger span:nth-child(3) {
animation-name: fm-b3;
}
.fm.is-demo .fm__link:nth-child(1) {
animation-name: fm-l1;
}
.fm.is-demo .fm__link:nth-child(2) {
animation-name: fm-l2;
}
.fm.is-demo .fm__link:nth-child(3) {
animation-name: fm-l3;
}
.fm.is-demo .fm__link:nth-child(4) {
animation-name: fm-l4;
}
@keyframes fm-ov {
0%, 8.3% {
opacity: 0;
transform: scale(1.06);
}
8.4%, 74% {
opacity: 1;
transform: scale(1);
}
80%, 100% {
opacity: 0;
transform: scale(1);
}
}
@keyframes fm-b1 {
0%, 8.3% {
transform: translateY(0) rotate(0deg);
}
18%, 78% {
transform: translateY(6px) rotate(45deg);
}
88%, 100% {
transform: translateY(0) rotate(0deg);
}
}
@keyframes fm-b2 {
0%, 8.3% {
opacity: 1;
transform: scaleX(1);
}
18%, 78% {
opacity: 0;
transform: scaleX(0);
}
88%, 100% {
opacity: 1;
transform: scaleX(1);
}
}
@keyframes fm-b3 {
0%, 8.3% {
transform: translateY(0) rotate(0deg);
}
18%, 78% {
transform: translateY(-6px) rotate(-45deg);
}
88%, 100% {
transform: translateY(0) rotate(0deg);
}
}
@keyframes fm-l1 {
0%, 12% {
opacity: 0;
transform: translateY(18px);
}
24%, 100% {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fm-l2 {
0%, 20% {
opacity: 0;
transform: translateY(18px);
}
32%, 100% {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fm-l3 {
0%, 28% {
opacity: 0;
transform: translateY(18px);
}
40%, 100% {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fm-l4 {
0%, 36% {
opacity: 0;
transform: translateY(18px);
}
48%, 100% {
opacity: 1;
transform: translateY(0);
}
}
@media (max-width: 340px) {
.fm__bar {
padding: 8px 12px;
}
.fm__brand {
font-size: 14px;
line-height: 20px;
}
.fm__btn {
width: 36px;
height: 36px;
}
.fm__page {
gap: 4px;
padding: 0 12px 8px;
}
.fm__rule {
height: 6px;
}
.fm__overlay {
gap: 8px;
padding: 0 20px;
}
.fm__link {
font-size: 19px;
line-height: 26px;
}
}
@media (prefers-reduced-motion: reduce) {
.fm.is-demo .fm__overlay,
.fm.is-demo .fm__burger span,
.fm.is-demo .fm__link {
animation: none !important;
}
.fm.is-demo .fm__overlay {
visibility: visible !important;
opacity: 1 !important;
transform: none !important;
}
.fm.is-demo .fm__link {
opacity: 1 !important;
transform: none !important;
}
.fm.is-demo .fm__burger span:nth-child(1) {
transform: translateY(6px) rotate(45deg) !important;
}
.fm.is-demo .fm__burger span:nth-child(2) {
opacity: 0 !important;
transform: scaleX(0) !important;
}
.fm.is-demo .fm__burger span:nth-child(3) {
transform: translateY(-6px) rotate(-45deg) !important;
}
.fm.is-demo .fm__brand {
opacity: 0 !important;
}
.fm__burger span, .fm__overlay, .fm__link, .fm__brand {
transition: none !important;
transition-delay: 0ms !important;
}
}// 실제 동작: 버튼 하나가 열기/닫기를 겸한다 — 햄버거 막대가 X 로 모프한다.
var fm = document.querySelector('.fm');
var btn = fm.querySelector('.fm__btn');
var ov = fm.querySelector('.fm__overlay');
var links = Array.prototype.slice.call(ov.querySelectorAll('.fm__link'));
btn.setAttribute('aria-haspopup', 'true');
btn.setAttribute('aria-expanded', 'false');
ov.setAttribute('inert', '');
function stopDemo() { fm.classList.remove('is-demo'); } // 사람이 만졌으니 자동 루프를 끈다
fm.addEventListener('pointerdown', stopDemo);
function open() {
fm.classList.add('is-open');
btn.setAttribute('aria-expanded', 'true');
ov.removeAttribute('inert');
btn.focus({ preventScroll: true }); // 같은 버튼이 X 닫기가 된다 — 초점 유지
}
function close() {
fm.classList.remove('is-open');
btn.setAttribute('aria-expanded', 'false');
ov.setAttribute('inert', '');
btn.focus({ preventScroll: true });
}
btn.addEventListener('click', function () {
fm.classList.contains('is-open') ? close() : open();
});
// Tab 이 오버레이 밖으로 나가면 다시 안으로 돌린다(포커스 트랩)
ov.addEventListener('keydown', function (e) {
if (e.key !== 'Tab' || links.length === 0) return;
var first = links[0], last = links[links.length - 1];
if (e.shiftKey && document.activeElement === first) {
e.preventDefault(); last.focus({ preventScroll: true });
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault(); first.focus({ preventScroll: true });
}
});
// Escape 閉じる — 초점은 버튼으로
fm.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && fm.classList.contains('is-open')) close();
});import { useState, useEffect, useRef } from "react";
import type { CSSProperties } from "react";
import styles from "./FullscreenMorph.module.scss";
export type FullscreenMorphProps = {
labels?: { brand?: string; menuLabel?: string; links?: string[] };
/** 격자 미리보기용 2초 자동 루프. 사람이 손대면 꺼진다 */
demo?: boolean;
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
type Box<T> = { current: T | null };
type KeyEvt = { key: string; shiftKey: boolean; preventDefault(): void };
const LINKS = ["Work", "About", "Log", "Contact"];
const ARROW = (
<g fill="currentColor">
<path d="M192 64v104L88 64Z" opacity=".2" />
<path d="M192 56H88a8 8 0 0 0-5.66 13.66L128.69 116l-70.35 70.34a8 8 0 0 0 11.32 11.32L140 127.31l46.34 46.35A8 8 0 0 0 200 168V64a8 8 0 0 0-8-8m-8 92.69l-38.34-38.34L107.31 72H184Z" />
</g>
);
export default function FullscreenMorph({
labels,
demo = true,
duration = 2000,
easing = "cubic-bezier(.2, 1.4, .4, 1)",
color = "#fff7e6",
}: FullscreenMorphProps) {
const brand = labels?.brand ?? "Studio";
const menuLabel = labels?.menuLabel ?? "Main menu";
const links = labels?.links ?? LINKS;
const [live, setLive] = useState(demo);
const [open, setOpen] = useState(false);
const btnRef: Box<HTMLButtonElement> = useRef(null);
const ovRef: Box<HTMLDivElement> = useRef(null);
// 닫힌 오버레이는 포커스·탐색에서도 밖으로 내보낸다(inert)
useEffect(() => {
const ov = ovRef.current;
if (ov === null) return;
if (open) ov.removeAttribute("inert");
else ov.setAttribute("inert", "");
}, [open]);
function toggle() {
setLive(false); // 사람이 만졌으니 자동 루프를 끈다
setOpen(!open);
const b = btnRef.current; // 같은 버튼이 X 닫기가 된다 — 초점 유지
if (b !== null) b.focus({ preventScroll: true });
}
// Escape 닫기 + Tab 이 오버레이 밖으로 나가면 다시 안으로(포커스 트랩)
function keys(e: KeyEvt) {
setLive(false);
if (e.key === "Escape" && open) {
setOpen(false);
const b = btnRef.current;
if (b !== null) b.focus({ preventScroll: true });
return;
}
if (e.key !== "Tab" || !open) return;
const ov = ovRef.current;
if (ov === null) return;
const els = Array.from(ov.querySelectorAll("a")) as HTMLElement[];
if (els.length === 0) return;
const first = els[0];
const last = els[els.length - 1];
const el = document.activeElement;
if (e.shiftKey && el === first) {
e.preventDefault();
last.focus({ preventScroll: true });
} else if (!e.shiftKey && el === last) {
e.preventDefault();
first.focus({ preventScroll: true });
}
}
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
const cls = [styles.fm, live ? styles.isDemo : "", open ? styles.isOpen : ""].filter(Boolean).join(" ");
return (
<section className={cls} style={vars} onPointerDown={() => setLive(false)} onKeyDown={keys}>
<div className={styles.page} aria-hidden="true">
<i className={`${styles.block} ${styles.blockA}`} />
<i className={`${styles.block} ${styles.blockB}`} />
<i className={styles.rule} />
<i className={`${styles.rule} ${styles.ruleS}`} />
<i className={`${styles.rule} ${styles.ruleM}`} />
</div>
<header className={styles.bar}>
<span className={styles.brand}>{brand}</span>
<button
className={styles.btn}
type="button"
aria-haspopup="true"
aria-expanded={open}
aria-controls="fm-menu"
ref={btnRef}
onClick={toggle}
>
<span className={styles.burger} aria-hidden="true"><span /><span /><span /></span>
</button>
</header>
<div
className={styles.overlay}
id="fm-menu"
role="dialog"
aria-modal="true"
aria-label={menuLabel}
ref={ovRef}
>
{links.map((t, i) => (
<a className={styles.link} key={t} href="#" style={{ "--i": i } as CSSProperties}>
{t}
<svg viewBox="0 0 256 256" width="18" height="18" aria-hidden="true">{ARROW}</svg>
</a>
))}
</div>
</section>
);
}06 ハンバーガーがXに折れるフルスクリーンオーバーレイ — 計測 累積面積 24.215% · 動いたフレーム 15/23GODRICH オリジナル · SCSS と React は zip に同梱 · メガメニュー css 9選 — リンクの羅列からサイトの地図へ
このデモが属する記事
メガメニュー css 9選 — リンクの羅列からサイトの地図へメガメニュー css とは、上部ナビの項目ひとつから画面いっぱいの幅のパネルを開き、サイト全体の構造を一目で渡すメニューの作り方のことです。ここに置いた九つは、そのパネルだけでなく、多段展開・スクロールでの縮小・モバイルへの切り替え・検索・ この記事のデモをすべて見る


