スライドしながら出てくるメニュー
クリックGSAPリスト
メニューを開くと左からアイテムが出現してくるアニメーションです。
実装のポイント
動きはスライドしながら入ってくるリストを応用したものですが、今回は動きの実装にGSAPというアニメーションライブラリを使用しています。
いくつかの要素の動きを統合するようなアニメーションにおいてはGSAPは非常に強力なツールです。以前はライセンスが少し複雑で使用場面が限られるところもありましたが、ライセンスが大きく緩和されて特定の用途以外では自由に使えるようになりました。
ここではGSAPそのものの使い方には言及しませんが、GSAPのtimeline
機能を使ってメニューのコンテナー.menuContainer
要素の開閉とアイテム.sampleItem
要素の出現が協調的に動くようなアニメーションにしています。
コード例
HTML
<div class="menuContainer">
<button class="menuButton">
<img src="/assets/icon_more.svg" alt="" width="24" height="24" />
</button>
<ul class="slideInMenu">
<li class="sampleItem">
<img
class="menuIcon"
src="/assets/icon_home.svg"
alt=""
width="24"
height="24"
/>
<p class="iconName">Home</p>
</li>
<li class="sampleItem">
<img
class="menuIcon"
src="/assets/icon_apps.svg"
alt=""
width="24"
height="24"
/>
<p class="iconName">Apps</p>
</li>
<li class="sampleItem">
<img
class="menuIcon"
src="/assets/icon_camera.svg"
alt=""
width="24"
height="24"
/>
<p class="iconName">Camera</p>
</li>
<li class="sampleItem">
<img
class="menuIcon"
src="/assets/icon_settings.svg"
alt=""
width="24"
height="24"
/>
<p class="iconName">Settings</p>
</li>
</ul>
</div>
CSS
:root{
}
.slideInMenu {
display: flex;
list-style: none;
gap: 12px;
padding-right: 16px;
}
.menuContainer {
position: relative;
border: 2px solid var(--color-text);
border-radius: 32px;
width: 64px;
height: 64px;
padding: 8px 8px 8px 52px;
overflow: hidden;
}
.menuButton {
position: absolute;
top: 50%;
left: 6px;
margin-top: -24px;
display: flex;
align-items: center;
justify-content: center;
width: 48px;
height: 48px;
border-radius: 50%;
z-index: 1;
}
.sampleItem {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 48px;
height: 48px;
opacity: 0;
}
.menuIcon {
width: 24px;
height: 24px;
display: block;
}
.iconName {
font-size: 12px;
}
JavaScript
import { gsap } from "gsap";
const menuButton = document.querySelector(".menuButton");
const menuContainer = document.querySelector(".menuContainer");
const openTimeline = gsap.timeline({
paused: true,
onComplete: () => {
isAnimating = false;
},
});
const closeTimeline = gsap.timeline({
paused: true,
onComplete: () => {
isAnimating = false;
},
});
let isAnimating = false;
openTimeline
.to(".menuContainer", {
width: "auto",
padding: "8px 8px 8px 52px",
duration: 0.8,
ease: "power4.out",
})
.fromTo(
".sampleItem",
{
opacity: 0,
xPercent: -80,
},
{
opacity: 1,
duration: 0.8,
ease: "power3.out",
xPercent: 0,
stagger: 0.07,
},
"<+=0.15",
);
closeTimeline
.to(".sampleItem", {
opacity: 0,
xPercent: -40,
duration: 0.4,
ease: "power4.out",
})
.to(
".menuContainer",
{
width: "64px",
padding: "8px",
duration: 0.6,
ease: "power3.out",
},
"<+=0.05",
);
menuButton.addEventListener("click", () => {
if (isAnimating) {
return;
}
isAnimating = true;
menuContainer.classList.toggle("isActive");
if (menuContainer.classList.contains("isActive")) {
openTimeline.restart();
} else {
closeTimeline.restart();
}
});