프로필 카드 디자인 9종 — 복붙하면 되는 스타일 모음
프로필 카드 디자인은 아바타·이름·직함·소셜 스탯·팔로우 버튼이라는 한 덩어리를 어떤 재질로 보여줄지 정하는 일입니다. 아홉 개는 그 안쪽 구조가 전부 같고, 바깥 재질과 움직임만 다릅니다.
자동 재생 · 눌러서 해당 항목으로 · 전부 한 zip
- 01 글래스모피즘
- 02 네오브루탈리즘 오프셋
- 03 클레이모피즘
- 04 미니멀 라인
- 05 회전하는 그라디언트 테두리
- 06 다크 글로우 링
- 07 3D 플립 연락처
- 08 소셜 스탯 카운트업
- 09 팀 카드 그리드 확장
앞의 여섯은 카드 한 장의 표면만 바꿉니다. 마크업도 글자도 버튼도 같고 재질만 달라지므로, 하나를 골라 붙인 뒤 스타일 블록만 갈아 끼우면 다른 다섯 개가 됩니다. 순서는 빛을 얼마나 다루느냐입니다. 뒤에 있는 것을 흐리게 비추는 유리(01)에서 시작해, 빛 대신 그림자를 도형처럼 쓰는 네오브루탈리즘(02), 안팎 그림자를 겹쳐 부피를 만드는 점토(03), 그림자를 전부 버리고 선만 남긴 미니멀(04), 테두리만 빛나는 그라디언트(05), 어둠 속에서 번지는 네온(06)으로 갑니다. 뒤의 셋은 표면이 아니라 구조를 하나씩 더 갖습니다 — 뒷면(07), 시간(08), 이웃(09). 아홉 개 전부 팔로우 버튼이 실제로 aria-pressed 를 바꾸고 글자와 아이콘을 갈아 끼웁니다.
01글래스모피즘
카드 뒤에서 색 얼룩 두 개가 천천히 흘러가고, 반투명한 카드가 그 색을 흐리게 비춥니다. 서리 낀 유리를 만드는 것은 backdrop-filter 한 줄이지만, 그 뒤에 실제로 흐려질 것이 있어야 효과가 보입니다.
.gf__card {
position: absolute;
inset: 0;
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__blob--a {
top: -24px; left: -30px;
width: 140px; height: 140px;
background: radial-gradient(circle at 38% 38%, $subject-pink, rgba($subject-pink, 0) 72%);
}
02네오브루탈리즘 오프셋
굵은 테두리와 오른쪽 아래로 어긋난 단색 그림자가 카드를 오려 붙인 종이처럼 띄웁니다. 누르면 카드가 그림자 자리로 정확히 내려앉고 그림자는 사라집니다.
.nb__card {
border: 3px solid $subject-ink;
border-radius: $r-card;
box-shadow: 8px 8px 0 $subject-ink;
}
@keyframes nb-press {
0%, 100% { transform: translate(0, 0); box-shadow: 8px 8px 0 $subject-ink; }
9% { transform: translate(8px, 8px); box-shadow: 0 0 0 $subject-ink; }
20% { transform: translate(0, 0); box-shadow: 8px 8px 0 $subject-ink; }
46% { transform: translate(0, 0); box-shadow: 8px 8px 0 $subject-ink; }
55% { transform: translate(8px, 8px); box-shadow: 0 0 0 $subject-ink; }
68% { transform: translate(0, 0); box-shadow: 8px 8px 0 $subject-ink; }
}
03클레이모피즘
그림자를 세 겹 겹치면 카드가 부풀어 오른 점토처럼 보입니다. 바깥으로 퍼지는 그림자 하나에 안쪽의 밝은 하이라이트와 어두운 그늘을 더한 조합이 말랑한 부피를 만듭니다.
.cp__card {
border-radius: $r-card;
background: rgba($subject-pink, .28);
box-shadow:
0 18px 30px rgba(23, 20, 26, .14),
inset 6px 6px 12px rgba(255, 255, 255, .9),
inset -6px -6px 12px rgba(23, 20, 26, .10);
}
@keyframes cp-breathe {
0%, 100% { transform: scale(1); }
44% { transform: scale(1.12); }
88% { transform: scale(1); }
}
04미니멀 라인
흰 배경 위의 흰 카드라 배경과 대비가 없습니다. 1px 선과 여백만으로 형태를 잡고, 왼쪽부터 그어지는 이름 밑줄 하나가 유일한 장식입니다.
.ml__line {
display: block;
height: 2px;
background: $subject-ink;
transform: scaleX(0);
transform-origin: left center;
transition: transform $duration $easing;
}
.ml__name:hover .ml__line--name,
.ml__follow:hover .ml__line--btn,
.ml__follow:focus-visible .ml__line--btn,
.ml__follow[aria-pressed="true"] .ml__line--btn { transform: scaleX(1); }
05회전하는 그라디언트 테두리
원뿔 그라디언트가 카드 둘레를 한 바퀴 돌고, 안쪽 판이 그 위를 덮어 4px 테두리만 빛으로 남습니다. 각도를 담는 커스텀 속성을 @property 로 등록하는 것이 이 항목의 전부입니다.
@property --angle {
syntax: "<angle>";
initial-value: 0deg;
inherits: false;
}
.gr__rim {
padding: $sp-1;
background: conic-gradient(from var(--angle),
$subject-blue, $subject-pink, $subject-mint, $subject-lilac, $subject-blue);
animation-name: gr-spin;
animation-duration: $dur-loop;
animation-timing-function: linear;
animation-iteration-count: infinite;
}
@keyframes gr-spin { to { --angle: 360deg; } }
06다크 글로우 링
어두운 카드 위에서 아바타를 감싼 고리가 숨 쉬듯 밝아졌다 잦아듭니다. 고리에는 box-shadow 를 안팎으로 걸고, 글자에는 모양을 따라가는 drop-shadow 를 걸어 번짐을 나눕니다.
.dg__ring {
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__init {
color: $subject-cream;
filter: drop-shadow(0 0 6px rgba($subject-mint, .9));
}
@keyframes dg-breathe {
0% { opacity: .18; transform: scale(.92); }
38% { opacity: 1; transform: scale(1.10); }
62% { opacity: 1; transform: scale(1.10); }
}
073D 플립 연락처
카드가 세로축을 중심으로 반 바퀴 돌아 앞면 소개가 뒷면 연락처로 바뀝니다. 양면 모두 자기 뒷모습을 감춰야 반대편 글자가 거울처럼 비치지 않습니다.
.fc { perspective: 900px; width: 300px; height: 212px; }
.fc__inner {
position: relative;
width: 100%;
height: 100%;
transform-style: preserve-3d;
transition: transform $duration $easing;
}
.fc__inner.is-back { transform: rotateY(180deg); }
.fc__face {
position: absolute;
inset: 0;
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
}
.fc__face--back { transform: rotateY(180deg); }
08소셜 스탯 카운트업
세 숫자가 0에서 실제 값까지 프레임마다 올라갑니다. 팔로우를 누르면 팔로워 값이 1 늘어나고 그 숫자만 다시 세며, 이전 카운트가 겹쳐 돌지 않도록 실행마다 번호를 매깁니다.
function countTo(i, to, ms) {
var run = ++runs[i];
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
current[i] = to;
nums[i].textContent = commas(to);
return;
}
var from = current[i], t0 = performance.now();
function frame(now) {
if (runs[i] !== run) return;
var p = Math.min(1, (now - t0) / ms);
current[i] = Math.round(from + (to - from) * easeOutCubic(p));
nums[i].textContent = commas(current[i]);
if (p < 1) requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
}
09팀 카드 그리드 확장
타일 하나가 넓어지면 직함과 팔로우 버튼이 아래에서 올라오고, 나머지 둘이 같은 시간 동안 좁아져 전체 폭이 그대로 유지됩니다. 좌우 화살표로 초점을 옮길 때는 화면이 따라 스크롤되지 않게 막습니다.
mains[i].addEventListener('keydown', function (e) {
if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') return;
e.preventDefault();
root.classList.remove('is-demo');
var next = (i + (e.key === 'ArrowRight' ? 1 : tiles.length - 1)) % tiles.length;
open(next);
mains[next].focus({ preventScroll: true });
});
어디서 깨지는가 — 함정
이 편의 함정은 스타일이 아니라 썸네일에서 나왔습니다. 격자와 공유 카드에 쓰는 대표 이미지는 2초를 24장으로 뽑은 뒤 "직전 장과 가장 크게 달라진 장"을 고르는데, 07번 플립이 늘 정확히 모서리에서 본 장면으로 뽑혔습니다. 주황 배경에 흰 선 한 줄만 남은 그림이었습니다. 우연이 아닙니다. 세로축으로 돌아간 면의 화면 폭은 회전각의 코사인이고, 그 변화율은 사인이라 90도 부근에서 언제나 최대입니다. 등속으로 돌리든 이징을 넣든 가장 크게 변한 장은 모서리입니다.
같은 병이 세 항목에 더 있었습니다. 08번 스탯 세 칸과 09번 직함·버튼을 투명도로 부드럽게 띄웠더니 대표 이미지가 반투명 유령이 됐고, 04번 밑줄은 등속으로 그어서 시작 직후의 짧은 작대기가 뽑혀 오타처럼 보였습니다. 고친 방향은 셋 다 같습니다. 서서히 바뀌는 구간을 없애고, 완성된 상태가 한 장에 떨어지게 맞췄습니다. 08번 스탯 칸과 09번 직함·버튼은 등장과 퇴장을 steps(1, end) 로 한 장에 갈아 끼웠고, 04번 밑줄은 마지막 한 칸을 한 장 간격인 83.3밀리초 안에 몰아 넣어 선이 끝까지 그어지는 장이 최대 변화가 되게 했습니다. 07번은 미리보기 루프만 컷으로 바꾸고 실제 클릭은 부드러운 전환 그대로 뒀습니다. 컷으로 바꾸자 회전하는 동안 화면을 쓸고 지나가던 면적이 빠지면서 변한 픽셀 면적이 56.2%에서 44.0%로 내려갔고, 대신 양면 안쪽에 투명도를 쓰지 않는 작은 움직임을 넣어 움직인 장을 10장에서 18장으로 올렸습니다. 컷 하나만 남기면 움직임이 두 장뿐이라 애니메이션이 없는 것으로 측정되기 때문입니다.
지연을 준 애니메이션에서 하나 더 걸렸습니다. animation-delay 만 주고 animation-fill-mode: backwards 를 빼면 지연 구간 동안 요소가 원래 상태 그대로 그려집니다. 08번 스탯 세 칸이 첫 장에 다 보였다가 꺼진 뒤 차례로 다시 켜졌습니다. 지연 구간에는 애니메이션이 아직 적용되지 않는데, 프레임을 뽑는 쪽은 그 시각을 그대로 찍기 때문입니다. 시차를 줄 때 animation 단축을 쓰면 지연 자체가 0으로 리셋되는 것도 같은 계열입니다 — 지연을 쓰는 03·04·06·07·08·09번은 전부 낱개 속성으로 씁니다.
접근성
색 대비는 짐작하지 않고 합성한 실제 색으로 계산했습니다. 반투명 판은 아래 배경과 섞인 뒤의 색이 진짜 배경이라, 눈으로는 흰 글자가 잘 보이는 것 같아도 숫자가 다릅니다.
| 항목 | 글자 / 바탕 | 대비 | 판정 |
|---|---|---|---|
| 01 글래스 | 먹색 / 서리판 (255, 130, 98) | 7.49:1 | 통과 |
| 01 글래스 (버린 안) | 흰색 / 같은 서리판 | 2.44:1 | 미달 |
| 02 네오브루탈 | 흰색 / 파랑 알약 | 4.53:1 | 통과 |
| 03 클레이 | 먹색 62% / 분홍 카드 | 4.79:1 | 통과 |
| 04 미니멀 | 먹색 64% / 흰 카드 | 5.44:1 | 통과 |
| 06 다크 | 민트 상태 점 / 어두운 판 | 8.64:1 | 통과 |
| 09 팀 | 크림 / 펼친 타일 | 10.99:1 | 통과 |
01번은 처음에 흰 글자였고 2.44:1 로 기준 미달이었습니다. 서리판을 더 희게 올리고 글자를 먹색으로 바꾸자 7.49:1 이 됐습니다. 04번 보조 글자도 투명도 55%에서 4.04:1 이라 64%로 올렸습니다. 이렇게 대비까지 손본 아홉 벌 소스는 압축 비밀번호 kx4qjk9q를 넣으면 열리는 zip 안에 있고, 바닐라와 React 판이 같은 값을 씁니다.
prefers-reduced-motion: reduce 에서는 아홉 개 모두 자동 재생 루프를 끕니다. 01번 얼룩은 제자리에 서고, 02번은 눌림 반복 대신 실제 :active 만 남고, 03번 호흡과 06번 번짐은 기본 상태에서 멈추되 점토 그림자와 고리 불빛은 그대로 보이고, 05번 테두리는 회전을 멈추되 그라디언트는 그대로 보입니다. 07번은 뒤집기 전환이 꺼지고 클릭하면 즉시 뒷면이 되며, 08번은 스탯 칸이 차례로 떠오르는 루프가 멈추고, 09번은 펼치기 전환 없이 상태만 바뀝니다. 08번 숫자 카운트업만 CSS 가 아니라 자바스크립트라 미디어 질의가 자동으로 닿지 않으므로, 바닐라와 React 판 양쪽에서 matchMedia('(prefers-reduced-motion: reduce)') 를 한 번 읽어 세지 않고 최종값을 바로 넣습니다. 어느 경우에도 팔로우 여부·펼침 여부 같은 상태 값은 남습니다. 07번의 3D 회전 속성은 MDN 의 transform-style 문서에 정리돼 있습니다.
같은 카드 계열을 더 찾는다면 CSS 카드 효과 9가지와 로그인 화면 디자인 9가지가 이웃입니다.
FAQ
아바타 이미지 없이 이니셜만 써도 괜찮나요?
괜찮습니다. 아홉 개 전부 이미지 파일 없이 CSS 로 칠한 원 안에 이니셜 두 글자를 넣었습니다 — 대부분 그라디언트이고 03·04번만 단색입니다. 사진이 아직 없는 계정에서 회색 사람 모양 대신 쓰면 빈 자리가 덜 눈에 띄고, 네트워크 요청이 하나도 늘지 않습니다. 나중에 사진이 생기면 원 안쪽을 background-image 로 덮기만 하면 되고 바깥 크기와 테두리는 그대로 둡니다.
backdrop-filter 를 넣었는데 아무것도 안 흐려집니다
그 요소 뒤에 흐려질 것이 없기 때문입니다. backdrop-filter 는 요소 자신이 아니라 뒤에 깔린 것을 흐리게 하므로, 단색 배경 위에 올리면 흐린 단색이 되어 차이가 안 보입니다. 01번처럼 카드 뒤에 색 얼룩이나 사진을 깔아야 효과가 드러납니다. 카드 배경이 완전히 불투명해도 마찬가지로 효과가 드러나지 않으니 알파 값을 반드시 1 미만으로 둡니다.
그라디언트 테두리가 안 돕니다
@property 등록이 빠졌을 가능성이 큽니다. 그냥 선언한 커스텀 속성은 브라우저가 문자열로만 다루기 때문에 두 값 사이를 보간하지 못하고, 각도가 0도에서 360도로 한 번에 점프해 멈춘 것처럼 보입니다. 05번처럼 syntax: "<angle>" 과 initial-value 를 적어 등록해야 각도가 실제로 이어집니다.