06Fullscreen overlay where the burger folds into an X
The three lines swing apart and fold into an X, and links climb onto the sheet that covers the whole screen one by one. While the sheet is open the keyboard focus stays inside it.
Best forPrimary menus on mobile and portfolio sites
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">Studio</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="Main menu">
<a class="fm__link" style="--i: 0" href="#">Work
<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="#">About
<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="#">Log
<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="#">Contact
<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 Close — 초점은 버튼으로
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 Fullscreen overlay where the burger folds into an X — Measured area 24.215% · moving frames 15/23Original by GODRICH · SCSS and React ship in the zip · 9 mega menu ui patterns, link list to site map
The post this demo lives in
9 mega menu ui patterns, link list to site mapA mega menu ui opens a screen-wide panel from one top-row item, turning the site into one readable map. The nine running See all demos in this post
If you like this effect - similar demos
Transitions04 Center-lift scale toggle — when the middle tab's selection value turns on, only that spot's height value grows above the backgroundCSS Bottom Navigation Bar: 9 Transition Patterns
Transitions05 The Rate Where Flicker Fuses Into MotionFlipbook Animation: 9 Canvas Page Flip Scenes
Transitions05 Fullscreen overlay menu9 CSS Hamburger Menu Animations, No Library