01미니멀 라인 독
얇은 테두리 바에 아이콘 다섯 개가 떠 있고, 누른 항목 아래 밑줄이 가운데에서 양옆으로 자라며 이전 밑줄은 같은 속도로 접힙니다.
쓰임장식을 최소로 하고 싶은 포트폴리오·블로그 하단 고정 메뉴
transformscaleXaria-current

<main class="stage">
<div class="mn is-demo" id="mn">
<nav class="mn__dock" aria-label="주요 메뉴">
<button class="mn__btn" type="button" aria-label="홈">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" aria-hidden="true"><g fill="currentColor"><path d="M216 120v96h-64v-64h-48v64H40v-96a8 8 0 0 1 2.34-5.66l80-80a8 8 0 0 1 11.32 0l80 80A8 8 0 0 1 216 120" opacity=".2"/><path d="m219.31 108.68l-80-80a16 16 0 0 0-22.62 0l-80 80A15.87 15.87 0 0 0 32 120v96a8 8 0 0 0 8 8h64a8 8 0 0 0 8-8v-56h32v56a8 8 0 0 0 8 8h64a8 8 0 0 0 8-8v-96a15.87 15.87 0 0 0-4.69-11.32M208 208h-48v-56a8 8 0 0 0-8-8h-48a8 8 0 0 0-8 8v56H48v-88l80-80l80 80Z"/></g></svg>
<i class="mn__line" aria-hidden="true"></i>
</button>
<button class="mn__btn" type="button" aria-label="검색">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" 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>
<i class="mn__line" aria-hidden="true"></i>
</button>
<button class="mn__btn" type="button" aria-label="좋아요">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" aria-hidden="true"><g fill="currentColor"><path d="M232 102c0 66-104 122-104 122S24 168 24 102a54 54 0 0 1 54-54c22.59 0 41.94 12.31 50 32c8.06-19.69 27.41-32 50-32a54 54 0 0 1 54 54" opacity=".2"/><path d="M178 40c-20.65 0-38.73 8.88-50 23.89C116.73 48.88 98.65 40 78 40a62.07 62.07 0 0 0-62 62c0 70 103.79 126.66 108.21 129a8 8 0 0 0 7.58 0C136.21 228.66 240 172 240 102a62.07 62.07 0 0 0-62-62m-50 174.8c-18.26-10.64-96-59.11-96-112.8a46.06 46.06 0 0 1 46-46c19.45 0 35.78 10.36 42.6 27a8 8 0 0 0 14.8 0c6.82-16.67 23.15-27 42.6-27a46.06 46.06 0 0 1 46 46c0 53.61-77.76 102.15-96 112.8"/></g></svg>
<i class="mn__line" aria-hidden="true"></i>
</button>
<button class="mn__btn" type="button" aria-label="알림">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" aria-hidden="true"><g fill="currentColor"><path d="M208 192H48a8 8 0 0 1-6.88-12C47.71 168.6 56 139.81 56 104a72 72 0 0 1 144 0c0 35.82 8.3 64.6 14.9 76a8 8 0 0 1-6.9 12" opacity=".2"/><path d="M221.8 175.94c-5.55-9.56-13.8-36.61-13.8-71.94a80 80 0 1 0-160 0c0 35.34-8.26 62.38-13.81 71.94A16 16 0 0 0 48 200h40.81a40 40 0 0 0 78.38 0H208a16 16 0 0 0 13.8-24.06M128 216a24 24 0 0 1-22.62-16h45.24A24 24 0 0 1 128 216m-80-32c7.7-13.24 16-43.92 16-80a64 64 0 1 1 128 0c0 36.05 8.28 66.73 16 80Z"/></g></svg>
<i class="mn__line" aria-hidden="true"></i>
</button>
<button class="mn__btn" type="button" aria-label="내 정보">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" aria-hidden="true"><g fill="currentColor"><path d="M192 96a64 64 0 1 1-64-64a64 64 0 0 1 64 64" opacity=".2"/><path d="M230.92 212c-15.23-26.33-38.7-45.21-66.09-54.16a72 72 0 1 0-73.66 0c-27.39 8.94-50.86 27.82-66.09 54.16a8 8 0 1 0 13.85 8c18.84-32.56 52.14-52 89.07-52s70.23 19.44 89.07 52a8 8 0 1 0 13.85-8M72 96a56 56 0 1 1 56 56a56.06 56.06 0 0 1-56-56"/></g></svg>
<i class="mn__line" aria-hidden="true"></i>
</button>
</nav>
<p class="mn__cap">누른 항목 아래 밑줄이 가운데서 자랍니다</p>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-quick;
$easing: $ease-spring;
$color: $subject-ink;
@include stage($stage-paper);
.mn {
display: flex;
flex-direction: column;
align-items: center;
gap: $sp-4;
}
.mn__dock {
display: flex;
gap: $sp-2;
padding: $sp-2 $sp-3;
border: 1.5px solid rgba(23, 20, 26, .3);
border-radius: $r-pill;
background: rgba(255, 255, 255, .94);
box-shadow: 0 8px 20px rgba(23, 20, 26, .08);
}
.mn__btn {
position: relative;
width: 44px;
height: 44px;
display: grid;
place-items: center;
padding: 0;
border: 0;
border-radius: $r-sm;
background: none;
color: rgba(23, 20, 26, .48);
cursor: pointer;
transition: color $duration $easing, transform $duration $easing;
}
.mn__btn:hover { color: rgba(23, 20, 26, .85); }
.mn__btn:focus-visible { outline: 2px solid $subject-blue; outline-offset: 2px; }
.mn__btn svg { width: 24px; height: 24px; display: block; }
.mn__btn.on { color: $color; transform: translateY(-2px); }
.mn__line {
position: absolute;
left: 11px;
right: 11px;
bottom: 2px;
height: 2px;
border-radius: $r-pill;
background: $color;
transform: scaleX(0);
transition: transform $duration $easing;
}
.mn__btn.on .mn__line { transform: scaleX(1); }
.mn__cap {
margin: 0;
font-size: 13px;
line-height: 1.5;
color: rgba(23, 20, 26, .62);
}
/* 자동 재생 — 항목마다 400ms 슬롯. delay 는 개별 속성으로(단축 리셋 방지). */
.is-demo .mn__line {
animation-name: mn-ln;
animation-duration: 2s;
animation-timing-function: $ease-spring;
animation-iteration-count: infinite;
}
.is-demo .mn__btn:nth-child(2) .mn__line { animation-delay: .4s; }
.is-demo .mn__btn:nth-child(3) .mn__line { animation-delay: .8s; }
.is-demo .mn__btn:nth-child(4) .mn__line { animation-delay: 1.2s; }
.is-demo .mn__btn:nth-child(5) .mn__line { animation-delay: 1.6s; }
.is-demo .mn__btn svg {
animation-name: mn-ic;
animation-duration: 2s;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
}
.is-demo .mn__btn:nth-child(2) svg { animation-delay: .4s; }
.is-demo .mn__btn:nth-child(3) svg { animation-delay: .8s; }
.is-demo .mn__btn:nth-child(4) svg { animation-delay: 1.2s; }
.is-demo .mn__btn:nth-child(5) svg { animation-delay: 1.6s; }
@keyframes mn-ln {
0% { transform: scaleX(0); }
4% { transform: scaleX(1); }
15% { transform: scaleX(1); }
20% { transform: scaleX(0); }
100% { transform: scaleX(0); }
}
@keyframes mn-ic {
0% { color: rgba(23, 20, 26, .48); }
20% { color: $subject-ink; }
100% { color: rgba(23, 20, 26, .48); }
}
@media (max-width: 340px) {
.mn__dock { gap: $sp-1; padding: $sp-2; }
.mn__btn { width: 40px; height: 40px; }
.mn__btn svg { width: 22px; height: 22px; }
}
@media (prefers-reduced-motion: reduce) {
.is-demo .mn__line,
.is-demo .mn__btn svg { animation-name: none; }
.mn__btn,
.mn__line { transition: none; }
}@charset "UTF-8";
html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffffff;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.mn {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.mn__dock {
display: flex;
gap: 8px;
padding: 8px 12px;
border: 1.5px solid rgba(23, 20, 26, 0.3);
border-radius: 999px;
background: rgba(255, 255, 255, 0.94);
box-shadow: 0 8px 20px rgba(23, 20, 26, 0.08);
}
.mn__btn {
position: relative;
width: 44px;
height: 44px;
display: grid;
place-items: center;
padding: 0;
border: 0;
border-radius: 8px;
background: none;
color: rgba(23, 20, 26, 0.48);
cursor: pointer;
transition: color 220ms cubic-bezier(0.2, 0.8, 0.2, 1), transform 220ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.mn__btn:hover {
color: rgba(23, 20, 26, 0.85);
}
.mn__btn:focus-visible {
outline: 2px solid #2f6df6;
outline-offset: 2px;
}
.mn__btn svg {
width: 24px;
height: 24px;
display: block;
}
.mn__btn.on {
color: #17141a;
transform: translateY(-2px);
}
.mn__line {
position: absolute;
left: 11px;
right: 11px;
bottom: 2px;
height: 2px;
border-radius: 999px;
background: #17141a;
transform: scaleX(0);
transition: transform 220ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.mn__btn.on .mn__line {
transform: scaleX(1);
}
.mn__cap {
margin: 0;
font-size: 13px;
line-height: 1.5;
color: rgba(23, 20, 26, 0.62);
}
/* 자동 재생 — 항목마다 400ms 슬롯. delay 는 개별 속성으로(단축 리셋 방지). */
.is-demo .mn__line {
animation-name: mn-ln;
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
animation-iteration-count: infinite;
}
.is-demo .mn__btn:nth-child(2) .mn__line {
animation-delay: 0.4s;
}
.is-demo .mn__btn:nth-child(3) .mn__line {
animation-delay: 0.8s;
}
.is-demo .mn__btn:nth-child(4) .mn__line {
animation-delay: 1.2s;
}
.is-demo .mn__btn:nth-child(5) .mn__line {
animation-delay: 1.6s;
}
.is-demo .mn__btn svg {
animation-name: mn-ic;
animation-duration: 2s;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
}
.is-demo .mn__btn:nth-child(2) svg {
animation-delay: 0.4s;
}
.is-demo .mn__btn:nth-child(3) svg {
animation-delay: 0.8s;
}
.is-demo .mn__btn:nth-child(4) svg {
animation-delay: 1.2s;
}
.is-demo .mn__btn:nth-child(5) svg {
animation-delay: 1.6s;
}
@keyframes mn-ln {
0% {
transform: scaleX(0);
}
4% {
transform: scaleX(1);
}
15% {
transform: scaleX(1);
}
20% {
transform: scaleX(0);
}
100% {
transform: scaleX(0);
}
}
@keyframes mn-ic {
0% {
color: rgba(23, 20, 26, 0.48);
}
20% {
color: #17141a;
}
100% {
color: rgba(23, 20, 26, 0.48);
}
}
@media (max-width: 340px) {
.mn__dock {
gap: 4px;
padding: 8px;
}
.mn__btn {
width: 40px;
height: 40px;
}
.mn__btn svg {
width: 22px;
height: 22px;
}
}
@media (prefers-reduced-motion: reduce) {
.is-demo .mn__line,
.is-demo .mn__btn svg {
animation-name: none;
}
.mn__btn,
.mn__line {
transition: none;
}
}// 실제 동작: 클릭 → 활성 항목 교체(aria-current). 밑줄은 transform scaleX 전환.
var root = document.getElementById('mn');
var btns = Array.prototype.slice.call(root.querySelectorAll('.mn__btn'));
function pick(i) {
btns.forEach(function (b, j) {
b.classList.toggle('on', i === j);
if (i === j) b.setAttribute('aria-current', 'page');
else b.removeAttribute('aria-current');
});
}
pick(0);
btns.forEach(function (b, i) {
b.addEventListener('pointerdown', function () { root.classList.remove('is-demo'); });
b.addEventListener('click', function () { pick(i); });
});import { useState } from "react";
import type { CSSProperties } from "react";
import styles from "./MinimalLineDock.module.scss";
export type MinimalLineDockProps = {
/** 처음 켜 둘 항목 (0부터) */
start?: number;
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
const ICONS = [
{
label: "Home",
svg: (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" aria-hidden="true"><g fill="currentColor"><path d="M216 120v96h-64v-64h-48v64H40v-96a8 8 0 0 1 2.34-5.66l80-80a8 8 0 0 1 11.32 0l80 80A8 8 0 0 1 216 120" opacity=".2"/><path d="m219.31 108.68l-80-80a16 16 0 0 0-22.62 0l-80 80A15.87 15.87 0 0 0 32 120v96a8 8 0 0 0 8 8h64a8 8 0 0 0 8-8v-56h32v56a8 8 0 0 0 8 8h64a8 8 0 0 0 8-8v-96a15.87 15.87 0 0 0-4.69-11.32M208 208h-48v-56a8 8 0 0 0-8-8h-48a8 8 0 0 0-8 8v56H48v-88l80-80l80 80Z"/></g></svg>
),
},
{
label: "Search",
svg: (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" 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>
),
},
{
label: "Likes",
svg: (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" aria-hidden="true"><g fill="currentColor"><path d="M232 102c0 66-104 122-104 122S24 168 24 102a54 54 0 0 1 54-54c22.59 0 41.94 12.31 50 32c8.06-19.69 27.41-32 50-32a54 54 0 0 1 54 54" opacity=".2"/><path d="M178 40c-20.65 0-38.73 8.88-50 23.89C116.73 48.88 98.65 40 78 40a62.07 62.07 0 0 0-62 62c0 70 103.79 126.66 108.21 129a8 8 0 0 0 7.58 0C136.21 228.66 240 172 240 102a62.07 62.07 0 0 0-62-62m-50 174.8c-18.26-10.64-96-59.11-96-112.8a46.06 46.06 0 0 1 46-46c19.45 0 35.78 10.36 42.6 27a8 8 0 0 0 14.8 0c6.82-16.67 23.15-27 42.6-27a46.06 46.06 0 0 1 46 46c0 53.61-77.76 102.15-96 112.8"/></g></svg>
),
},
{
label: "Alerts",
svg: (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" aria-hidden="true"><g fill="currentColor"><path d="M208 192H48a8 8 0 0 1-6.88-12C47.71 168.6 56 139.81 56 104a72 72 0 0 1 144 0c0 35.82 8.3 64.6 14.9 76a8 8 0 0 1-6.9 12" opacity=".2"/><path d="M221.8 175.94c-5.55-9.56-13.8-36.61-13.8-71.94a80 80 0 1 0-160 0c0 35.34-8.26 62.38-13.81 71.94A16 16 0 0 0 48 200h40.81a40 40 0 0 0 78.38 0H208a16 16 0 0 0 13.8-24.06M128 216a24 24 0 0 1-22.62-16h45.24A24 24 0 0 1 128 216m-80-32c7.7-13.24 16-43.92 16-80a64 64 0 1 1 128 0c0 36.05 8.28 66.73 16 80Z"/></g></svg>
),
},
{
label: "Profile",
svg: (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" aria-hidden="true"><g fill="currentColor"><path d="M192 96a64 64 0 1 1-64-64a64 64 0 0 1 64 64" opacity=".2"/><path d="M230.92 212c-15.23-26.33-38.7-45.21-66.09-54.16a72 72 0 1 0-73.66 0c-27.39 8.94-50.86 27.82-66.09 54.16a8 8 0 1 0 13.85 8c18.84-32.56 52.14-52 89.07-52s70.23 19.44 89.07 52a8 8 0 1 0 13.85-8M72 96a56 56 0 1 1 56 56a56.06 56.06 0 0 1-56-56"/></g></svg>
),
},
];
export default function MinimalLineDock({
start = 0,
duration = 220,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#17141a",
}: MinimalLineDockProps) {
// 실제 동작: 클릭 → 활성 항목 교체(aria-current). 밑줄은 transform scaleX 전환.
const [on, setOn] = useState(start);
const vars: Vars = {
"--duration": `${duration}ms`,
"--easing": easing,
"--color": color,
};
return (
<div className={styles.mn} style={vars}>
<nav className={styles.dock} aria-label="Main menu">
{ICONS.map((it, i) => (
<button
key={it.label}
className={i === on ? `${styles.btn} ${styles.on}` : styles.btn}
type="button"
aria-label={it.label}
aria-current={i === on ? "page" : undefined}
onClick={() => setOn(i)}
>
{it.svg}
<i className={styles.line} aria-hidden="true" />
</button>
))}
</nav>
<p className={styles.cap}>An underline grows from the center of the item you pick</p>
</div>
);
}01 미니멀 라인 독 — 측정 누적면적 0.905% · 움직인 프레임 14/23GODRICH 원작 · zip 에 SCSS·React 포함 · 플로팅 내비게이션 독 스타일 9종 — 호버 물결이 따라오는
이 데모가 속한 글
플로팅 내비게이션 독 스타일 9종 — 호버 물결이 따라오는플로팅 내비게이션은 화면 가장자리에 띄워 둔 작은 메뉴 막대이고, 아이콘 단추 몇 개와 지금 어느 칸인지 알려 주는 표시 하나로 이루어집니다. 아홉 개 중 하나는 이웃 아이콘까지 밀어내며 부풉니다. 이 글의 데모 전부 보기


