06다크 글로우 링
어두운 카드 위에서 아바타를 감싼 고리가 숨 쉬듯 밝아졌다 잦아들고, 같은 색 번짐이 이름과 팔로우 버튼에도 옮아 붙습니다.
쓰임게임·음악·개발자 도구처럼 어두운 화면이 기본인 서비스의 계정 카드
box-shadowdrop-shadowaria-pressed

<main class="stage">
<div class="dg is-demo">
<article class="dg__card">
<div class="dg__ava">
<span class="dg__halo" aria-hidden="true"></span>
<span class="dg__ring" aria-hidden="true"></span>
<span class="dg__avatar" aria-hidden="true"><span class="dg__init">MJ</span></span>
<span class="dg__dot" aria-hidden="true"></span>
</div>
<h2 class="dg__name">서민재</h2>
<p class="dg__role">사운드 엔지니어</p>
<p class="dg__stats">
<span class="dg__stat"><strong>3,325</strong>팔로워</span>
<span class="dg__stat"><strong>57</strong>게시물</span>
</p>
<button class="dg__follow" type="button" id="dg-follow" aria-pressed="false">
<svg class="dg__i dg__i--plus" 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 class="dg__i dg__i--check" 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>
<span class="dg__label" id="dg-label">팔로우</span>
</button>
</article>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-enter;
$easing: $ease-spring;
$color: $subject-mint;
@include stage($stage-ink);
.dg {
display: grid;
place-items: center;
user-select: none;
-webkit-user-select: none;
}
.dg__card {
display: flex;
flex-direction: column;
align-items: center;
width: 300px;
padding: $sp-4;
background: rgba(255, 255, 255, .05);
border: 1px solid rgba(255, 255, 255, .10);
border-radius: $r-card;
}
.dg__ava {
position: relative;
width: 68px;
height: 68px;
}
.dg__ring {
position: absolute;
inset: 0;
border: 2px solid $subject-mint;
border-radius: 50%;
box-shadow: 0 0 18px rgba($subject-mint, .55), inset 0 0 10px rgba($subject-mint, .45);
}
.dg__halo {
position: absolute;
inset: -8px;
border-radius: 50%;
box-shadow: 0 0 26px 4px rgba($subject-mint, .55);
opacity: .35;
pointer-events: none;
}
.dg__avatar {
position: absolute;
inset: 8px;
display: grid;
place-items: center;
border-radius: 50%;
background: linear-gradient(rgba($subject-mint, .28), rgba($subject-blue, .20)), rgba(255, 255, 255, .05);
font-size: 16px;
line-height: 20px;
font-weight: 700;
}
.dg__init {
color: $subject-cream;
filter: drop-shadow(0 0 6px rgba($subject-mint, .9));
}
.dg__dot {
position: absolute;
right: 0;
bottom: 2px;
width: 12px;
height: 12px;
border-radius: 50%;
background: $subject-mint;
box-shadow: 0 0 10px rgba($subject-mint, .8);
}
.dg__name {
margin: $sp-2 0 0;
font-size: 16px;
line-height: 20px;
font-weight: 700;
color: $subject-cream;
}
.dg__role {
margin: $sp-1 0 0;
font-size: 12px;
line-height: 16px;
font-weight: 500;
color: rgba(255, 255, 255, .6);
}
.dg__stats {
display: flex;
gap: $sp-4;
margin: $sp-3 0 0;
font-size: 12px;
line-height: 18px;
font-weight: 500;
color: rgba(255, 255, 255, .6);
}
.dg__stat strong {
margin-right: $sp-1;
font-weight: 700;
color: $subject-cream;
font-variant-numeric: tabular-nums;
}
.dg__follow {
display: inline-flex;
align-items: center;
justify-content: center;
gap: $sp-1;
height: 36px;
margin: $sp-3 0 0;
padding: 0 $sp-4;
background: rgba($subject-mint, .10);
border: 1px solid rgba($subject-mint, .7);
border-radius: $r-control;
color: $subject-cream;
font-family: inherit;
font-size: 13px;
line-height: 16px;
font-weight: 600;
cursor: pointer;
box-shadow: 0 0 14px rgba($subject-mint, .3);
transition: box-shadow $duration $easing, background $dur-quick $easing;
}
.dg__follow:hover { background: rgba($subject-mint, .2); box-shadow: 0 0 22px rgba($subject-mint, .55); }
.dg__follow:focus-visible { outline: 2px solid $color; outline-offset: 2px; }
.dg__follow[aria-pressed="true"] { box-shadow: 0 0 20px rgba($subject-mint, .5); }
.dg__i { display: inline-flex; }
.dg__i--check { display: none; color: $subject-mint; }
.dg__follow[aria-pressed="true"] .dg__i--plus { display: none; }
.dg__follow[aria-pressed="true"] .dg__i--check { display: inline-flex; }
// ── 자동 루프: 고리 번짐이 숨 쉬듯 오르내린다(아바타 고리·상태 점만 — 카드 바탕·무대는 고정).
// box-shadow 값은 고정하고 halo/dot 의 opacity·scale 만 움직인다. 복귀는 88% 안 ──
.dg.is-demo .dg__halo {
animation-name: dg-breathe;
animation-duration: $dur-loop;
animation-timing-function: $ease-out;
animation-iteration-count: infinite;
}
.dg.is-demo .dg__dot {
animation-name: dg-dot-breathe;
animation-duration: $dur-loop;
animation-timing-function: $ease-out;
animation-iteration-count: infinite;
animation-delay: 120ms;
animation-fill-mode: backwards; // 지연 구간에 선언 상태가 한 프레임 찍히는 것을 막는다
}
// 번짐의 양끝을 벌린다 — backwards 로 첫 프레임의 밝은 장이 빠지면서 강도가 14.3 까지
// 내려가 임계(15) 를 못 넘었다 (실측 2026-09-12). 밝기 폭을 넓혀 다시 올린다
@keyframes dg-breathe {
0% { opacity: .18; transform: scale(.92); }
38% { opacity: 1; transform: scale(1.10); }
62% { opacity: 1; transform: scale(1.10); }
88% { opacity: .18; transform: scale(.92); }
100% { opacity: .18; transform: scale(.92); }
}
@keyframes dg-dot-breathe {
0% { opacity: .4; transform: scale(.86); }
38% { opacity: 1; transform: scale(1.12); }
62% { opacity: 1; transform: scale(1.12); }
88% { opacity: .4; transform: scale(.86); }
100% { opacity: .4; transform: scale(.86); }
}
@media (max-width: 340px) {
.dg__card { width: 272px; padding: $sp-2 $sp-3; }
.dg__ava { width: 56px; height: 56px; }
.dg__avatar { inset: 6px; font-size: 14px; line-height: 16px; }
.dg__halo { inset: -6px; }
.dg__dot { width: 10px; height: 10px; bottom: 0; }
.dg__name { margin-top: $sp-1; font-size: 13px; line-height: 16px; }
.dg__role { margin-top: 0; font-size: 11px; line-height: 14px; }
.dg__stats { margin-top: $sp-1; gap: $sp-3; font-size: 11px; line-height: 15px; }
.dg__follow { height: 32px; margin-top: $sp-1; padding: 0 $sp-3; font-size: 12px; line-height: 14px; }
}
@media (prefers-reduced-motion: reduce) {
.dg.is-demo .dg__halo,
.dg.is-demo .dg__dot { animation: none !important; }
.dg__follow { transition: none; }
}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);
}
.dg {
display: grid;
place-items: center;
user-select: none;
-webkit-user-select: none;
}
.dg__card {
display: flex;
flex-direction: column;
align-items: center;
width: 300px;
padding: 16px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 16px;
}
.dg__ava {
position: relative;
width: 68px;
height: 68px;
}
.dg__ring {
position: absolute;
inset: 0;
border: 2px solid #4cd4a6;
border-radius: 50%;
box-shadow: 0 0 18px rgba(76, 212, 166, 0.55), inset 0 0 10px rgba(76, 212, 166, 0.45);
}
.dg__halo {
position: absolute;
inset: -8px;
border-radius: 50%;
box-shadow: 0 0 26px 4px rgba(76, 212, 166, 0.55);
opacity: 0.35;
pointer-events: none;
}
.dg__avatar {
position: absolute;
inset: 8px;
display: grid;
place-items: center;
border-radius: 50%;
background: linear-gradient(rgba(76, 212, 166, 0.28), rgba(47, 109, 246, 0.2)), rgba(255, 255, 255, 0.05);
font-size: 16px;
line-height: 20px;
font-weight: 700;
}
.dg__init {
color: #fff7e6;
filter: drop-shadow(0 0 6px rgba(76, 212, 166, 0.9));
}
.dg__dot {
position: absolute;
right: 0;
bottom: 2px;
width: 12px;
height: 12px;
border-radius: 50%;
background: #4cd4a6;
box-shadow: 0 0 10px rgba(76, 212, 166, 0.8);
}
.dg__name {
margin: 8px 0 0;
font-size: 16px;
line-height: 20px;
font-weight: 700;
color: #fff7e6;
}
.dg__role {
margin: 4px 0 0;
font-size: 12px;
line-height: 16px;
font-weight: 500;
color: rgba(255, 255, 255, 0.6);
}
.dg__stats {
display: flex;
gap: 16px;
margin: 12px 0 0;
font-size: 12px;
line-height: 18px;
font-weight: 500;
color: rgba(255, 255, 255, 0.6);
}
.dg__stat strong {
margin-right: 4px;
font-weight: 700;
color: #fff7e6;
font-variant-numeric: tabular-nums;
}
.dg__follow {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 4px;
height: 36px;
margin: 12px 0 0;
padding: 0 16px;
background: rgba(76, 212, 166, 0.1);
border: 1px solid rgba(76, 212, 166, 0.7);
border-radius: 12px;
color: #fff7e6;
font-family: inherit;
font-size: 13px;
line-height: 16px;
font-weight: 600;
cursor: pointer;
box-shadow: 0 0 14px rgba(76, 212, 166, 0.3);
transition: box-shadow 600ms cubic-bezier(0.2, 0.8, 0.2, 1), background 220ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.dg__follow:hover {
background: rgba(76, 212, 166, 0.2);
box-shadow: 0 0 22px rgba(76, 212, 166, 0.55);
}
.dg__follow:focus-visible {
outline: 2px solid #4cd4a6;
outline-offset: 2px;
}
.dg__follow[aria-pressed=true] {
box-shadow: 0 0 20px rgba(76, 212, 166, 0.5);
}
.dg__i {
display: inline-flex;
}
.dg__i--check {
display: none;
color: #4cd4a6;
}
.dg__follow[aria-pressed=true] .dg__i--plus {
display: none;
}
.dg__follow[aria-pressed=true] .dg__i--check {
display: inline-flex;
}
.dg.is-demo .dg__halo {
animation-name: dg-breathe;
animation-duration: 2s;
animation-timing-function: ease-out;
animation-iteration-count: infinite;
}
.dg.is-demo .dg__dot {
animation-name: dg-dot-breathe;
animation-duration: 2s;
animation-timing-function: ease-out;
animation-iteration-count: infinite;
animation-delay: 120ms;
animation-fill-mode: backwards;
}
@keyframes dg-breathe {
0% {
opacity: 0.18;
transform: scale(0.92);
}
38% {
opacity: 1;
transform: scale(1.1);
}
62% {
opacity: 1;
transform: scale(1.1);
}
88% {
opacity: 0.18;
transform: scale(0.92);
}
100% {
opacity: 0.18;
transform: scale(0.92);
}
}
@keyframes dg-dot-breathe {
0% {
opacity: 0.4;
transform: scale(0.86);
}
38% {
opacity: 1;
transform: scale(1.12);
}
62% {
opacity: 1;
transform: scale(1.12);
}
88% {
opacity: 0.4;
transform: scale(0.86);
}
100% {
opacity: 0.4;
transform: scale(0.86);
}
}
@media (max-width: 340px) {
.dg__card {
width: 272px;
padding: 8px 12px;
}
.dg__ava {
width: 56px;
height: 56px;
}
.dg__avatar {
inset: 6px;
font-size: 14px;
line-height: 16px;
}
.dg__halo {
inset: -6px;
}
.dg__dot {
width: 10px;
height: 10px;
bottom: 0;
}
.dg__name {
margin-top: 4px;
font-size: 13px;
line-height: 16px;
}
.dg__role {
margin-top: 0;
font-size: 11px;
line-height: 14px;
}
.dg__stats {
margin-top: 4px;
gap: 12px;
font-size: 11px;
line-height: 15px;
}
.dg__follow {
height: 32px;
margin-top: 4px;
padding: 0 12px;
font-size: 12px;
line-height: 14px;
}
}
@media (prefers-reduced-motion: reduce) {
.dg.is-demo .dg__halo,
.dg.is-demo .dg__dot {
animation: none !important;
}
.dg__follow {
transition: none;
}
}// 실제 동작: 팔로우 토글(aria-pressed) · 첫 조작에 자동 루프 종료 (정적 번짐은 유지)
var root = document.querySelector('.dg');
var btn = document.getElementById('dg-follow');
var label = document.getElementById('dg-label');
function stopDemo() { root.classList.remove('is-demo'); }
root.addEventListener('pointerdown', stopDemo);
root.addEventListener('keydown', stopDemo);
btn.addEventListener('click', function () {
var on = btn.getAttribute('aria-pressed') === 'true';
btn.setAttribute('aria-pressed', on ? 'false' : 'true');
label.textContent = on ? '팔로우' : '팔로잉';
});import { useState } from "react";
import type { CSSProperties, ReactNode } from "react";
import styles from "./DarkGlowCard.module.scss";
export type DarkGlowCardProps = {
name?: string;
role?: string;
initials?: string;
followers?: string;
posts?: string;
duration?: number;
easing?: string;
color?: string;
};
type ClickEvt = { currentTarget: { blur(): void } };
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
const IconUserPlus = (): 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 DarkGlowCard({
name = "서민재",
role = "사운드 엔지니어",
initials = "MJ",
followers = "3,325",
posts = "57",
duration = 600,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#4cd4a6",
}: DarkGlowCardProps): ReactNode {
const [on, setOn] = useState(false);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
const toggle = (e: ClickEvt): void => {
setOn((v: boolean): boolean => !v);
e.currentTarget.blur();
};
return (
<div className={styles.dg} style={vars}>
<article className={styles.card}>
<div className={styles.ava}>
<span className={styles.halo} aria-hidden="true" />
<span className={styles.ring} aria-hidden="true" />
<span className={styles.avatar} aria-hidden="true"><span className={styles.init}>{initials}</span></span>
<span className={styles.dot} aria-hidden="true" />
</div>
<h2 className={styles.name}>{name}</h2>
<p className={styles.role}>{role}</p>
<p className={styles.stats}>
<span className={styles.stat}><strong>{followers}</strong>팔로워</span>
<span className={styles.stat}><strong>{posts}</strong>게시물</span>
</p>
<button type="button" className={styles.follow} aria-pressed={on} onClick={toggle}>
{on ? <IconCheck /> : <IconUserPlus />}
<span className={styles.label}>{on ? "팔로잉" : "팔로우"}</span>
</button>
</article>
</div>
);
}06 다크 글로우 링 — 측정 누적면적 5.456% · 움직인 프레임 14/23GODRICH 원작 · zip 에 SCSS·React 포함 · 프로필 카드 디자인 9종 — 복붙하면 되는 스타일 모음
이 데모가 속한 글
프로필 카드 디자인 9종 — 복붙하면 되는 스타일 모음프로필 카드 디자인은 아바타·이름·직함·소셜 스탯·팔로우 버튼이라는 한 덩어리를 어떤 재질로 보여줄지 정하는 일입니다. 아홉 개는 그 안쪽 구조가 전부 같고, 바깥 재질과 움직임만 다릅니다. 이 글의 데모 전부 보기


