08아이콘에서 입력창으로 펼쳐지는 검색
돋보기를 누르면 알약 모양 입력창이 아이콘 자리에서 펼쳐지고, 몇 글자 넣으면 건너뛰기 링크가 그 아래로 좁게 따라옵니다.
쓰임상단 여백이 아까운 문서·블로그 헤더
clip-pathinsetaria-expanded

<main class="stage">
<section class="frame is-demo" id="sch" aria-label="자료 창고">
<div class="page">
<header class="bar">
<p class="brand">자료 창고</p>
<div class="search" id="sch-box">
<button class="glass" id="sch-btn" type="button" aria-label="검색 열기" aria-controls="sch-box">
<svg viewBox="0 0 256 256" width="18" height="18" aria-hidden="true"><g fill="currentColor"><path d="M192 112a80 80 0 1 1-80-80a80 80 0 0 1 80 80" opacity=".2"/><path d="m229.66 218.34l-50.06-50.06a88.21 88.21 0 1 0-11.32 11.31l50.06 50.07a8 8 0 0 0 11.32-11.32M40 112a72 72 0 1 1 72 72a72.08 72.08 0 0 1-72-72"/></g></svg>
</button>
<input class="input" id="sch-in" type="text" placeholder="무엇을 찾으세요" autocomplete="off">
</div>
</header>
<div class="body">
<p class="skeleton" aria-hidden="true"></p>
<p class="skeleton skeleton--short" aria-hidden="true"></p>
</div>
</div>
<ul class="skips" id="sch-skips" aria-hidden="true">
<li><button type="button"><span>템플릿 모음</span><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></button></li>
<li><button type="button"><span>아이콘 세트</span><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></button></li>
<li><button type="button"><span>시작 가이드</span><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></button></li>
</ul>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop;
$easing: $ease-spring;
$color: $subject-ink;
@include stage($stage-yellow);
$ink: $subject-ink;
$cream: $subject-cream;
$line: rgba(23, 20, 26, .12);
$hover: rgba(23, 20, 26, .06);
$clip-shut: inset(0 calc(100% - 40px) 0 0 round 999px); // 아이콘 칸 40px 만 보임
$clip-open: inset(0 0 0 0 round 999px); // 전부 보임
// ── 프레임: 노랑 무대 위 크림 카드 ──────────────────
.frame {
position: relative;
overflow: hidden;
box-sizing: border-box;
width: min(420px, 94vw);
height: min(260px, 84vh);
border-radius: $r-card;
background: $cream;
color: $ink;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
font-size: 12px;
line-height: 1.35;
}
.frame, .frame * { box-sizing: border-box; }
.page { display: flex; flex-direction: column; height: 100%; }
.bar {
display: flex;
align-items: center;
justify-content: space-between;
gap: $sp-2;
padding: $sp-2 $sp-3;
}
.brand { margin: 0; font-size: 14px; font-weight: 700; letter-spacing: .02em; }
// 검색 자리 — 폭은 고정, clip-path 로 40px 아이콘 칸만 보이다가 펼쳐진다 (레이아웃 안 건드림)
.search {
position: relative;
width: min(220px, 60vw);
height: 36px;
border-radius: $r-pill;
background: $stage-paper;
clip-path: $clip-shut;
transition: clip-path $dur-base $easing;
}
.frame.is-open .search { clip-path: $clip-open; }
.search:focus-within { box-shadow: inset 0 0 0 2px $color; } // clip 밖으로 나가지 않는 초점 링
.glass {
position: absolute;
top: 0;
left: 0;
display: inline-flex;
align-items: center;
justify-content: center;
width: 40px;
height: 36px;
margin: 0;
padding: 0;
border: 0;
background: transparent;
color: $ink;
cursor: pointer;
}
.glass:focus-visible { box-shadow: inset 0 0 0 2px $color; }
.input {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 36px;
margin: 0;
padding: 0 $sp-3 0 40px;
border: 0;
border-radius: $r-pill;
background: transparent;
color: $ink;
font-family: inherit;
font-size: 12px;
line-height: 1.3;
}
.input::placeholder { color: rgba(23, 20, 26, .45); }
// 본문 흉내 — 회색 막대
.body {
display: grid;
align-content: start;
gap: $sp-2;
padding: 0 $sp-3 $sp-3;
}
.skeleton {
height: 10px;
margin: 0;
border-radius: $r-pill;
background: rgba(23, 20, 26, .12);
}
.skeleton--short { width: 62%; }
// ── 건너뛰기 목록 — input 값으로 걸러져 나타난다 ─────
.skips {
position: absolute; // 프레임 안에 완전히 들어간다
top: 56px;
right: $sp-3;
width: min(220px, 56vw);
margin: 0;
padding: $sp-1;
list-style: none;
border: 1px solid $line;
border-radius: $r-card;
background: $stage-paper;
box-shadow: $shadow-raised;
opacity: 0;
transform: translateY(-4px);
pointer-events: none;
transition: opacity $dur-quick $easing, transform $dur-quick $easing;
}
.frame.is-listing .skips { opacity: 1; transform: translateY(0); pointer-events: auto; }
.skips li {
opacity: 0;
transform: translateY(6px);
transition: opacity $dur-quick $easing, transform $dur-quick $easing;
}
.frame.is-listing .skips li { opacity: 1; transform: translateY(0); }
.frame.is-listing .skips li:nth-child(2) { transition-delay: 40ms, 40ms; }
.frame.is-listing .skips li:nth-child(3) { transition-delay: 80ms, 80ms; }
.skips button {
display: flex;
align-items: center;
justify-content: space-between;
gap: $sp-2;
width: 100%;
height: 34px;
margin: 0;
padding: 0 $sp-2;
border: 0;
border-radius: $r-control;
background: transparent;
color: inherit;
font-family: inherit;
font-size: 12px;
font-weight: 600;
line-height: 1.3;
text-align: left;
cursor: pointer;
}
.skips button:hover { background: $hover; }
.skips button:focus-visible { outline: 2px solid $color; outline-offset: 2px; }
// ── 격자 미리보기 전용 2초 루프 ─────────────────────
// 0-10% 닫힘(아이콘만) → 12-30% clip-path 펼쳐짐 → 36% 목록 steps 컷 등장 + 항목 팝 →
// 78% 유지 → 80-88% 접힘. 펼쳐진 인풋과 목록이 주인공이라 닫힘은 8% 안에 끝낸다.
@keyframes sch-press {
0%, 4% { transform: translateY(0); }
6% { transform: translateY(2px); }
8%, 100% { transform: translateY(0); }
}
@keyframes sch-clip {
0%, 12% { clip-path: $clip-shut; }
30%, 78% { clip-path: $clip-open; }
90%, 100% { clip-path: $clip-shut; }
}
@keyframes sch-list-cut {
0%, 36% { opacity: 0; }
36.1%, 74.9% { opacity: 1; }
75%, 100% { opacity: 0; }
}
@keyframes sch-list-pop {
0%, 36% { transform: translateY(-4px); }
44%, 74.9% { transform: translateY(0); }
76%, 100% { transform: translateY(-4px); }
}
@keyframes sch-item-cut {
0%, 37% { opacity: 0; }
37.1%, 100% { opacity: 1; }
}
@keyframes sch-item-pop {
0%, 37% { transform: translateY(6px); }
45%, 100% { transform: translateY(0); }
}
.frame.is-demo {
.glass {
animation-name: sch-press;
animation-duration: $duration;
animation-timing-function: $easing;
animation-iteration-count: infinite;
}
.search {
animation-name: sch-clip;
animation-duration: $duration;
animation-timing-function: $easing;
animation-iteration-count: infinite;
}
.skips {
animation-name: sch-list-cut, sch-list-pop;
animation-duration: $duration, $duration;
animation-timing-function: steps(1, end), $easing;
animation-iteration-count: infinite, infinite;
}
.skips li {
animation-name: sch-item-cut, sch-item-pop;
animation-duration: $duration, $duration;
animation-timing-function: steps(1, end), $easing;
animation-iteration-count: infinite, infinite;
animation-fill-mode: backwards, backwards;
}
}
// 시차 — 같은 루프에 delay 만 더해 쓴다 (뒤 · 높은 특이도)
.frame.is-demo .skips li:nth-child(2) { animation-delay: 40ms, 40ms; }
.frame.is-demo .skips li:nth-child(3) { animation-delay: 80ms, 80ms; }
@media (max-width: 340px) {
.bar { padding: $sp-1 $sp-2; }
.brand { font-size: 13px; }
.search { width: 56vw; height: 32px; }
.glass { height: 32px; }
.input { height: 32px; padding-right: $sp-2; font-size: 11px; }
.skips { top: 44px; width: min(168px, 56vw); }
.skips button { height: 30px; font-size: 11px; }
.body { gap: $sp-1; padding: 0 $sp-2 $sp-2; }
}
@media (prefers-reduced-motion: reduce) {
.frame.is-demo .glass,
.frame.is-demo .search,
.frame.is-demo .skips,
.frame.is-demo .skips li { animation: none !important; }
// 움직임만 끄고 「펼쳐진 검색 + 목록」 결과는 남긴다
.frame.is-demo .search { clip-path: $clip-open !important; }
.frame.is-demo .skips { opacity: 1 !important; transform: none !important; }
.frame.is-demo .skips li { opacity: 1 !important; transform: none !important; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffd23f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.frame {
position: relative;
overflow: hidden;
box-sizing: border-box;
width: min(420px, 94vw);
height: min(260px, 84vh);
border-radius: 16px;
background: #fff7e6;
color: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
font-size: 12px;
line-height: 1.35;
}
.frame, .frame * {
box-sizing: border-box;
}
.page {
display: flex;
flex-direction: column;
height: 100%;
}
.bar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 8px 12px;
}
.brand {
margin: 0;
font-size: 14px;
font-weight: 700;
letter-spacing: 0.02em;
}
.search {
position: relative;
width: min(220px, 60vw);
height: 36px;
border-radius: 999px;
background: #ffffff;
clip-path: inset(0 calc(100% - 40px) 0 0 round 999px);
transition: clip-path 300ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.frame.is-open .search {
clip-path: inset(0 0 0 0 round 999px);
}
.search:focus-within {
box-shadow: inset 0 0 0 2px #17141a;
}
.glass {
position: absolute;
top: 0;
left: 0;
display: inline-flex;
align-items: center;
justify-content: center;
width: 40px;
height: 36px;
margin: 0;
padding: 0;
border: 0;
background: transparent;
color: #17141a;
cursor: pointer;
}
.glass:focus-visible {
box-shadow: inset 0 0 0 2px #17141a;
}
.input {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 36px;
margin: 0;
padding: 0 12px 0 40px;
border: 0;
border-radius: 999px;
background: transparent;
color: #17141a;
font-family: inherit;
font-size: 12px;
line-height: 1.3;
}
.input::placeholder {
color: rgba(23, 20, 26, 0.45);
}
.body {
display: grid;
align-content: start;
gap: 8px;
padding: 0 12px 12px;
}
.skeleton {
height: 10px;
margin: 0;
border-radius: 999px;
background: rgba(23, 20, 26, 0.12);
}
.skeleton--short {
width: 62%;
}
.skips {
position: absolute;
top: 56px;
right: 12px;
width: min(220px, 56vw);
margin: 0;
padding: 4px;
list-style: none;
border: 1px solid rgba(23, 20, 26, 0.12);
border-radius: 16px;
background: #ffffff;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
opacity: 0;
transform: translateY(-4px);
pointer-events: none;
transition: opacity 220ms cubic-bezier(0.2, 0.8, 0.2, 1), transform 220ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.frame.is-listing .skips {
opacity: 1;
transform: translateY(0);
pointer-events: auto;
}
.skips li {
opacity: 0;
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);
}
.frame.is-listing .skips li {
opacity: 1;
transform: translateY(0);
}
.frame.is-listing .skips li:nth-child(2) {
transition-delay: 40ms, 40ms;
}
.frame.is-listing .skips li:nth-child(3) {
transition-delay: 80ms, 80ms;
}
.skips button {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
width: 100%;
height: 34px;
margin: 0;
padding: 0 8px;
border: 0;
border-radius: 12px;
background: transparent;
color: inherit;
font-family: inherit;
font-size: 12px;
font-weight: 600;
line-height: 1.3;
text-align: left;
cursor: pointer;
}
.skips button:hover {
background: rgba(23, 20, 26, 0.06);
}
.skips button:focus-visible {
outline: 2px solid #17141a;
outline-offset: 2px;
}
@keyframes sch-press {
0%, 4% {
transform: translateY(0);
}
6% {
transform: translateY(2px);
}
8%, 100% {
transform: translateY(0);
}
}
@keyframes sch-clip {
0%, 12% {
clip-path: inset(0 calc(100% - 40px) 0 0 round 999px);
}
30%, 78% {
clip-path: inset(0 0 0 0 round 999px);
}
90%, 100% {
clip-path: inset(0 calc(100% - 40px) 0 0 round 999px);
}
}
@keyframes sch-list-cut {
0%, 36% {
opacity: 0;
}
36.1%, 74.9% {
opacity: 1;
}
75%, 100% {
opacity: 0;
}
}
@keyframes sch-list-pop {
0%, 36% {
transform: translateY(-4px);
}
44%, 74.9% {
transform: translateY(0);
}
76%, 100% {
transform: translateY(-4px);
}
}
@keyframes sch-item-cut {
0%, 37% {
opacity: 0;
}
37.1%, 100% {
opacity: 1;
}
}
@keyframes sch-item-pop {
0%, 37% {
transform: translateY(6px);
}
45%, 100% {
transform: translateY(0);
}
}
.frame.is-demo .glass {
animation-name: sch-press;
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
animation-iteration-count: infinite;
}
.frame.is-demo .search {
animation-name: sch-clip;
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
animation-iteration-count: infinite;
}
.frame.is-demo .skips {
animation-name: sch-list-cut, sch-list-pop;
animation-duration: 2s, 2s;
animation-timing-function: steps(1, end), cubic-bezier(0.2, 0.8, 0.2, 1);
animation-iteration-count: infinite, infinite;
}
.frame.is-demo .skips li {
animation-name: sch-item-cut, sch-item-pop;
animation-duration: 2s, 2s;
animation-timing-function: steps(1, end), cubic-bezier(0.2, 0.8, 0.2, 1);
animation-iteration-count: infinite, infinite;
animation-fill-mode: backwards, backwards;
}
.frame.is-demo .skips li:nth-child(2) {
animation-delay: 40ms, 40ms;
}
.frame.is-demo .skips li:nth-child(3) {
animation-delay: 80ms, 80ms;
}
@media (max-width: 340px) {
.bar {
padding: 4px 8px;
}
.brand {
font-size: 13px;
}
.search {
width: 56vw;
height: 32px;
}
.glass {
height: 32px;
}
.input {
height: 32px;
padding-right: 8px;
font-size: 11px;
}
.skips {
top: 44px;
width: min(168px, 56vw);
}
.skips button {
height: 30px;
font-size: 11px;
}
.body {
gap: 4px;
padding: 0 8px 8px;
}
}
@media (prefers-reduced-motion: reduce) {
.frame.is-demo .glass,
.frame.is-demo .search,
.frame.is-demo .skips,
.frame.is-demo .skips li {
animation: none !important;
}
.frame.is-demo .search {
clip-path: inset(0 0 0 0 round 999px) !important;
}
.frame.is-demo .skips {
opacity: 1 !important;
transform: none !important;
}
.frame.is-demo .skips li {
opacity: 1 !important;
transform: none !important;
}
}// 실제 동작: 버튼으로 펼치고 input 에 치면 목록이 includes 필터로 걸러진다. Escape 로 닫는다.
var root = document.getElementById('sch');
var btn = document.getElementById('sch-btn');
var input = document.getElementById('sch-in');
var skips = document.getElementById('sch-skips');
var items = Array.prototype.slice.call(skips.querySelectorAll('button'));
btn.setAttribute('aria-haspopup', 'true');
btn.setAttribute('aria-expanded', 'false');
input.tabIndex = -1; // 닫힌 동안 input 은 탭 순서에서 빠진다
function hits() {
var q = input.value.trim();
if (q === '') return [];
return items.filter(function (b) { return b.textContent.includes(q); });
}
function render() {
var hit = hits();
root.classList.toggle('is-listing', hit.length > 0);
skips.setAttribute('aria-hidden', hit.length ? 'false' : 'true');
items.forEach(function (b) { b.parentElement.hidden = hit.indexOf(b) < 0; });
}
function setOpen(on) {
root.classList.remove('is-demo'); // 첫 조작에서 격자 루프를 끈다
root.classList.toggle('is-open', on);
btn.setAttribute('aria-expanded', on ? 'true' : 'false');
input.tabIndex = on ? 0 : -1;
if (on) { input.focus({ preventScroll: true }); render(); return; }
root.classList.remove('is-listing');
skips.setAttribute('aria-hidden', 'true');
btn.focus({ preventScroll: true });
}
btn.addEventListener('click', function () { setOpen(!root.classList.contains('is-open')); });
input.addEventListener('input', function () { root.classList.remove('is-demo'); render(); });
items.forEach(function (b) { b.addEventListener('click', function () { setOpen(false); }); });
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && root.classList.contains('is-open')) setOpen(false);
});import { useState, useRef } from "react";
import type { CSSProperties } from "react";
import styles from "./SearchExpand.module.scss";
export type SearchExpandLabels = {
brand: string;
openLabel: string;
placeholder: string;
items: string[];
};
export type SearchExpandProps = {
labels?: Partial<SearchExpandLabels>;
/** 격자 미리보기용 2초 자동 루프. 사람이 손대면 꺼진다 */
demo?: boolean;
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
// ambient 가 얇아 이벤트 제네릭을 못 쓴다 — 필요한 꼴만 로컬 타입으로 선언
type KeyEvt = { key: string; preventDefault(): void };
type InputEvt = { target: HTMLInputElement };
type Focusable = { focus: (o: { preventScroll: boolean }) => void };
type Box<T> = { current: T | null };
const defaults: SearchExpandLabels = {
brand: "File depot",
openLabel: "Open search",
placeholder: "Search the site",
items: ["Templates", "Icon sets", "Quick start"],
};
function MagnifyIcon() {
return (
<svg viewBox="0 0 256 256" width="18" height="18" aria-hidden="true"><g fill="currentColor"><path d="M192 112a80 80 0 1 1-80-80a80 80 0 0 1 80 80" opacity=".2"/><path d="m229.66 218.34l-50.06-50.06a88.21 88.21 0 1 0-11.32 11.31l50.06 50.07a8 8 0 0 0 11.32-11.32M40 112a72 72 0 1 1 72 72a72.08 72.08 0 0 1-72-72"/></g></svg>
);
}
function ArrowIcon() {
return (
<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>
);
}
export default function SearchExpand({
labels,
demo = true,
duration = 2000,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#17141a",
}: SearchExpandProps) {
const L = { ...defaults, ...labels };
const [open, setOpen] = useState(false);
const [live, setLive] = useState(demo);
const [value, setValue] = useState("");
const btnRef = useRef<Focusable | null>(null);
const inputRef = useRef<Focusable | null>(null);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
const q = value.trim();
const hits = q === "" ? [] : L.items.filter((t) => t.includes(q));
const listing = open && hits.length > 0;
function setSearch(on: boolean) {
setLive(false);
setOpen(on);
if (on) { inputRef.current?.focus({ preventScroll: true }); return; }
btnRef.current?.focus({ preventScroll: true });
}
function rootKeys(e: KeyEvt) {
if (e.key === "Escape" && open) setSearch(false);
}
const cls = [styles.frame, live ? styles.isDemo : "", open ? styles.isOpen : "", listing ? styles.isListing : ""]
.filter(Boolean)
.join(" ");
return (
<section className={cls} style={vars} aria-label={L.brand} onKeyDown={rootKeys}>
<div className={styles.page}>
<header className={styles.bar}>
<p className={styles.brand}>{L.brand}</p>
<div className={styles.search}>
<button
className={styles.glass}
type="button"
aria-label={L.openLabel}
aria-haspopup="true"
aria-expanded={open}
onClick={() => setSearch(!open)}
ref={(el: Focusable | null) => { btnRef.current = el; }}
>
<MagnifyIcon />
</button>
<input
className={styles.input}
type="text"
placeholder={L.placeholder}
autoComplete="off"
tabIndex={open ? 0 : -1}
value={value}
onChange={(e: InputEvt) => { setLive(false); setValue(e.target.value); }}
ref={(el: Focusable | null) => { inputRef.current = el; }}
/>
</div>
</header>
<div className={styles.body}>
<p className={styles.skeleton} aria-hidden="true" />
<p className={styles.skeletonShort} aria-hidden="true" />
</div>
</div>
<ul className={styles.skips} aria-hidden={!listing}>
{L.items.map((t) => (
<li key={t} hidden={!hits.includes(t)}>
<button type="button" onClick={() => setSearch(false)}>
<span>{t}</span>
<ArrowIcon />
</button>
</li>
))}
</ul>
</section>
);
}08 아이콘에서 입력창으로 펼쳐지는 검색 — 측정 누적면적 27.184% · 움직인 프레임 16/23GODRICH 원작 · zip 에 SCSS·React 포함 · 메가 메뉴 ui 9종 — 링크 나열이 아니라 사이트 지도
이 데모가 속한 글
메가 메뉴 ui 9종 — 링크 나열이 아니라 사이트 지도메가 메뉴 ui 는 상단 내비의 항목 하나에서 화면 폭만큼 넓은 패널이 열려 사이트 전체 구조를 한눈에 보여 주는 메뉴입니다. 여기 모은 아홉 개는 그 패널뿐 아니라 다단 전개·스크롤 축소·모바일 전환·검색·알림까지 이 글의 데모 전부 보기
