06ニューモーフィズムドック
柔らかい背景の上で浮き上がったアイコンの円板を押すと影が内側へ窪んで押した跡になり、選んだものは押されたまま残ります。
使いどころムード・インテリア・ベビー用品など柔らかい印象を生かすブランド画面
box-shadowinsetaria-pressed

<main class="stage">
<div class="nu is-demo" id="nu">
<nav class="nu__dock" aria-label="メインメニュー">
<button class="nu__btn" type="button" aria-label="ホーム" aria-pressed="true">
<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>
</button>
<button class="nu__btn" type="button" aria-label="検索" aria-pressed="false">
<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>
</button>
<button class="nu__btn" type="button" aria-label="フォルダ" aria-pressed="false">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" aria-hidden="true"><g fill="currentColor"><path d="M128 80H32V56a8 8 0 0 1 8-8h52.69a8 8 0 0 1 5.65 2.34Z" opacity=".2"/><path d="M216 72h-84.69L104 44.69A15.86 15.86 0 0 0 92.69 40H40a16 16 0 0 0-16 16v144.62A15.4 15.4 0 0 0 39.38 216h177.51A15.13 15.13 0 0 0 232 200.89V88a16 16 0 0 0-16-16M92.69 56l16 16H40V56ZM216 200H40V88h176Z"/></g></svg>
</button>
<button class="nu__btn" type="button" aria-label="通知" aria-pressed="false">
<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>
</button>
<button class="nu__btn" type="button" aria-label="マイページ" aria-pressed="false">
<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>
</button>
</nav>
<p class="nu__cap">押したアイコンは影が内側へ窪みます</p>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-quick;
$easing: $ease-out;
$color: #6d5fae;
$bg: #e8e6f0;
$light: #ffffff;
$dark: #a9a3c0;
@include stage($bg);
.nu {
display: flex;
flex-direction: column;
align-items: center;
gap: $sp-5;
}
.nu__dock {
display: flex;
gap: $sp-3;
padding: $sp-3 $sp-4;
border-radius: $r-pill;
background: $bg;
box-shadow: 8px 8px 16px $dark, -8px -8px 16px $light;
}
.nu__btn {
flex: 0 0 auto;
width: 48px;
height: 48px;
display: grid;
place-items: center;
padding: 0;
border: 0;
border-radius: 50%;
background: $bg;
color: rgba(60, 54, 84, .6);
cursor: pointer;
/* 떠오른 상태 — 바깥 그림자 두 개 */
box-shadow: 5px 5px 10px $dark, -5px -5px 10px $light;
transition: box-shadow $duration $easing, color $duration $easing;
}
.nu__btn svg { width: 22px; height: 22px; display: block; }
.nu__btn:focus-visible { outline: 2px solid $color; outline-offset: 4px; }
.nu__btn[aria-pressed="true"] {
color: $color;
/* 눌린 상태 — 같은 그림자를 inset 으로 뒤집는다 */
box-shadow: inset 5px 5px 10px $dark, inset -5px -5px 10px $light;
}
.nu__cap {
margin: 0;
font-size: 13px;
line-height: 1.5;
color: rgba(60, 54, 84, .82); // 뉴모피즘 연면 위 흐린 글자 3.40:1 → 4.6:1 이상
}
/* 자동 재생 — 왼쪽부터 하나씩 눌렸다 떠오른다. 마지막은 84% 에 풀린다 */
.is-demo .nu__btn {
animation-name: nu-press;
animation-duration: 2s;
animation-timing-function: $ease-out;
animation-iteration-count: infinite;
}
.is-demo .nu__btn:nth-child(2) { animation-delay: .34s; }
.is-demo .nu__btn:nth-child(3) { animation-delay: .68s; }
.is-demo .nu__btn:nth-child(4) { animation-delay: 1.02s; }
.is-demo .nu__btn:nth-child(5) { animation-delay: 1.36s; }
@keyframes nu-press {
0% { box-shadow: 5px 5px 10px $dark, -5px -5px 10px $light; color: rgba(60, 54, 84, .6); }
8% { box-shadow: inset 5px 5px 10px $dark, inset -5px -5px 10px $light; color: $color; }
17% { box-shadow: inset 5px 5px 10px $dark, inset -5px -5px 10px $light; color: $color; }
24% { box-shadow: 5px 5px 10px $dark, -5px -5px 10px $light; color: rgba(60, 54, 84, .6); }
100% { box-shadow: 5px 5px 10px $dark, -5px -5px 10px $light; color: rgba(60, 54, 84, .6); }
}
@media (max-width: 340px) {
.nu__dock { gap: $sp-2; padding: $sp-2 $sp-3; }
.nu__btn { width: 44px; height: 44px; }
}
@media (prefers-reduced-motion: reduce) {
.is-demo .nu__btn { animation-name: none; }
.nu__btn { 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: #e8e6f0;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.nu {
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
}
.nu__dock {
display: flex;
gap: 12px;
padding: 12px 16px;
border-radius: 999px;
background: #e8e6f0;
box-shadow: 8px 8px 16px #a9a3c0, -8px -8px 16px #ffffff;
}
.nu__btn {
flex: 0 0 auto;
width: 48px;
height: 48px;
display: grid;
place-items: center;
padding: 0;
border: 0;
border-radius: 50%;
background: #e8e6f0;
color: rgba(60, 54, 84, 0.6);
cursor: pointer;
/* 떠오른 상태 — 바깥 그림자 두 개 */
box-shadow: 5px 5px 10px #a9a3c0, -5px -5px 10px #ffffff;
transition: box-shadow 220ms ease-out, color 220ms ease-out;
}
.nu__btn svg {
width: 22px;
height: 22px;
display: block;
}
.nu__btn:focus-visible {
outline: 2px solid #6d5fae;
outline-offset: 4px;
}
.nu__btn[aria-pressed=true] {
color: #6d5fae;
/* 눌린 상태 — 같은 그림자를 inset 으로 뒤집는다 */
box-shadow: inset 5px 5px 10px #a9a3c0, inset -5px -5px 10px #ffffff;
}
.nu__cap {
margin: 0;
font-size: 13px;
line-height: 1.5;
color: rgba(60, 54, 84, 0.82);
}
/* 자동 재생 — 왼쪽부터 하나씩 눌렸다 떠오른다. 마지막은 84% 에 풀린다 */
.is-demo .nu__btn {
animation-name: nu-press;
animation-duration: 2s;
animation-timing-function: ease-out;
animation-iteration-count: infinite;
}
.is-demo .nu__btn:nth-child(2) {
animation-delay: 0.34s;
}
.is-demo .nu__btn:nth-child(3) {
animation-delay: 0.68s;
}
.is-demo .nu__btn:nth-child(4) {
animation-delay: 1.02s;
}
.is-demo .nu__btn:nth-child(5) {
animation-delay: 1.36s;
}
@keyframes nu-press {
0% {
box-shadow: 5px 5px 10px #a9a3c0, -5px -5px 10px #ffffff;
color: rgba(60, 54, 84, 0.6);
}
8% {
box-shadow: inset 5px 5px 10px #a9a3c0, inset -5px -5px 10px #ffffff;
color: #6d5fae;
}
17% {
box-shadow: inset 5px 5px 10px #a9a3c0, inset -5px -5px 10px #ffffff;
color: #6d5fae;
}
24% {
box-shadow: 5px 5px 10px #a9a3c0, -5px -5px 10px #ffffff;
color: rgba(60, 54, 84, 0.6);
}
100% {
box-shadow: 5px 5px 10px #a9a3c0, -5px -5px 10px #ffffff;
color: rgba(60, 54, 84, 0.6);
}
}
@media (max-width: 340px) {
.nu__dock {
gap: 8px;
padding: 8px 12px;
}
.nu__btn {
width: 44px;
height: 44px;
}
}
@media (prefers-reduced-motion: reduce) {
.is-demo .nu__btn {
animation-name: none;
}
.nu__btn {
transition: none;
}
}// 실제 동작: 클릭 → aria-pressed 를 옮긴다. 눌린 표현은 CSS 가 [aria-pressed="true"] 로 그린다.
var root = document.getElementById('nu');
var btns = Array.prototype.slice.call(root.querySelectorAll('.nu__btn'));
btns.forEach(function (b) {
b.addEventListener('pointerdown', function () { root.classList.remove('is-demo'); });
b.addEventListener('click', function () {
btns.forEach(function (x) { x.setAttribute('aria-pressed', String(x === b)); });
});
});import { useState } from "react";
import type { CSSProperties } from "react";
import styles from "./NeuSoftDock.module.scss";
export type NeuSoftDockProps = {
/** 처음 눌려 있는 칸 */
start?: number;
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
const ITEMS = [
{
label: "Home",
icon: (
<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",
icon: (
<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: "Files",
icon: (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" aria-hidden="true"><g fill="currentColor"><path d="M128 80H32V56a8 8 0 0 1 8-8h52.69a8 8 0 0 1 5.65 2.34Z" opacity=".2" /><path d="M216 72h-84.69L104 44.69A15.86 15.86 0 0 0 92.69 40H40a16 16 0 0 0-16 16v144.62A15.4 15.4 0 0 0 39.38 216h177.51A15.13 15.13 0 0 0 232 200.89V88a16 16 0 0 0-16-16M92.69 56l16 16H40V56ZM216 200H40V88h176Z" /></g></svg>
),
},
{
label: "Alerts",
icon: (
<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",
icon: (
<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 NeuSoftDock({
start = 0,
duration = 220,
easing = "ease-out",
color = "#6d5fae",
}: NeuSoftDockProps) {
// 클릭 → aria-pressed 를 옮긴다. 눌린 표현은 CSS 가 [aria-pressed="true"] 로 그린다.
const [on, setOn] = useState(start);
const vars: Vars = {
"--duration": `${duration}ms`,
"--easing": easing,
"--color": color,
};
return (
<div className={styles.nu} style={vars}>
<nav className={styles.dock} aria-label="Main menu">
{ITEMS.map(function (it, i) {
return (
<button
className={styles.btn}
type="button"
key={it.label}
aria-label={it.label}
aria-pressed={i === on}
onClick={function () { setOn(i); }}
>
{it.icon}
</button>
);
})}
</nav>
<p className={styles.cap}>The pressed icon has its shadow carved inward</p>
</div>
);
}06 ニューモーフィズムドック — 計測 累積面積 6.16% · 動いたフレーム 10/23GODRICH オリジナル · SCSS と React は zip に同梱 · フローティングメニュー デザイン9選 — ホバーで隣まで膨らむ
このデモが属する記事
フローティングメニュー デザイン9選 — ホバーで隣まで膨らむフローティングメニュー デザインとは、画面の端に浮かせた小さなメニュー帯の作り方のことで、数個のアイコンボタンと、今どの枠にいるかを示す印1つで成り立ちます。9つのうち1つは、ポインターが近づくと隣のアイコンを押し退けながら膨らみます。 この記事のデモをすべて見る


