02サブが右に滑る多段ドロップダウン
第一段のリストで矢印付きの項目に触れると第二段が右から押し出され、階層は開いた段数ぶん深く読まれます。
使いどころカテゴリが2〜3層になるドキュメント・ヘルプサイトのナビ
translateXaria-haspopup:focus-within

<main class="stage">
<div class="mls is-demo" id="root">
<button class="mls__trigger" id="trigger" type="button" aria-controls="l1">ライブラリ
<svg class="mls__caret" viewBox="0 0 256 256" width="16" height="16" aria-hidden="true"><g fill="currentColor"><path d="m208 96l-80 80l-80-80Z" opacity=".2"/><path d="M215.39 92.94A8 8 0 0 0 208 88H48a8 8 0 0 0-5.66 13.66l80 80a8 8 0 0 0 11.32 0l80-80a8 8 0 0 0 1.73-8.72M128 164.69L67.31 104h121.38Z"/></g></svg>
</button>
<nav class="mls__l1" id="l1" aria-label="ライブラリメニュー">
<ul class="mls__u">
<li class="mls__i"><a class="mls__a" href="#">はじめる</a></li>
<li class="mls__i mls__i--parent mls__i--feat">
<button class="mls__b" type="button" aria-controls="l2">機能
<svg class="mls__sub-caret" viewBox="0 0 256 256" width="16" height="16" aria-hidden="true"><g fill="currentColor"><path d="m176 128l-80 80V48Z" opacity=".2"/><path d="m181.66 122.34l-80-80A8 8 0 0 0 88 48v160a8 8 0 0 0 13.66 5.66l80-80a8 8 0 0 0 0-11.32M104 188.69V67.31L164.69 128Z"/></g></svg>
</button>
<ul class="mls__l2" id="l2">
<li class="mls__i"><a class="mls__a" href="#">リストを作る</a></li>
<li class="mls__i mls__i--parent mls__i--share">
<button class="mls__b" type="button" aria-controls="l3">共有
<svg class="mls__sub-caret" viewBox="0 0 256 256" width="16" height="16" aria-hidden="true"><g fill="currentColor"><path d="m176 128l-80 80V48Z" opacity=".2"/><path d="m181.66 122.34l-80-80A8 8 0 0 0 88 48v160a8 8 0 0 0 13.66 5.66l80-80a8 8 0 0 0 0-11.32M104 188.69V67.31L164.69 128Z"/></g></svg>
</button>
<ul class="mls__l3" id="l3">
<li class="mls__i"><a class="mls__a" href="#">リンクをコピー</a></li>
<li class="mls__i"><a class="mls__a" href="#">コードを埋め込む</a></li>
</ul>
</li>
<li class="mls__i"><a class="mls__a" href="#">インポート</a></li>
</ul>
</li>
<li class="mls__i"><a class="mls__a" href="#">料金</a></li>
<li class="mls__i"><a class="mls__a" href="#">お問い合わせ</a></li>
</ul>
</nav>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop;
$easing: $ease-spring;
$color: $stage-yellow;
@include stage($stage-ink);
// 플라이아웃은 왼쪽에서 폭을 쓴다 — 무대 중앙 정렬 대신 상단 왼쪽 고정
.stage { align-items: flex-start; justify-items: start; }
$ink-70: rgba(23, 20, 26, .72);
.mls {
position: relative;
}
.mls__trigger {
box-sizing: border-box;
display: inline-flex;
align-items: center;
gap: $sp-2;
height: 36px;
padding: 0 $sp-3;
border: 0;
border-radius: $r-control;
background: $subject-cream;
box-shadow: $shadow-press;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
font-size: 13px;
font-weight: 700;
line-height: 18px;
color: $subject-ink;
cursor: pointer;
}
.mls__trigger:focus-visible { outline: 2px solid $color; outline-offset: -2px; }
// 실제 조작에서도 열려 있는 동안 트리거가 눌려 있음을 배경으로 보여 준다
.mls:not(.is-demo) .mls__trigger[aria-expanded="true"] { background-color: rgba(255, 210, 63, .45); }
.mls__caret { display: block; transition: transform $dur-quick $ease-spring; }
.mls__trigger[aria-expanded="true"] .mls__caret { transform: rotate(180deg); }
.mls__l1,
.mls__l2,
.mls__l3 {
position: absolute;
z-index: 2;
box-sizing: border-box;
margin: 0;
padding: $sp-1 0;
list-style: none;
background: $subject-cream;
border-radius: $r-card;
box-shadow: $shadow-raised;
}
.mls__u {
margin: 0;
padding: 0;
list-style: none;
}
.mls__l1 {
top: 42px;
left: 0;
width: min(160px, 50vw);
opacity: 0;
visibility: hidden;
transform: translateY(-6px);
transition: opacity $dur-quick $easing, transform $dur-quick $easing, visibility 0s linear $dur-quick;
}
.mls:not(.is-demo).is-open .mls__l1 {
opacity: 1;
visibility: visible;
transform: translateY(0);
transition: opacity $dur-quick $easing, transform $dur-quick $easing, visibility 0s;
}
// 하위 단계는 각 항목(li) 기준 오른쪽 옆 — hover/:focus-within CSS 로 열린다
.mls__l2,
.mls__l3 {
top: 0;
left: 100%;
opacity: 0;
visibility: hidden;
transform: translateX(12px);
transition: opacity $dur-quick $easing, transform $dur-quick $easing, visibility 0s linear $dur-quick;
}
.mls__l2 { width: min(140px, 44vw); }
.mls__l3 { width: min(120px, 38vw); }
.mls__i { position: relative; }
.mls__i--parent:hover > .mls__l2,
.mls__i--parent:focus-within > .mls__l2,
.mls__i--parent:hover > .mls__l3,
.mls__i--parent:focus-within > .mls__l3 {
opacity: 1;
visibility: visible;
transform: translateX(0);
transition: opacity $dur-quick $easing, transform $dur-quick $easing, visibility 0s;
}
.mls__a,
.mls__b {
box-sizing: border-box;
display: flex;
width: 100%;
align-items: center;
justify-content: space-between;
gap: $sp-1;
height: 30px;
padding: 0 $sp-3;
border: 0;
background: transparent;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
font-size: 12px;
font-weight: 400;
line-height: 16px;
color: $ink-70;
text-decoration: none;
cursor: pointer;
}
.mls__a:hover,
.mls__b:hover { background: rgba(255, 210, 63, .3); color: $subject-ink; }
.mls__a:focus-visible,
.mls__b:focus-visible { outline: 2px solid $color; outline-offset: -2px; }
.mls__b { font-weight: 700; color: $subject-ink; }
.mls__sub-caret { display: block; transition: transform $dur-quick $ease-spring; }
.mls__i--parent:hover > .mls__b .mls__sub-caret,
.mls__i--parent:focus-within > .mls__b .mls__sub-caret { transform: rotate(90deg); }
// ── 자동 루프 (2s) ────────────────────────────────────
// 6% L1 컷 등장 → 30% L2 슬라이드 → 52% L3 슬라이드 → 76·82·88.5% 에 나눠 순차 닫힘(L3→L2→L1)
// 슬라이드는 transform 을 $easing 으로, 보임은 steps(1, end) 컷으로 나눠 건다
.mls.is-demo .mls__l1,
.mls.is-demo .mls__l2,
.mls.is-demo .mls__l3 { visibility: visible; }
.mls.is-demo .mls__l1 {
animation-name: mls-l1;
animation-duration: $duration;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.mls.is-demo .mls__caret {
animation-name: mls-caret;
animation-duration: $duration;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.mls.is-demo .mls__trigger {
animation-name: mls-trig;
animation-duration: $duration;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.mls.is-demo .mls__l2 {
animation-name: mls-l2-slide, mls-l2-show;
animation-duration: $duration, $duration;
animation-timing-function: $easing, steps(1, end);
animation-iteration-count: infinite, infinite;
animation-fill-mode: backwards, backwards;
}
.mls.is-demo .mls__l3 {
animation-name: mls-l3-slide, mls-l3-show;
animation-duration: $duration, $duration;
animation-timing-function: $easing, steps(1, end);
animation-iteration-count: infinite, infinite;
animation-fill-mode: backwards, backwards;
}
.mls.is-demo .mls__i--feat > .mls__b .mls__sub-caret {
animation-name: mls-caret-feat;
animation-duration: $duration;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.mls.is-demo .mls__i--share > .mls__b .mls__sub-caret {
animation-name: mls-caret-share;
animation-duration: $duration;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
@keyframes mls-l1 {
0%, 5% { opacity: 0; transform: translateY(-6px); clip-path: inset(0 0 100% 0); }
6%, 82% { opacity: 1; transform: translateY(0); clip-path: inset(0 0 0 0); }
90%, 100% { opacity: 1; transform: translateY(0); clip-path: inset(100% 0 0 0); }
}
@keyframes mls-caret {
0%, 5% { transform: rotate(0deg); }
6%, 88% { transform: rotate(180deg); }
88.5%, 100% { transform: rotate(0deg); }
}
@keyframes mls-l2-slide {
0%, 28% { transform: translateX(12px); }
36%, 80% { transform: translateX(0); }
86%, 100% { transform: translateX(12px); }
}
@keyframes mls-l2-show {
0%, 29% { opacity: 0; }
31%, 82% { opacity: 1; }
82.1%, 100% { opacity: 0; }
}
@keyframes mls-caret-feat {
0%, 30% { transform: rotate(0deg); }
31%, 82% { transform: rotate(90deg); }
83%, 100% { transform: rotate(0deg); }
}
@keyframes mls-l3-slide {
0%, 50% { transform: translateX(12px); }
58%, 77% { transform: translateX(0); }
83%, 100% { transform: translateX(12px); }
}
@keyframes mls-l3-show {
0%, 51% { opacity: 0; }
53%, 76% { opacity: 1; }
76.1%, 100% { opacity: 0; }
}
@keyframes mls-caret-share {
0%, 52% { transform: rotate(0deg); }
53%, 76% { transform: rotate(90deg); }
77%, 100% { transform: rotate(0deg); }
}
// 트리거 배경도 열림 컷과 같은 순간에 갈린다 — 열림 순간의 변화량을 닫힘보다 크게
@keyframes mls-trig {
0%, 5.9% { background-color: transparent; }
6%, 88% { background-color: rgba(255, 210, 63, .45); }
88.5%, 100% { background-color: transparent; }
}
// 루프 동안 L1 목록을 훑는 하이라이트 띠 — 열려 있는 긴 유지 구간의 프레임을 채운다.
// 배경색 스윕은 강도가 0으로 측정되는 함정(134 실측)이 있어 띠가 실제 translateY 로 움직인다
.mls.is-demo .mls__l1::after {
content: "";
position: absolute;
left: 0;
right: 0;
top: $sp-1;
height: 30px;
border-radius: $r-sm;
background: rgba(255, 210, 63, .3);
pointer-events: none;
animation-name: mls-sweep;
animation-duration: 900ms;
animation-timing-function: $ease-out;
animation-iteration-count: infinite;
}
@keyframes mls-sweep {
0% { transform: translateY(0); opacity: 0; }
10% { opacity: 1; }
90% { opacity: 1; }
100% { transform: translateY(90px); opacity: 0; }
}
// 320px 폰 — L1+L2+L3 가 옆으로 나가지 않게 폭을 줄인다.
// 슬라이드 중엔 부모 transform(+12px) 이 abs 자식에 겹쳐 최대 +24px 까지 나가므로
// 12.8 + 108+92+80 + 24 = 316.8 ≤ 320 이 되게 폭을 잡는다
@media (max-width: 340px) {
.mls__trigger { height: 30px; padding: 0 $sp-2; font-size: 11px; line-height: 15px; }
.mls__l1 { top: 34px; width: 108px; }
.mls__l2 { width: 92px; }
.mls__l3 { width: 80px; }
.mls__a,
.mls__b { height: 26px; padding: 0 $sp-1; gap: 0; font-size: 10px; line-height: 14px; }
}
@media (prefers-reduced-motion: reduce) {
.mls.is-demo .mls__l1,
.mls.is-demo .mls__l2,
.mls.is-demo .mls__l3,
.mls.is-demo .mls__caret,
.mls.is-demo .mls__sub-caret { animation: none !important; }
.mls.is-demo .mls__l1::after { animation: none !important; opacity: 0; }
.mls.is-demo .mls__l1,
.mls.is-demo .mls__l2,
.mls.is-demo .mls__l3 { opacity: 1; visibility: visible; transform: none; }
.mls.is-demo .mls__caret { transform: rotate(180deg); }
.mls.is-demo .mls__trigger { background-color: rgba(255, 210, 63, .45); }
.mls.is-demo .mls__i--feat > .mls__b .mls__sub-caret,
.mls.is-demo .mls__i--share > .mls__b .mls__sub-caret { transform: rotate(90deg); }
}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);
}
.stage {
align-items: flex-start;
justify-items: start;
}
.mls {
position: relative;
}
.mls__trigger {
box-sizing: border-box;
display: inline-flex;
align-items: center;
gap: 8px;
height: 36px;
padding: 0 12px;
border: 0;
border-radius: 12px;
background: #fff7e6;
box-shadow: 0 6px 12px rgba(23, 20, 26, 0.32);
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
font-size: 13px;
font-weight: 700;
line-height: 18px;
color: #17141a;
cursor: pointer;
}
.mls__trigger:focus-visible {
outline: 2px solid #ffd23f;
outline-offset: -2px;
}
.mls:not(.is-demo) .mls__trigger[aria-expanded=true] {
background-color: rgba(255, 210, 63, 0.45);
}
.mls__caret {
display: block;
transition: transform 220ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.mls__trigger[aria-expanded=true] .mls__caret {
transform: rotate(180deg);
}
.mls__l1,
.mls__l2,
.mls__l3 {
position: absolute;
z-index: 2;
box-sizing: border-box;
margin: 0;
padding: 4px 0;
list-style: none;
background: #fff7e6;
border-radius: 16px;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
}
.mls__u {
margin: 0;
padding: 0;
list-style: none;
}
.mls__l1 {
top: 42px;
left: 0;
width: min(160px, 50vw);
opacity: 0;
visibility: hidden;
transform: translateY(-6px);
transition: opacity 220ms cubic-bezier(0.2, 0.8, 0.2, 1), transform 220ms cubic-bezier(0.2, 0.8, 0.2, 1), visibility 0s linear 220ms;
}
.mls:not(.is-demo).is-open .mls__l1 {
opacity: 1;
visibility: visible;
transform: translateY(0);
transition: opacity 220ms cubic-bezier(0.2, 0.8, 0.2, 1), transform 220ms cubic-bezier(0.2, 0.8, 0.2, 1), visibility 0s;
}
.mls__l2,
.mls__l3 {
top: 0;
left: 100%;
opacity: 0;
visibility: hidden;
transform: translateX(12px);
transition: opacity 220ms cubic-bezier(0.2, 0.8, 0.2, 1), transform 220ms cubic-bezier(0.2, 0.8, 0.2, 1), visibility 0s linear 220ms;
}
.mls__l2 {
width: min(140px, 44vw);
}
.mls__l3 {
width: min(120px, 38vw);
}
.mls__i {
position: relative;
}
.mls__i--parent:hover > .mls__l2,
.mls__i--parent:focus-within > .mls__l2,
.mls__i--parent:hover > .mls__l3,
.mls__i--parent:focus-within > .mls__l3 {
opacity: 1;
visibility: visible;
transform: translateX(0);
transition: opacity 220ms cubic-bezier(0.2, 0.8, 0.2, 1), transform 220ms cubic-bezier(0.2, 0.8, 0.2, 1), visibility 0s;
}
.mls__a,
.mls__b {
box-sizing: border-box;
display: flex;
width: 100%;
align-items: center;
justify-content: space-between;
gap: 4px;
height: 30px;
padding: 0 12px;
border: 0;
background: transparent;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
font-size: 12px;
font-weight: 400;
line-height: 16px;
color: rgba(23, 20, 26, 0.72);
text-decoration: none;
cursor: pointer;
}
.mls__a:hover,
.mls__b:hover {
background: rgba(255, 210, 63, 0.3);
color: #17141a;
}
.mls__a:focus-visible,
.mls__b:focus-visible {
outline: 2px solid #ffd23f;
outline-offset: -2px;
}
.mls__b {
font-weight: 700;
color: #17141a;
}
.mls__sub-caret {
display: block;
transition: transform 220ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.mls__i--parent:hover > .mls__b .mls__sub-caret,
.mls__i--parent:focus-within > .mls__b .mls__sub-caret {
transform: rotate(90deg);
}
.mls.is-demo .mls__l1,
.mls.is-demo .mls__l2,
.mls.is-demo .mls__l3 {
visibility: visible;
}
.mls.is-demo .mls__l1 {
animation-name: mls-l1;
animation-duration: 2s;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.mls.is-demo .mls__caret {
animation-name: mls-caret;
animation-duration: 2s;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.mls.is-demo .mls__trigger {
animation-name: mls-trig;
animation-duration: 2s;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.mls.is-demo .mls__l2 {
animation-name: mls-l2-slide, mls-l2-show;
animation-duration: 2s, 2s;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1), steps(1, end);
animation-iteration-count: infinite, infinite;
animation-fill-mode: backwards, backwards;
}
.mls.is-demo .mls__l3 {
animation-name: mls-l3-slide, mls-l3-show;
animation-duration: 2s, 2s;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1), steps(1, end);
animation-iteration-count: infinite, infinite;
animation-fill-mode: backwards, backwards;
}
.mls.is-demo .mls__i--feat > .mls__b .mls__sub-caret {
animation-name: mls-caret-feat;
animation-duration: 2s;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.mls.is-demo .mls__i--share > .mls__b .mls__sub-caret {
animation-name: mls-caret-share;
animation-duration: 2s;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
@keyframes mls-l1 {
0%, 5% {
opacity: 0;
transform: translateY(-6px);
clip-path: inset(0 0 100% 0);
}
6%, 82% {
opacity: 1;
transform: translateY(0);
clip-path: inset(0 0 0 0);
}
90%, 100% {
opacity: 1;
transform: translateY(0);
clip-path: inset(100% 0 0 0);
}
}
@keyframes mls-caret {
0%, 5% {
transform: rotate(0deg);
}
6%, 88% {
transform: rotate(180deg);
}
88.5%, 100% {
transform: rotate(0deg);
}
}
@keyframes mls-l2-slide {
0%, 28% {
transform: translateX(12px);
}
36%, 80% {
transform: translateX(0);
}
86%, 100% {
transform: translateX(12px);
}
}
@keyframes mls-l2-show {
0%, 29% {
opacity: 0;
}
31%, 82% {
opacity: 1;
}
82.1%, 100% {
opacity: 0;
}
}
@keyframes mls-caret-feat {
0%, 30% {
transform: rotate(0deg);
}
31%, 82% {
transform: rotate(90deg);
}
83%, 100% {
transform: rotate(0deg);
}
}
@keyframes mls-l3-slide {
0%, 50% {
transform: translateX(12px);
}
58%, 77% {
transform: translateX(0);
}
83%, 100% {
transform: translateX(12px);
}
}
@keyframes mls-l3-show {
0%, 51% {
opacity: 0;
}
53%, 76% {
opacity: 1;
}
76.1%, 100% {
opacity: 0;
}
}
@keyframes mls-caret-share {
0%, 52% {
transform: rotate(0deg);
}
53%, 76% {
transform: rotate(90deg);
}
77%, 100% {
transform: rotate(0deg);
}
}
@keyframes mls-trig {
0%, 5.9% {
background-color: transparent;
}
6%, 88% {
background-color: rgba(255, 210, 63, 0.45);
}
88.5%, 100% {
background-color: transparent;
}
}
.mls.is-demo .mls__l1::after {
content: "";
position: absolute;
left: 0;
right: 0;
top: 4px;
height: 30px;
border-radius: 8px;
background: rgba(255, 210, 63, 0.3);
pointer-events: none;
animation-name: mls-sweep;
animation-duration: 900ms;
animation-timing-function: ease-out;
animation-iteration-count: infinite;
}
@keyframes mls-sweep {
0% {
transform: translateY(0);
opacity: 0;
}
10% {
opacity: 1;
}
90% {
opacity: 1;
}
100% {
transform: translateY(90px);
opacity: 0;
}
}
@media (max-width: 340px) {
.mls__trigger {
height: 30px;
padding: 0 8px;
font-size: 11px;
line-height: 15px;
}
.mls__l1 {
top: 34px;
width: 108px;
}
.mls__l2 {
width: 92px;
}
.mls__l3 {
width: 80px;
}
.mls__a,
.mls__b {
height: 26px;
padding: 0 4px;
gap: 0;
font-size: 10px;
line-height: 14px;
}
}
@media (prefers-reduced-motion: reduce) {
.mls.is-demo .mls__l1,
.mls.is-demo .mls__l2,
.mls.is-demo .mls__l3,
.mls.is-demo .mls__caret,
.mls.is-demo .mls__sub-caret {
animation: none !important;
}
.mls.is-demo .mls__l1::after {
animation: none !important;
opacity: 0;
}
.mls.is-demo .mls__l1,
.mls.is-demo .mls__l2,
.mls.is-demo .mls__l3 {
opacity: 1;
visibility: visible;
transform: none;
}
.mls.is-demo .mls__caret {
transform: rotate(180deg);
}
.mls.is-demo .mls__trigger {
background-color: rgba(255, 210, 63, 0.45);
}
.mls.is-demo .mls__i--feat > .mls__b .mls__sub-caret,
.mls.is-demo .mls__i--share > .mls__b .mls__sub-caret {
transform: rotate(90deg);
}
}var root = document.getElementById('root');
var trigger = document.getElementById('trigger');
var parents = Array.prototype.slice.call(root.querySelectorAll('.mls__i--parent'));
trigger.setAttribute('aria-haspopup', 'true');
trigger.setAttribute('aria-expanded', 'false');
parents.forEach(function (li) {
var btn = li.querySelector('button');
btn.setAttribute('aria-haspopup', 'true');
btn.setAttribute('aria-expanded', 'false');
['mouseenter', 'focusin'].forEach(function (on) {
li.addEventListener(on, function () { btn.setAttribute('aria-expanded', 'true'); });
});
['mouseleave', 'focusout'].forEach(function (off) {
li.addEventListener(off, function () { btn.setAttribute('aria-expanded', 'false'); });
});
});
function stopDemo() { root.classList.remove('is-demo'); }
function setL1(on) {
stopDemo();
root.classList.toggle('is-open', on);
trigger.setAttribute('aria-expanded', String(on));
}
function closeAll() {
setL1(false);
parents.forEach(function (li) { li.querySelector('button').setAttribute('aria-expanded', 'false'); });
}
trigger.addEventListener('click', function () { setL1(!root.classList.contains('is-open')); });
trigger.addEventListener('focus', function () { setL1(true); });
document.addEventListener('keydown', function (e) { if (e.key === 'Escape') closeAll(); });
document.addEventListener('pointerdown', function (e) { if (!root.contains(e.target)) closeAll(); });
document.addEventListener('pointerdown', stopDemo, { once: true });
document.addEventListener('keydown', stopDemo, { once: true });import { useState } from "react";
import type { CSSProperties } from "react";
import styles from "./MultilevelSlide.module.scss";
export type MultilevelSlideLabels = {
menuLabel: string;
trigger: string;
start: string;
features: string;
pricing: string;
contact: string;
makeList: string;
share: string;
importItems: string;
copyLink: string;
embed: string;
};
export type MultilevelSlideProps = {
labels?: MultilevelSlideLabels;
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
type KeyEvt = { key: string };
type BlurEvt = { relatedTarget: Node | null; currentTarget: Node };
const DEFAULT_LABELS: MultilevelSlideLabels = {
menuLabel: "Library menu",
trigger: "Library",
start: "Get started",
features: "Features",
pricing: "Pricing",
contact: "Contact",
makeList: "Make a list",
share: "Share",
importItems: "Import",
copyLink: "Copy link",
embed: "Embed code",
};
function CaretDown() {
return (
<svg viewBox="0 0 256 256" width="16" height="16" aria-hidden="true">
<g fill="currentColor">
<path d="m208 96l-80 80l-80-80Z" opacity=".2" />
<path d="M215.39 92.94A8 8 0 0 0 208 88H48a8 8 0 0 0-5.66 13.66l80 80a8 8 0 0 0 11.32 0l80-80a8 8 0 0 0 1.73-8.72M128 164.69L67.31 104h121.38Z" />
</g>
</svg>
);
}
function CaretRight() {
return (
<svg viewBox="0 0 256 256" width="16" height="16" aria-hidden="true">
<g fill="currentColor">
<path d="m176 128l-80 80V48Z" opacity=".2" />
<path d="m181.66 122.34l-80-80A8 8 0 0 0 88 48v160a8 8 0 0 0 13.66 5.66l80-80a8 8 0 0 0 0-11.32M104 188.69V67.31L164.69 128Z" />
</g>
</svg>
);
}
export default function MultilevelSlide({
labels = DEFAULT_LABELS,
duration = 2000,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#ffd23f",
}: MultilevelSlideProps) {
const [live, setLive] = useState(true);
const [l1, setL1] = useState(false);
const [feat, setFeat] = useState(false);
const [share, setShare] = useState(false);
function wake() {
setLive(false);
}
function closeAll() {
wake();
setL1(false);
setFeat(false);
setShare(false);
}
function leftSub(e: BlurEvt) {
if (!e.currentTarget.contains(e.relatedTarget)) closeAll();
}
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<div
className={[styles.mls, live ? styles.isDemo : "", l1 ? styles.isOpen : ""].join(" ")}
style={vars}
onPointerDownCapture={wake}
onKeyDownCapture={wake}
onKeyDown={(e: KeyEvt) => {
if (e.key === "Escape") closeAll();
}}
onBlurCapture={leftSub}
>
<button
type="button"
className={styles.mlsTrigger}
aria-expanded={l1}
aria-haspopup="true"
onClick={() => {
wake();
setL1(!l1);
}}
onFocus={() => {
wake();
setL1(true);
}}
>
{labels.trigger}
<span className={styles.mlsCaret}>
<CaretDown />
</span>
</button>
<nav className={styles.mlsL1} aria-label={labels.menuLabel}>
<ul className={styles.mlsU}>
<li className={styles.mlsI}>
<a className={styles.mlsA} href="#">{labels.start}</a>
</li>
<li
className={[styles.mlsI, styles.mlsParent, styles.iFeat].join(" ")}
onMouseEnter={() => {
wake();
setFeat(true);
}}
onMouseLeave={() => setFeat(false)}
onFocusCapture={() => setFeat(true)}
onBlurCapture={(e: BlurEvt) => {
if (!e.currentTarget.contains(e.relatedTarget)) setFeat(false);
}}
>
<button type="button" className={styles.mlsB} aria-expanded={feat} aria-haspopup="true">
{labels.features}
<span className={styles.mlsSubCaret}>
<CaretRight />
</span>
</button>
<ul className={[styles.mlsU, styles.mlsL2].join(" ")}>
<li className={styles.mlsI}>
<a className={styles.mlsA} href="#">{labels.makeList}</a>
</li>
<li
className={[styles.mlsI, styles.mlsParent, styles.iShare].join(" ")}
onMouseEnter={() => {
wake();
setShare(true);
}}
onMouseLeave={() => setShare(false)}
onFocusCapture={() => setShare(true)}
onBlurCapture={(e: BlurEvt) => {
if (!e.currentTarget.contains(e.relatedTarget)) setShare(false);
}}
>
<button type="button" className={styles.mlsB} aria-expanded={share} aria-haspopup="true">
{labels.share}
<span className={styles.mlsSubCaret}>
<CaretRight />
</span>
</button>
<ul className={[styles.mlsU, styles.mlsL3].join(" ")}>
<li className={styles.mlsI}>
<a className={styles.mlsA} href="#">{labels.copyLink}</a>
</li>
<li className={styles.mlsI}>
<a className={styles.mlsA} href="#">{labels.embed}</a>
</li>
</ul>
</li>
<li className={styles.mlsI}>
<a className={styles.mlsA} href="#">{labels.importItems}</a>
</li>
</ul>
</li>
<li className={styles.mlsI}>
<a className={styles.mlsA} href="#">{labels.pricing}</a>
</li>
<li className={styles.mlsI}>
<a className={styles.mlsA} href="#">{labels.contact}</a>
</li>
</ul>
</nav>
</div>
);
}02 サブが右に滑る多段ドロップダウン — 計測 累積面積 16.116% · 動いたフレーム 21/23GODRICH オリジナル · SCSS と React は zip に同梱 · メガメニュー css 9選 — リンクの羅列からサイトの地図へ
このデモが属する記事
メガメニュー css 9選 — リンクの羅列からサイトの地図へメガメニュー css とは、上部ナビの項目ひとつから画面いっぱいの幅のパネルを開き、サイト全体の構造を一目で渡すメニューの作り方のことです。ここに置いた九つは、そのパネルだけでなく、多段展開・スクロールでの縮小・モバイルへの切り替え・検索・ この記事のデモをすべて見る


