05Glassmorphism dock
Color blobs placed behind show through the frosted glass smeared by the blur, while the active pill glides across it and its icon lifts a touch.
Best forLanding screens that set the menu over photos or gradients and want the background colors to stay alive
backdrop-filterradial-gradienttranslateY

<main class="stage">
<div class="gl is-demo" id="gl" style="--idx: 0">
<i class="gl__blob gl__blob--a" aria-hidden="true"></i>
<i class="gl__blob gl__blob--b" aria-hidden="true"></i>
<nav class="gl__dock" aria-label="Main menu">
<i class="gl__pill" aria-hidden="true"></i>
<button class="gl__btn" type="button" aria-label="Home">
<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="gl__btn" type="button" aria-label="Search">
<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="gl__btn" type="button" aria-label="Likes">
<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>
</button>
<button class="gl__btn" type="button" aria-label="Alerts">
<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="gl__btn" type="button" aria-label="Profile">
<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="gl__cap">Background colors smear through the frosted glass</p>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-base;
$easing: $ease-spring;
$color: $subject-lilac;
@include stage($stage-ink);
@property --idx {
syntax: "<integer>";
inherits: true;
initial-value: 0;
}
.gl {
--idx: 0;
position: relative;
display: flex;
flex-direction: column;
align-items: center;
gap: $sp-4;
padding: $sp-6 $sp-8;
border-radius: $r-card;
overflow: hidden;
background: #241d33;
}
/* 유리 뒤에 깔아 두는 색 얼룩 — 이게 있어야 흐림이 눈에 보인다 */
.gl__blob {
position: absolute;
width: 168px;
height: 168px;
border-radius: 50%;
filter: blur(4px);
}
.gl__blob--a {
top: -40px;
left: -20px;
background: radial-gradient(circle, rgba(185, 160, 255, .95), rgba(185, 160, 255, 0) 70%);
}
.gl__blob--b {
bottom: -48px;
right: -16px;
background: radial-gradient(circle, rgba(110, 200, 255, .9), rgba(110, 200, 255, 0) 70%);
}
.gl__dock {
position: relative;
display: flex;
gap: $sp-2;
padding: $sp-2;
border: 1px solid rgba(255, 255, 255, .3);
border-radius: $r-pill;
background: rgba(255, 255, 255, .14);
backdrop-filter: blur(12px) saturate(1.6);
box-shadow: 0 12px 28px rgba(23, 20, 26, .4);
}
.gl__pill {
position: absolute;
top: $sp-2;
left: $sp-2;
width: 44px;
height: 44px;
border-radius: $r-pill;
background: rgba(255, 255, 255, .32);
transform: translateX(calc(var(--idx) * 52px));
transition: transform $duration $easing;
}
.gl__btn {
position: relative;
flex: 0 0 auto;
width: 44px;
height: 44px;
display: grid;
place-items: center;
padding: 0;
border: 0;
border-radius: $r-pill;
background: none;
color: rgba(255, 255, 255, .72);
cursor: pointer;
transition: transform $duration $easing, color $duration $easing;
}
.gl__btn svg { width: 22px; height: 22px; display: block; }
.gl__btn:focus-visible { outline: 2px solid $color; outline-offset: 2px; }
.gl__btn[aria-current="page"] { color: #fff; transform: translateY(-3px); }
.gl__cap {
position: relative;
margin: 0;
font-size: 13px;
line-height: 1.5;
color: rgba(255, 255, 255, .68);
}
/* 자동 재생 — 알약이 다섯 칸을 돌고 88% 에 첫 칸으로 복귀 */
.is-demo .gl__pill {
animation-name: gl-slide;
animation-duration: 2s;
animation-timing-function: $ease-spring;
animation-iteration-count: infinite;
}
.is-demo .gl__btn {
animation-name: gl-lift;
animation-duration: 2s;
animation-timing-function: $ease-spring;
animation-iteration-count: infinite;
}
.is-demo .gl__btn:nth-of-type(2) { animation-delay: .32s; }
.is-demo .gl__btn:nth-of-type(3) { animation-delay: .64s; }
.is-demo .gl__btn:nth-of-type(4) { animation-delay: .96s; }
.is-demo .gl__btn:nth-of-type(5) { animation-delay: 1.28s; }
@keyframes gl-slide {
0%, 12% { transform: translateX(0); }
16%, 28% { transform: translateX(52px); }
32%, 44% { transform: translateX(104px); }
48%, 60% { transform: translateX(156px); }
64%, 76% { transform: translateX(208px); }
88%, 100% { transform: translateX(0); }
}
@keyframes gl-lift {
0% { transform: translateY(0); color: rgba(255, 255, 255, .72); }
4% { transform: translateY(-3px); color: #fff; }
14% { transform: translateY(-3px); color: #fff; }
18% { transform: translateY(0); color: rgba(255, 255, 255, .72); }
100% { transform: translateY(0); color: rgba(255, 255, 255, .72); }
}
@media (max-width: 340px) {
.gl { padding: $sp-5 $sp-4; }
.gl__blob { width: 120px; height: 120px; }
}
@media (prefers-reduced-motion: reduce) {
.is-demo .gl__pill,
.is-demo .gl__btn { animation-name: none; }
.gl__pill,
.gl__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: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
@property --idx {
syntax: "<integer>";
inherits: true;
initial-value: 0;
}
.gl {
--idx: 0;
position: relative;
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
padding: 24px 32px;
border-radius: 16px;
overflow: hidden;
background: #241d33;
}
/* 유리 뒤에 깔아 두는 색 얼룩 — 이게 있어야 흐림이 눈에 보인다 */
.gl__blob {
position: absolute;
width: 168px;
height: 168px;
border-radius: 50%;
filter: blur(4px);
}
.gl__blob--a {
top: -40px;
left: -20px;
background: radial-gradient(circle, rgba(185, 160, 255, 0.95), rgba(185, 160, 255, 0) 70%);
}
.gl__blob--b {
bottom: -48px;
right: -16px;
background: radial-gradient(circle, rgba(110, 200, 255, 0.9), rgba(110, 200, 255, 0) 70%);
}
.gl__dock {
position: relative;
display: flex;
gap: 8px;
padding: 8px;
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 999px;
background: rgba(255, 255, 255, 0.14);
backdrop-filter: blur(12px) saturate(1.6);
box-shadow: 0 12px 28px rgba(23, 20, 26, 0.4);
}
.gl__pill {
position: absolute;
top: 8px;
left: 8px;
width: 44px;
height: 44px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.32);
transform: translateX(calc(var(--idx) * 52px));
transition: transform 300ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.gl__btn {
position: relative;
flex: 0 0 auto;
width: 44px;
height: 44px;
display: grid;
place-items: center;
padding: 0;
border: 0;
border-radius: 999px;
background: none;
color: rgba(255, 255, 255, 0.72);
cursor: pointer;
transition: transform 300ms cubic-bezier(0.2, 0.8, 0.2, 1), color 300ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.gl__btn svg {
width: 22px;
height: 22px;
display: block;
}
.gl__btn:focus-visible {
outline: 2px solid #b9a0ff;
outline-offset: 2px;
}
.gl__btn[aria-current=page] {
color: #fff;
transform: translateY(-3px);
}
.gl__cap {
position: relative;
margin: 0;
font-size: 13px;
line-height: 1.5;
color: rgba(255, 255, 255, 0.68);
}
/* 자동 재생 — 알약이 다섯 칸을 돌고 88% 에 첫 칸으로 복귀 */
.is-demo .gl__pill {
animation-name: gl-slide;
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
animation-iteration-count: infinite;
}
.is-demo .gl__btn {
animation-name: gl-lift;
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
animation-iteration-count: infinite;
}
.is-demo .gl__btn:nth-of-type(2) {
animation-delay: 0.32s;
}
.is-demo .gl__btn:nth-of-type(3) {
animation-delay: 0.64s;
}
.is-demo .gl__btn:nth-of-type(4) {
animation-delay: 0.96s;
}
.is-demo .gl__btn:nth-of-type(5) {
animation-delay: 1.28s;
}
@keyframes gl-slide {
0%, 12% {
transform: translateX(0);
}
16%, 28% {
transform: translateX(52px);
}
32%, 44% {
transform: translateX(104px);
}
48%, 60% {
transform: translateX(156px);
}
64%, 76% {
transform: translateX(208px);
}
88%, 100% {
transform: translateX(0);
}
}
@keyframes gl-lift {
0% {
transform: translateY(0);
color: rgba(255, 255, 255, 0.72);
}
4% {
transform: translateY(-3px);
color: #fff;
}
14% {
transform: translateY(-3px);
color: #fff;
}
18% {
transform: translateY(0);
color: rgba(255, 255, 255, 0.72);
}
100% {
transform: translateY(0);
color: rgba(255, 255, 255, 0.72);
}
}
@media (max-width: 340px) {
.gl {
padding: 20px 16px;
}
.gl__blob {
width: 120px;
height: 120px;
}
}
@media (prefers-reduced-motion: reduce) {
.is-demo .gl__pill,
.is-demo .gl__btn {
animation-name: none;
}
.gl__pill,
.gl__btn {
transition: none;
}
}// 실제 동작: 클릭 → --idx 교체. 알약이 유리 위를 미끄러지고 해당 아이콘이 살짝 들린다.
var root = document.getElementById('gl');
var btns = Array.prototype.slice.call(root.querySelectorAll('.gl__btn'));
btns[0].setAttribute('aria-current', 'page');
btns.forEach(function (b, i) {
b.addEventListener('pointerdown', function () { root.classList.remove('is-demo'); });
b.addEventListener('click', function () {
root.style.setProperty('--idx', i);
btns.forEach(function (x) { x.removeAttribute('aria-current'); });
b.setAttribute('aria-current', 'page');
});
});import { useState } from "react";
import type { CSSProperties } from "react";
import styles from "./GlassFrostDock.module.scss";
export type GlassFrostDockProps = {
/** 처음 켜져 있는 칸 */
start?: number;
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & {
"--idx"?: number;
"--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: "Likes",
icon: (
<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",
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 GlassFrostDock({
start = 0,
duration = 300,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#b9a0ff",
}: GlassFrostDockProps) {
// 클릭 → --idx 교체. 알약이 유리 위를 미끄러지고 해당 아이콘이 살짝 들린다.
const [idx, setIdx] = useState(start);
const vars: Vars = {
"--idx": idx,
"--duration": `${duration}ms`,
"--easing": easing,
"--color": color,
};
return (
<div className={styles.gl} style={vars}>
{/* 유리 뒤에 깔아 두는 색 얼룩 — 이게 있어야 흐림이 눈에 보인다 */}
<i className={`${styles.blob} ${styles.blobA}`} aria-hidden="true" />
<i className={`${styles.blob} ${styles.blobB}`} aria-hidden="true" />
<nav className={styles.dock} aria-label="Main menu">
<i className={styles.pill} aria-hidden="true" />
{ITEMS.map(function (it, i) {
return (
<button
className={styles.btn}
type="button"
key={it.label}
aria-label={it.label}
aria-current={i === idx ? "page" : undefined}
onClick={function () { setIdx(i); }}
>
{it.icon}
</button>
);
})}
</nav>
<p className={styles.cap}>Background colors smear through the frosted glass</p>
</div>
);
}05 Glassmorphism dock — Measured area 6.322% · moving frames 15/23Original by GODRICH · SCSS and React ship in the zip · Mac Dock CSS — 9 Floating Dock Styles, No Library
The post this demo lives in
Mac Dock CSS — 9 Floating Dock Styles, No LibraryMac dock CSS is the plain-CSS way to rebuild that floating strip of icons at a screen edge: a short bar, a few icon butt See all demos in this post
If you like this effect - similar demos
Transitions03 Badge count-bump toggle — each time a notification count value rises, that display's size value grows once then settles backCSS Bottom Navigation Bar: 9 Transition Patterns
Transitions08 Mobile bottom toast9 CSS Toast Notifications — No Library, Pure CSS
Transitions03 Overlay fade9 CSS Sidebar Animations — Minimal JS