01글래스모피즘
반투명한 카드 뒤로 색 얼룩 두 개가 천천히 흘러가고 카드가 그 색을 흐리게 비쳐 서리 낀 유리판처럼 보이며, 팔로우 버튼은 눌린 상태를 스스로 기억합니다.
쓰임사진이나 진한 색 배경 위에 사람 정보를 얹어야 하는 팀 소개·크리에이터 소개 화면
backdrop-filterradial-gradientaria-pressed

<main class="stage">
<div class="gf is-demo">
<div class="gf__blob gf__blob--a" aria-hidden="true"></div>
<div class="gf__blob gf__blob--b" aria-hidden="true"></div>
<article class="gf__card">
<div class="gf__head">
<div class="gf__avatar">SR</div>
<div class="gf__who">
<p class="gf__name">이서린</p>
<p class="gf__role">프로덕트 디자이너</p>
</div>
</div>
<div class="gf__stats">
<div class="gf__stat"><strong class="gf__num">2,431</strong><span class="gf__lab">팔로워</span></div>
<div class="gf__stat"><strong class="gf__num">128</strong><span class="gf__lab">게시물</span></div>
</div>
<button class="gf__btn" type="button" id="gf-btn" aria-pressed="false">
<svg viewBox="0 0 256 256" width="16" height="16" aria-hidden="true"><g fill="currentColor"><path d="M168 100a60 60 0 1 1-60-60a60 60 0 0 1 60 60" opacity=".2"/><path d="M256 136a8 8 0 0 1-8 8h-16v16a8 8 0 0 1-16 0v-16h-16a8 8 0 0 1 0-16h16v-16a8 8 0 0 1 16 0v16h16a8 8 0 0 1 8 8m-57.87 58.85a8 8 0 0 1-12.26 10.3C165.75 181.19 138.09 168 108 168s-57.75 13.19-77.87 37.15a8 8 0 0 1-12.25-10.3c14.94-17.78 33.52-30.41 54.17-37.17a68 68 0 1 1 71.9 0c20.65 6.76 39.23 19.39 54.18 37.17M108 152a52 52 0 1 0-52-52a52.06 52.06 0 0 0 52 52"/></g></svg>
<svg viewBox="0 0 256 256" width="16" height="16" aria-hidden="true" hidden><g fill="currentColor"><path d="M232 56v144a16 16 0 0 1-16 16H40a16 16 0 0 1-16-16V56a16 16 0 0 1 16-16h176a16 16 0 0 1 16 16" opacity=".2"/><path d="m205.66 85.66l-96 96a8 8 0 0 1-11.32 0l-40-40a8 8 0 0 1 11.32-11.32L104 164.69l90.34-90.35a8 8 0 0 1 11.32 11.32"/></g></svg>
<span id="gf-label">팔로우</span>
</button>
</article>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-quick;
$easing: $ease-out;
$color: $subject-cream;
@include stage($stage-orange);
// 글래스모피즘 — 반투명 카드가 뒤에서 떠다니는 색 얼룩 두 개를 흐리게 비춘다.
.gf {
position: relative;
width: 300px;
height: 192px;
overflow: hidden; // 얼룩이 카드 바깥으로 새지 않게 (고정 크기 + clip)
border-radius: $r-card;
user-select: none;
-webkit-user-select: none;
}
.gf__blob { position: absolute; border-radius: 50%; pointer-events: none; }
.gf__blob--a {
top: -24px; left: -30px;
width: 140px; height: 140px;
background: radial-gradient(circle at 38% 38%, $subject-pink, rgba($subject-pink, 0) 72%);
}
.gf__blob--b {
top: 50px; left: 160px;
width: 150px; height: 150px;
background: radial-gradient(circle at 62% 62%, $subject-lilac, rgba($subject-lilac, 0) 72%);
}
.gf__card {
position: absolute;
inset: 0;
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: $sp-3;
padding: 0 $sp-4;
background: rgba(255, 255, 255, .30);
border: 1px solid rgba(255, 255, 255, .62);
border-radius: $r-card;
backdrop-filter: blur(14px) saturate(140%);
-webkit-backdrop-filter: blur(14px) saturate(140%);
color: $subject-ink;
}
.gf__head { display: flex; align-items: center; gap: $sp-3; }
.gf__avatar {
display: flex; align-items: center; justify-content: center;
width: 56px; height: 56px;
border-radius: 50%;
background: linear-gradient(135deg, $subject-cream, $subject-pink);
border: 1px solid rgba(255, 255, 255, .55);
color: $subject-ink;
font-size: 18px; line-height: 22px; font-weight: 800;
}
.gf__who { display: flex; flex-direction: column; gap: $sp-1; }
.gf__name { margin: 0; font-size: 20px; line-height: 24px; font-weight: 800; color: $subject-ink; }
.gf__role { margin: 0; font-size: 12px; line-height: 16px; font-weight: 600; color: rgba(23, 20, 26, .74); }
.gf__stats { display: flex; align-items: center; gap: $sp-4; }
.gf__stat { display: flex; flex-direction: column; align-items: center; gap: $sp-1; }
.gf__stat + .gf__stat { border-left: 1px solid rgba(23, 20, 26, .22); padding-left: $sp-4; }
.gf__num { font-size: 17px; line-height: 20px; font-weight: 800; color: $subject-ink; font-variant-numeric: tabular-nums; }
.gf__lab { font-size: 11px; line-height: 13px; font-weight: 600; color: rgba(23, 20, 26, .74); }
.gf__btn {
display: inline-flex; align-items: center; justify-content: center;
gap: $sp-1;
height: 36px;
padding: 0 $sp-4;
background: rgba(255, 255, 255, .58);
border: 1px solid rgba(255, 255, 255, .82);
border-radius: $r-pill;
color: $subject-ink;
font-size: 13px; line-height: 16px; font-weight: 700;
cursor: pointer;
transition: background $duration $easing, color $duration $easing;
}
.gf__btn svg { display: block; }
.gf__btn svg[hidden] { display: none; }
.gf__btn:hover { background: rgba(255, 255, 255, .72); }
.gf__btn:focus-visible { outline: 2px solid $color; outline-offset: 2px; }
.gf__btn[aria-pressed="true"] { background: $subject-ink; border-color: rgba(23, 20, 26, .55); color: $subject-cream; }
// ── 자동 루프: 얼룩 두 개가 서로 다른 경로로 카드 뒤를 천천히 지난다. 끝 프레임 = 첫 프레임 ──
.gf.is-demo .gf__blob--a { animation: gf-drift-a $dur-loop linear infinite; }
.gf.is-demo .gf__blob--b { animation: gf-drift-b $dur-loop linear infinite; }
@keyframes gf-drift-a {
0%, 100% { transform: translate(0, 0); }
36% { transform: translate(84px, 46px); }
70% { transform: translate(148px, 8px); }
}
@keyframes gf-drift-b {
0%, 100% { transform: translate(0, 0); }
40% { transform: translate(-64px, -38px); }
76% { transform: translate(-132px, -10px); }
}
@media (max-width: 340px) {
.gf { width: 272px; height: 146px; }
.gf__card { gap: $sp-1; padding: 0 $sp-3; }
.gf__head { gap: $sp-2; }
.gf__avatar { width: 44px; height: 44px; font-size: 14px; line-height: 18px; }
.gf__who { gap: 0; }
.gf__name { font-size: 14px; line-height: 18px; }
.gf__role { font-size: 11px; line-height: 13px; }
.gf__stats { gap: $sp-3; }
.gf__stat { gap: 0; }
.gf__stat + .gf__stat { padding-left: $sp-3; }
.gf__num { font-size: 13px; line-height: 16px; }
.gf__lab { font-size: 11px; line-height: 13px; }
.gf__btn { height: 32px; padding: 0 $sp-3; font-size: 12px; line-height: 14px; }
.gf__blob--a { width: 104px; height: 104px; }
.gf__blob--b { width: 116px; height: 116px; top: 32px; left: 148px; }
}
@media (prefers-reduced-motion: reduce) {
.gf.is-demo .gf__blob--a,
.gf.is-demo .gf__blob--b { animation: none !important; }
.gf__btn { transition: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ff4d1f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.gf {
position: relative;
width: 300px;
height: 192px;
overflow: hidden;
border-radius: 16px;
user-select: none;
-webkit-user-select: none;
}
.gf__blob {
position: absolute;
border-radius: 50%;
pointer-events: none;
}
.gf__blob--a {
top: -24px;
left: -30px;
width: 140px;
height: 140px;
background: radial-gradient(circle at 38% 38%, #ff9ebb, rgba(255, 158, 187, 0) 72%);
}
.gf__blob--b {
top: 50px;
left: 160px;
width: 150px;
height: 150px;
background: radial-gradient(circle at 62% 62%, #b9a0ff, rgba(185, 160, 255, 0) 72%);
}
.gf__card {
position: absolute;
inset: 0;
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
padding: 0 16px;
background: rgba(255, 255, 255, 0.3);
border: 1px solid rgba(255, 255, 255, 0.62);
border-radius: 16px;
backdrop-filter: blur(14px) saturate(140%);
-webkit-backdrop-filter: blur(14px) saturate(140%);
color: #17141a;
}
.gf__head {
display: flex;
align-items: center;
gap: 12px;
}
.gf__avatar {
display: flex;
align-items: center;
justify-content: center;
width: 56px;
height: 56px;
border-radius: 50%;
background: linear-gradient(135deg, #fff7e6, #ff9ebb);
border: 1px solid rgba(255, 255, 255, 0.55);
color: #17141a;
font-size: 18px;
line-height: 22px;
font-weight: 800;
}
.gf__who {
display: flex;
flex-direction: column;
gap: 4px;
}
.gf__name {
margin: 0;
font-size: 20px;
line-height: 24px;
font-weight: 800;
color: #17141a;
}
.gf__role {
margin: 0;
font-size: 12px;
line-height: 16px;
font-weight: 600;
color: rgba(23, 20, 26, 0.74);
}
.gf__stats {
display: flex;
align-items: center;
gap: 16px;
}
.gf__stat {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.gf__stat + .gf__stat {
border-left: 1px solid rgba(23, 20, 26, 0.22);
padding-left: 16px;
}
.gf__num {
font-size: 17px;
line-height: 20px;
font-weight: 800;
color: #17141a;
font-variant-numeric: tabular-nums;
}
.gf__lab {
font-size: 11px;
line-height: 13px;
font-weight: 600;
color: rgba(23, 20, 26, 0.74);
}
.gf__btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 4px;
height: 36px;
padding: 0 16px;
background: rgba(255, 255, 255, 0.58);
border: 1px solid rgba(255, 255, 255, 0.82);
border-radius: 999px;
color: #17141a;
font-size: 13px;
line-height: 16px;
font-weight: 700;
cursor: pointer;
transition: background 220ms ease-out, color 220ms ease-out;
}
.gf__btn svg {
display: block;
}
.gf__btn svg[hidden] {
display: none;
}
.gf__btn:hover {
background: rgba(255, 255, 255, 0.72);
}
.gf__btn:focus-visible {
outline: 2px solid #fff7e6;
outline-offset: 2px;
}
.gf__btn[aria-pressed=true] {
background: #17141a;
border-color: rgba(23, 20, 26, 0.55);
color: #fff7e6;
}
.gf.is-demo .gf__blob--a {
animation: gf-drift-a 2s linear infinite;
}
.gf.is-demo .gf__blob--b {
animation: gf-drift-b 2s linear infinite;
}
@keyframes gf-drift-a {
0%, 100% {
transform: translate(0, 0);
}
36% {
transform: translate(84px, 46px);
}
70% {
transform: translate(148px, 8px);
}
}
@keyframes gf-drift-b {
0%, 100% {
transform: translate(0, 0);
}
40% {
transform: translate(-64px, -38px);
}
76% {
transform: translate(-132px, -10px);
}
}
@media (max-width: 340px) {
.gf {
width: 272px;
height: 146px;
}
.gf__card {
gap: 4px;
padding: 0 12px;
}
.gf__head {
gap: 8px;
}
.gf__avatar {
width: 44px;
height: 44px;
font-size: 14px;
line-height: 18px;
}
.gf__who {
gap: 0;
}
.gf__name {
font-size: 14px;
line-height: 18px;
}
.gf__role {
font-size: 11px;
line-height: 13px;
}
.gf__stats {
gap: 12px;
}
.gf__stat {
gap: 0;
}
.gf__stat + .gf__stat {
padding-left: 12px;
}
.gf__num {
font-size: 13px;
line-height: 16px;
}
.gf__lab {
font-size: 11px;
line-height: 13px;
}
.gf__btn {
height: 32px;
padding: 0 12px;
font-size: 12px;
line-height: 14px;
}
.gf__blob--a {
width: 104px;
height: 104px;
}
.gf__blob--b {
width: 116px;
height: 116px;
top: 32px;
left: 148px;
}
}
@media (prefers-reduced-motion: reduce) {
.gf.is-demo .gf__blob--a,
.gf.is-demo .gf__blob--b {
animation: none !important;
}
.gf__btn {
transition: none;
}
}// 실제 동작: 팔로우 버튼 토글 — 아이콘 두 개를 hidden 으로 교체한다. 첫 조작에 자동 루프가 끊긴다.
var root = document.querySelector('.gf');
var btn = document.getElementById('gf-btn');
var label = document.getElementById('gf-label');
var icons = btn.querySelectorAll('svg');
root.addEventListener('pointerdown', function () { root.classList.remove('is-demo'); });
btn.addEventListener('click', function () {
root.classList.remove('is-demo');
var on = btn.getAttribute('aria-pressed') !== 'true';
btn.setAttribute('aria-pressed', String(on));
icons[0].hidden = on;
icons[1].hidden = !on;
label.textContent = on ? '팔로잉' : '팔로우';
});import { useState } from "react";
import type { CSSProperties, ReactNode } from "react";
import styles from "./GlassFrostCard.module.scss";
export type GlassFrostCardProps = {
duration?: number;
easing?: string;
color?: string;
};
type ClickEvt = { currentTarget: { blur(): void } };
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
const IconFollow = (): ReactNode => (
<svg viewBox="0 0 256 256" width="16" height="16" aria-hidden="true"><g fill="currentColor"><path d="M168 100a60 60 0 1 1-60-60a60 60 0 0 1 60 60" opacity=".2"/><path d="M256 136a8 8 0 0 1-8 8h-16v16a8 8 0 0 1-16 0v-16h-16a8 8 0 0 1 0-16h16v-16a8 8 0 0 1 16 0v16h16a8 8 0 0 1 8 8m-57.87 58.85a8 8 0 0 1-12.26 10.3C165.75 181.19 138.09 168 108 168s-57.75 13.19-77.87 37.15a8 8 0 0 1-12.25-10.3c14.94-17.78 33.52-30.41 54.17-37.17a68 68 0 1 1 71.9 0c20.65 6.76 39.23 19.39 54.18 37.17M108 152a52 52 0 1 0-52-52a52.06 52.06 0 0 0 52 52"/></g></svg>
);
const IconCheck = (): ReactNode => (
<svg viewBox="0 0 256 256" width="16" height="16" aria-hidden="true"><g fill="currentColor"><path d="M232 56v144a16 16 0 0 1-16 16H40a16 16 0 0 1-16-16V56a16 16 0 0 1 16-16h176a16 16 0 0 1 16 16" opacity=".2"/><path d="m205.66 85.66l-96 96a8 8 0 0 1-11.32 0l-40-40a8 8 0 0 1 11.32-11.32L104 164.69l90.34-90.35a8 8 0 0 1 11.32 11.32"/></g></svg>
);
export default function GlassFrostCard({
duration = 220,
easing = "ease-out",
color = "#fff7e6",
}: GlassFrostCardProps) {
const [on, setOn] = useState(false);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
function toggle(e: ClickEvt): void {
setOn((v: boolean): boolean => !v);
e.currentTarget.blur();
}
return (
<div className={styles.gf}>
<div className={styles.blobA} aria-hidden="true" />
<div className={styles.blobB} aria-hidden="true" />
<article className={styles.card}>
<div className={styles.head}>
<div className={styles.avatar}>SR</div>
<div className={styles.who}>
<p className={styles.name}>이서린</p>
<p className={styles.role}>프로덕트 디자이너</p>
</div>
</div>
<div className={styles.stats}>
<div className={styles.stat}><strong className={styles.num}>2,431</strong><span className={styles.lab}>팔로워</span></div>
<div className={styles.stat}><strong className={styles.num}>128</strong><span className={styles.lab}>게시물</span></div>
</div>
<button type="button" className={styles.btn} aria-pressed={on} onClick={toggle}>
{on ? <IconCheck /> : <IconFollow />}
<span>{on ? "팔로잉" : "팔로우"}</span>
</button>
</article>
</div>
);
}01 글래스모피즘 — 측정 누적면적 33.061% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 프로필 카드 디자인 9종 — 복붙하면 되는 스타일 모음
이 데모가 속한 글
프로필 카드 디자인 9종 — 복붙하면 되는 스타일 모음프로필 카드 디자인은 아바타·이름·직함·소셜 스탯·팔로우 버튼이라는 한 덩어리를 어떤 재질로 보여줄지 정하는 일입니다. 아홉 개는 그 안쪽 구조가 전부 같고, 바깥 재질과 움직임만 다릅니다. 이 글의 데모 전부 보기


