JSFiddle - React, Tailwind, and code Playground
by HDL52
HTML
<script src="https://unpkg.co/gsap@3/dist/gsap.min.js"></script>
<div class="wrapper">
<button
id="toggle"
class="hamburger"
type="button"
aria-label="打开菜单"
aria-controls="nav"
aria-expanded="false"
>
<span class="hamburgerLine"></span>
</button>
<nav id="nav" class="nav" aria-hidden="true">
<ul id="menu" class="menu">
<li>Menu 01</li>
<li>Menu 02</li>
<li>Mnue 03</li>
</ul>
</nav>
<main id="main" class="main">
<p>主要内容</p>
<p>主要内容</p>
<p>主要内容</p>
<p>主要内容</p>
<p>主要内容</p>
</main>
<svg
class="overlay"
width="100%"
height="100%"
viewBox="0 0 100 100"
preserveAspectRatio="none"
>
<path
id="overlayPath"
class="overlayPath"
vector-effect="non-scaling-stroke"
d="M 0 100 V 100 Q 50 100 100 100 V 100 z"
/>
</svg>
</div>
SCSS
body {
background-color: #cdbea5;
color: #000;
font-size: 2rem;
}
.wrapper {
display: flex;
flex-direction: column;
overflow: hidden;
}
.hamburger {
position: fixed;
top: 24px;
right: 24px;
z-index: 20;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
width: 80px;
height: 80px;
border: none;
border-radius: 100vmax;
background-color: black;
cursor: pointer;
}
.hamburger::before,
.hamburger::after,
.hamburger .hamburgerLine {
position: absolute;
display: block;
width: 50%;
height: 4px;
border-radius: 100vmax;
background-color: white;
}
.hamburger::before,
.hamburger::after {
content: "";
}
.hamburger::before {
transform: translateY(-16px);
}
.hamburger::after {
transform: translateY(16px);
}
.hamburger[aria-expanded=true] .hamburgerLine {
opacity: 0;
}
.hamburger[aria-expanded=true]::before {
transform: rotate(30deg);
}
.hamburger[aria-expanded=true]::after {
transform: rotate(-30deg);
}
.nav {
position: fixed;
top: 0;
left: 0;
z-index: 10;
width: 100%;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.nav[aria-hidden=true] {
visibility: hidden;
opacity: 0;
pointer-events: none;
}
.nav[aria-hidden=false] {
visibility: visible;
opacity: 1;
pointer-events: auto;
}
.menu {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2em;
width: 100%;
height: 100%;
background: #b46e41;
color: white;
}
.main {
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-evenly;
height: 200vh;
}
.overlay {
position: fixed;
top: 0;
left: 0;
z-index: 100;
width: 100%;
height: 100vh;
pointer-events: none;
}
JavaScript
const CONTENTS = {
MAIN: document.getElementById("main"),
FOOTER: document.getElementById("footer"),
}
const NAV = document.getElementById("nav")
const MENU = document.getElementById("menu")
const TOGGLE = document.getElementById("toggle")
const OVERLAYPATH = document.getElementById("overlayPath")
let isAnimating = false
function menuOpen() {
if (isAnimating) return
isAnimating = true
gsap
.timeline({
onStart: () => {
NAV.setAttribute("aria-hidden", "false")
TOGGLE.setAttribute("aria-label", "打开菜单")
gsap.to(TOGGLE, {
autoAlpha: 0,
scale: 0,
duration: 0.1,
})
gsap.set(MENU, {
y: -100,
autoAlpha: 0,
})
},
onComplete: () => {
isAnimating = false
},
})
.set(OVERLAYPATH, {
attr: {
d: "M 0 0 V 0 Q 50 0 100 0 V 0 z",
},
})
.to(
OVERLAYPATH,
{
attr: {
d: "M 0 0 V 50 Q 50 100 100 50 V 0 z",
},
ease: "power4.in",
duration: 0.5,
},
0,
)
.to(OVERLAYPATH, {
attr: {
d: "M 0 0 V 100 Q 50 100 100 100 V 0 z",
},
ease: "power2",
duration: 0.3,
})
.to(
[CONTENTS.MAIN, CONTENTS.FOOTER],
{
y: 100,
opacity: 0,
duration: 0.3,
ease: "power3.in",
},
0.1,
)
.set(OVERLAYPATH, {
attr: {
d: "M 0 100 V 0 Q 50 0 100 0 V 100 z",
},
})
.to(OVERLAYPATH, {
attr: {
d: "M 0 100 V 50 Q 50 100 100 50 V 100 z",
},
duration: 0.3,
ease: "power2.in",
})
.to(OVERLAYPATH, {
attr: {
d: "M 0 100 V 100 Q 50 100 100 100 V 100 z",
},
duration: 0.3,
ease: "power4",
})
.to(
MENU,
{
y: 0,
autoAlpha: 1,
duration: 0.5,
ease: "power4",
onStart: () => {
TOGGLE.setAttribute("aria-expanded",...