073D 플립 연락처
카드가 세로축을 중심으로 반 바퀴 돌아 앞면의 소개가 뒷면의 이메일·전화·웹사이트로 바뀌고, 양면 모두 뒷모습을 감춰 글자가 겹쳐 비치지 않습니다.
쓰임좁은 자리에 소개와 연락 수단을 둘 다 넣어야 하는 명함형 팀원 카드
rotateYbackface-visibilitytransform-style

<main class="stage">
<div class="fc is-demo">
<div class="fc__inner" id="fc-inner">
<div class="fc__face fc__face--front" id="fc-front">
<span class="fc__avatar" aria-hidden="true">SA</span>
<p class="fc__name">오세아</p>
<p class="fc__role">UX 리서처</p>
<p class="fc__stats"><span class="fc__num">1,204</span> 팔로워 · <span class="fc__num">62</span> 게시물</p>
<div class="fc__actions">
<button class="fc__btn fc__btn--follow" type="button" id="fc-follow" aria-pressed="false">
<span class="fc__f-off"><svg viewBox="0 0 256 256" width="14" height="14" 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>팔로우</span>
<span class="fc__f-on"><svg viewBox="0 0 256 256" width="14" height="14" 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>
</button>
<button class="fc__btn fc__btn--ghost" type="button" id="fc-flip" aria-pressed="false">
<svg viewBox="0 0 256 256" width="14" height="14" aria-hidden="true"><g fill="currentColor"><path d="M216 128a88 88 0 1 1-88-88a88 88 0 0 1 88 88" opacity=".2"/><path d="M224 48v48a8 8 0 0 1-8 8h-48a8 8 0 0 1 0-16h28.69l-14.63-14.63a79.56 79.56 0 0 0-56.13-23.43h-.45a79.52 79.52 0 0 0-55.89 22.77a8 8 0 0 1-11.18-11.44a96 96 0 0 1 135 .79L208 76.69V48a8 8 0 0 1 16 0m-37.59 135.29a80 80 0 0 1-112.47-.66L59.31 168H88a8 8 0 0 0 0-16H40a8 8 0 0 0-8 8v48a8 8 0 0 0 16 0v-28.69l14.63 14.63A95.43 95.43 0 0 0 130 222.06h.53a95.36 95.36 0 0 0 67.07-27.33a8 8 0 0 0-11.18-11.44Z"/></g></svg>연락처 보기
</button>
</div>
</div>
<div class="fc__face fc__face--back" id="fc-back" aria-hidden="true">
<ul class="fc__list">
<li class="fc__row">
<span class="fc__ic"><svg viewBox="0 0 256 256" width="15" height="15" aria-hidden="true"><g fill="currentColor"><path d="m224 56l-96 88l-96-88Z" opacity=".2"/><path d="M224 48H32a8 8 0 0 0-8 8v136a16 16 0 0 0 16 16h176a16 16 0 0 0 16-16V56a8 8 0 0 0-8-8m-20.57 16L128 133.15L52.57 64ZM216 192H40V74.19l82.59 75.71a8 8 0 0 0 10.82 0L216 74.19z"/></g></svg></span>
<span class="fc__meta"><span class="fc__k">이메일</span><span class="fc__v">[email protected]</span></span>
</li>
<li class="fc__row">
<span class="fc__ic"><svg viewBox="0 0 256 256" width="15" height="15" aria-hidden="true"><g fill="currentColor"><path d="M223.94 174.08A48.33 48.33 0 0 1 176 216A136 136 0 0 1 40 80a48.33 48.33 0 0 1 41.92-47.94a8 8 0 0 1 8.3 4.8l21.13 47.2a8 8 0 0 1-.66 7.53L89.32 117a7.93 7.93 0 0 0-.54 7.81c8.27 16.93 25.77 34.22 42.75 42.41a7.92 7.92 0 0 0 7.83-.59l25-21.3a8 8 0 0 1 7.59-.69l47.16 21.13a8 8 0 0 1 4.83 8.31" opacity=".2"/><path d="m222.37 158.46l-47.11-21.11l-.13-.06a16 16 0 0 0-15.17 1.4a8 8 0 0 0-.75.56L134.87 160c-15.42-7.49-31.34-23.29-38.83-38.51l20.78-24.71c.2-.25.39-.5.57-.77a16 16 0 0 0 1.32-15.06v-.12L97.54 33.64a16 16 0 0 0-16.62-9.52A56.26 56.26 0 0 0 32 80c0 79.4 64.6 144 144 144a56.26 56.26 0 0 0 55.88-48.92a16 16 0 0 0-9.51-16.62M176 208A128.14 128.14 0 0 1 48 80a40.2 40.2 0 0 1 34.87-40a.6.6 0 0 0 0 .12l21 47l-20.67 24.74a6 6 0 0 0-.57.77a16 16 0 0 0-1 15.7c9.06 18.53 27.73 37.06 46.46 46.11a16 16 0 0 0 15.75-1.14a8 8 0 0 0 .74-.56L168.89 152l47 21.05h.11A40.21 40.21 0 0 1 176 208"/></g></svg></span>
<span class="fc__meta"><span class="fc__k">전화</span><span class="fc__v">010-1234-5678</span></span>
</li>
<li class="fc__row">
<span class="fc__ic"><svg viewBox="0 0 256 256" width="15" height="15" aria-hidden="true"><g fill="currentColor"><path d="M224 128a96 96 0 1 1-96-96a96 96 0 0 1 96 96" opacity=".2"/><path d="M128 24a104 104 0 1 0 104 104A104.12 104.12 0 0 0 128 24m88 104a87.6 87.6 0 0 1-3.33 24h-38.51a157.4 157.4 0 0 0 0-48h38.51a87.6 87.6 0 0 1 3.33 24m-114 40h52a115.1 115.1 0 0 1-26 45a115.3 115.3 0 0 1-26-45m-3.9-16a140.8 140.8 0 0 1 0-48h59.88a140.8 140.8 0 0 1 0 48ZM40 128a87.6 87.6 0 0 1 3.33-24h38.51a157.4 157.4 0 0 0 0 48H43.33A87.6 87.6 0 0 1 40 128m114-40h-52a115.1 115.1 0 0 1 26-45a115.3 115.3 0 0 1 26 45m52.33 0h-35.62a135.3 135.3 0 0 0-22.3-45.6A88.29 88.29 0 0 1 206.37 88Zm-98.74-45.6A135.3 135.3 0 0 0 85.29 88H49.63a88.29 88.29 0 0 1 57.96-45.6M49.63 168h35.66a135.3 135.3 0 0 0 22.3 45.6A88.29 88.29 0 0 1 49.63 168m98.78 45.6a135.3 135.3 0 0 0 22.3-45.6h35.66a88.29 88.29 0 0 1-57.96 45.6"/></g></svg></span>
<span class="fc__meta"><span class="fc__k">웹사이트</span><span class="fc__v">seaoh.dev</span></span>
</li>
</ul>
<button class="fc__btn fc__btn--ghost" type="button" id="fc-unflip" aria-pressed="false" tabindex="-1">
<svg viewBox="0 0 256 256" width="14" height="14" aria-hidden="true"><g fill="currentColor"><path d="M216 128a88 88 0 1 1-88-88a88 88 0 0 1 88 88" opacity=".2"/><path d="M224 48v48a8 8 0 0 1-8 8h-48a8 8 0 0 1 0-16h28.69l-14.63-14.63a79.56 79.56 0 0 0-56.13-23.43h-.45a79.52 79.52 0 0 0-55.89 22.77a8 8 0 0 1-11.18-11.44a96 96 0 0 1 135 .79L208 76.69V48a8 8 0 0 1 16 0m-37.59 135.29a80 80 0 0 1-112.47-.66L59.31 168H88a8 8 0 0 0 0-16H40a8 8 0 0 0-8 8v48a8 8 0 0 0 16 0v-28.69l14.63 14.63A95.43 95.43 0 0 0 130 222.06h.53a95.36 95.36 0 0 0 67.07-27.33a8 8 0 0 0-11.18-11.44Z"/></g></svg>앞면 보기
</button>
</div>
</div>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-enter; // rotateY 플립 한 번의 시간
$easing: $ease-spring;
$color: $subject-blue;
@include stage($stage-orange);
.fc {
position: relative;
width: 300px;
height: 212px;
perspective: 900px;
}
.fc__inner {
position: relative;
width: 100%;
height: 100%;
transform-style: preserve-3d;
transition: transform $duration $easing;
}
.fc__inner.is-back { transform: rotateY(180deg); }
// 양면 모두 backface-visibility hidden — 빠지면 뒷면 글자가 비쳐 보인다
.fc__face {
position: absolute;
inset: 0;
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
padding: $sp-4;
border-radius: $r-card;
box-shadow: $shadow-raised;
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
}
.fc__face--front { background: #fff; color: $subject-ink; }
.fc__face--back {
background: $subject-cream;
color: $subject-ink;
transform: rotateY(180deg);
justify-content: center;
gap: $sp-4;
}
.fc__avatar {
display: flex;
align-items: center;
justify-content: center;
width: 56px;
height: 56px;
margin-bottom: $sp-2;
border-radius: 50%;
background: linear-gradient(135deg, $subject-blue, $subject-lilac);
color: $subject-ink;
font-weight: 800;
font-size: 17px;
line-height: 20px;
}
.fc__name { margin: 0; font-weight: 800; font-size: 17px; line-height: 22px; color: $subject-ink; }
.fc__role { margin: 0 0 $sp-2; font-weight: 600; font-size: 12px; line-height: 16px; color: rgba(23, 20, 26, .6); }
.fc__stats { margin: 0; font-weight: 600; font-size: 13px; line-height: 18px; color: rgba(23, 20, 26, .72); }
.fc__num { font-weight: 800; font-variant-numeric: tabular-nums; color: $subject-ink; }
.fc__actions { display: flex; gap: $sp-2; margin-top: auto; }
.fc__btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: $sp-1;
height: 36px;
padding: 0 $sp-3;
border: 0;
border-radius: $r-control;
font-weight: 700;
font-size: 13px;
line-height: 16px;
cursor: pointer;
touch-action: manipulation;
transition: background $dur-quick $easing, color $dur-quick $easing;
}
.fc__btn:focus-visible { outline: 2px solid $color; outline-offset: 2px; }
.fc__btn--follow { background: $color; color: #fff; }
.fc__btn--follow.is-on { background: rgba(23, 20, 26, .08); color: $subject-ink; }
.fc__btn--ghost {
background: transparent;
color: $subject-ink;
box-shadow: inset 0 0 0 1px rgba(23, 20, 26, .3);
}
.fc__f-off,
.fc__f-on { display: inline-flex; align-items: center; gap: $sp-1; }
.fc__f-on { display: none; }
.fc__btn--follow.is-on .fc__f-off { display: none; }
.fc__btn--follow.is-on .fc__f-on { display: inline-flex; }
.fc__list {
display: flex;
flex-direction: column;
gap: $sp-2;
margin: 0;
padding: 0;
list-style: none;
}
.fc__row { display: flex; align-items: center; gap: $sp-2; text-align: left; }
.fc__ic {
display: flex;
align-items: center;
justify-content: center;
flex: none;
width: 28px;
height: 28px;
border-radius: $r-sm;
background: rgba($color, .16);
color: $color;
}
.fc__meta { display: flex; flex-direction: column; }
.fc__k { font-weight: 600; font-size: 10px; line-height: 12px; color: rgba(23, 20, 26, .55); }
.fc__v { font-weight: 700; font-size: 13px; line-height: 16px; color: $subject-ink; font-variant-numeric: tabular-nums; }
// ── 자동 루프(격자 미리보기 전용) ──
// ⚠ 여기의 steps(1, end) 컷은 미리보기 루프에만 쓴다. 실제 클릭(연락처 보기 / 앞면 보기)은
// 위 .fc__inner 의 `transition: transform $duration $easing` 그대로 부드럽게 뒤집힌다.
// 회전은 |cos θ| 곡선이라 90° 부근에서 프레임당 변화가 언제나 최대다 — 등속·이징 어느 쪽으로도
// "주황 배경에 흰 선 하나" 인 모서리뷰가 poster 로 뽑히는 걸 못 막는다 (실측 2026-09-12).
// 컷으로 갈아끼우면 모서리뷰 프레임이 0장이고 앞면·뒷면의 완성 장면만 남는다.
// 앞면 0~30% · 뒷면 30~76% · 앞면 76~100% (복귀 76% ✓).
.fc.is-demo .fc__inner {
animation-name: fc-flip;
animation-duration: $dur-loop;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
}
@keyframes fc-flip {
0% { transform: rotateY(0deg); }
30% { transform: rotateY(180deg); }
76% { transform: rotateY(0deg); }
100% { transform: rotateY(0deg); }
}
// 컷만으로는 움직인 프레임이 2장뿐이라 양면 안쪽에 진짜 움직임을 더한다.
// 둘 다 transform 만 쓰고 페이드가 없어 반투명 중간 프레임이 생기지 않는다.
// 뒤집히는 두 프레임(30%·76%)에서는 모두 제자리 — 대표 이미지가 항상 "정돈된 면" 이 된다.
.fc.is-demo .fc__avatar {
animation-name: fc-pop;
animation-duration: $dur-loop;
animation-timing-function: $ease-spring;
animation-iteration-count: infinite;
}
@keyframes fc-pop {
0% { transform: scale(1); }
6% { transform: scale(1.09); }
22% { transform: scale(1); }
80% { transform: scale(1); }
86% { transform: scale(1.09); }
96% { transform: scale(1); }
100% { transform: scale(1); }
}
.fc.is-demo .fc__ic {
animation-name: fc-tap;
animation-duration: $dur-loop;
animation-timing-function: $ease-spring;
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.fc.is-demo .fc__row:nth-child(2) .fc__ic { animation-delay: 83ms; }
.fc.is-demo .fc__row:nth-child(3) .fc__ic { animation-delay: 167ms; }
@keyframes fc-tap {
0% { transform: scale(1); }
36% { transform: scale(1); }
42% { transform: scale(1.22); }
58% { transform: scale(1); }
100% { transform: scale(1); }
}
@media (max-width: 340px) {
.fc { width: 272px; height: 164px; }
.fc__face { padding: $sp-3; }
.fc__face--back { gap: $sp-3; }
.fc__avatar { width: 44px; height: 44px; margin-bottom: $sp-1; font-size: 14px; line-height: 17px; }
.fc__name { font-size: 15px; line-height: 18px; }
.fc__role { margin-bottom: $sp-1; font-size: 11px; line-height: 14px; }
.fc__stats { font-size: 12px; line-height: 16px; }
.fc__btn { height: 32px; padding: 0 $sp-2; font-size: 12px; line-height: 14px; }
.fc__btn svg { width: 13px; height: 13px; }
.fc__list { gap: $sp-1; }
.fc__ic { width: 24px; height: 24px; }
.fc__ic svg { width: 13px; height: 13px; }
.fc__k { font-size: 9px; line-height: 11px; }
.fc__v { font-size: 12px; line-height: 15px; }
}
@media (prefers-reduced-motion: reduce) {
.fc.is-demo .fc__inner,
.fc.is-demo .fc__avatar,
.fc.is-demo .fc__ic { animation: none !important; }
.fc__inner { transition: none; }
.fc__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);
}
.fc {
position: relative;
width: 300px;
height: 212px;
perspective: 900px;
}
.fc__inner {
position: relative;
width: 100%;
height: 100%;
transform-style: preserve-3d;
transition: transform 600ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.fc__inner.is-back {
transform: rotateY(180deg);
}
.fc__face {
position: absolute;
inset: 0;
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
padding: 16px;
border-radius: 16px;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
}
.fc__face--front {
background: #fff;
color: #17141a;
}
.fc__face--back {
background: #fff7e6;
color: #17141a;
transform: rotateY(180deg);
justify-content: center;
gap: 16px;
}
.fc__avatar {
display: flex;
align-items: center;
justify-content: center;
width: 56px;
height: 56px;
margin-bottom: 8px;
border-radius: 50%;
background: linear-gradient(135deg, #2f6df6, #b9a0ff);
color: #17141a;
font-weight: 800;
font-size: 17px;
line-height: 20px;
}
.fc__name {
margin: 0;
font-weight: 800;
font-size: 17px;
line-height: 22px;
color: #17141a;
}
.fc__role {
margin: 0 0 8px;
font-weight: 600;
font-size: 12px;
line-height: 16px;
color: rgba(23, 20, 26, 0.6);
}
.fc__stats {
margin: 0;
font-weight: 600;
font-size: 13px;
line-height: 18px;
color: rgba(23, 20, 26, 0.72);
}
.fc__num {
font-weight: 800;
font-variant-numeric: tabular-nums;
color: #17141a;
}
.fc__actions {
display: flex;
gap: 8px;
margin-top: auto;
}
.fc__btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 4px;
height: 36px;
padding: 0 12px;
border: 0;
border-radius: 12px;
font-weight: 700;
font-size: 13px;
line-height: 16px;
cursor: pointer;
touch-action: manipulation;
transition: background 220ms cubic-bezier(0.2, 0.8, 0.2, 1), color 220ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.fc__btn:focus-visible {
outline: 2px solid #2f6df6;
outline-offset: 2px;
}
.fc__btn--follow {
background: #2f6df6;
color: #fff;
}
.fc__btn--follow.is-on {
background: rgba(23, 20, 26, 0.08);
color: #17141a;
}
.fc__btn--ghost {
background: transparent;
color: #17141a;
box-shadow: inset 0 0 0 1px rgba(23, 20, 26, 0.3);
}
.fc__f-off,
.fc__f-on {
display: inline-flex;
align-items: center;
gap: 4px;
}
.fc__f-on {
display: none;
}
.fc__btn--follow.is-on .fc__f-off {
display: none;
}
.fc__btn--follow.is-on .fc__f-on {
display: inline-flex;
}
.fc__list {
display: flex;
flex-direction: column;
gap: 8px;
margin: 0;
padding: 0;
list-style: none;
}
.fc__row {
display: flex;
align-items: center;
gap: 8px;
text-align: left;
}
.fc__ic {
display: flex;
align-items: center;
justify-content: center;
flex: none;
width: 28px;
height: 28px;
border-radius: 8px;
background: rgba(47, 109, 246, 0.16);
color: #2f6df6;
}
.fc__meta {
display: flex;
flex-direction: column;
}
.fc__k {
font-weight: 600;
font-size: 10px;
line-height: 12px;
color: rgba(23, 20, 26, 0.55);
}
.fc__v {
font-weight: 700;
font-size: 13px;
line-height: 16px;
color: #17141a;
font-variant-numeric: tabular-nums;
}
.fc.is-demo .fc__inner {
animation-name: fc-flip;
animation-duration: 2s;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
}
@keyframes fc-flip {
0% {
transform: rotateY(0deg);
}
30% {
transform: rotateY(180deg);
}
76% {
transform: rotateY(0deg);
}
100% {
transform: rotateY(0deg);
}
}
.fc.is-demo .fc__avatar {
animation-name: fc-pop;
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
animation-iteration-count: infinite;
}
@keyframes fc-pop {
0% {
transform: scale(1);
}
6% {
transform: scale(1.09);
}
22% {
transform: scale(1);
}
80% {
transform: scale(1);
}
86% {
transform: scale(1.09);
}
96% {
transform: scale(1);
}
100% {
transform: scale(1);
}
}
.fc.is-demo .fc__ic {
animation-name: fc-tap;
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.fc.is-demo .fc__row:nth-child(2) .fc__ic {
animation-delay: 83ms;
}
.fc.is-demo .fc__row:nth-child(3) .fc__ic {
animation-delay: 167ms;
}
@keyframes fc-tap {
0% {
transform: scale(1);
}
36% {
transform: scale(1);
}
42% {
transform: scale(1.22);
}
58% {
transform: scale(1);
}
100% {
transform: scale(1);
}
}
@media (max-width: 340px) {
.fc {
width: 272px;
height: 164px;
}
.fc__face {
padding: 12px;
}
.fc__face--back {
gap: 12px;
}
.fc__avatar {
width: 44px;
height: 44px;
margin-bottom: 4px;
font-size: 14px;
line-height: 17px;
}
.fc__name {
font-size: 15px;
line-height: 18px;
}
.fc__role {
margin-bottom: 4px;
font-size: 11px;
line-height: 14px;
}
.fc__stats {
font-size: 12px;
line-height: 16px;
}
.fc__btn {
height: 32px;
padding: 0 8px;
font-size: 12px;
line-height: 14px;
}
.fc__btn svg {
width: 13px;
height: 13px;
}
.fc__list {
gap: 4px;
}
.fc__ic {
width: 24px;
height: 24px;
}
.fc__ic svg {
width: 13px;
height: 13px;
}
.fc__k {
font-size: 9px;
line-height: 11px;
}
.fc__v {
font-size: 12px;
line-height: 15px;
}
}
@media (prefers-reduced-motion: reduce) {
.fc.is-demo .fc__inner,
.fc.is-demo .fc__avatar,
.fc.is-demo .fc__ic {
animation: none !important;
}
.fc__inner {
transition: none;
}
.fc__btn {
transition: none;
}
}// 실제 동작: 연락처 보기/앞면 보기가 rotateY 플립을 토글하고, 안 보이는 면은 키보드 초점이 가지 않게 막는다.
var root = document.querySelector('.fc');
var inner = document.getElementById('fc-inner');
var front = document.getElementById('fc-front');
var back = document.getElementById('fc-back');
var flip = document.getElementById('fc-flip');
var unflip = document.getElementById('fc-unflip');
var follow = document.getElementById('fc-follow');
function setBack(on) {
inner.classList.toggle('is-back', on);
flip.setAttribute('aria-pressed', String(on));
unflip.setAttribute('aria-pressed', String(!on));
front.setAttribute('aria-hidden', on ? 'true' : 'false');
back.setAttribute('aria-hidden', on ? 'false' : 'true');
front.querySelectorAll('button').forEach(function (b) {
if (on) { b.setAttribute('tabindex', '-1'); } else { b.removeAttribute('tabindex'); }
});
back.querySelectorAll('button').forEach(function (b) {
if (on) { b.removeAttribute('tabindex'); } else { b.setAttribute('tabindex', '-1'); }
});
}
flip.addEventListener('click', function () { setBack(true); flip.blur(); });
unflip.addEventListener('click', function () { setBack(false); unflip.blur(); });
follow.addEventListener('click', function () {
var on = follow.getAttribute('aria-pressed') !== 'true';
follow.setAttribute('aria-pressed', String(on));
follow.classList.toggle('is-on', on);
});
['pointerdown', 'click', 'keydown'].forEach(function (ev) {
root.addEventListener(ev, function () { root.classList.remove('is-demo'); });
});import { useState } from "react";
import type { CSSProperties } from "react";
import styles from "./FlipContactCard.module.scss";
export type FlipContactCardProps = {
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
const IconUserPlus = () => (
<svg viewBox="0 0 256 256" width="14" height="14" 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 = () => (
<svg viewBox="0 0 256 256" width="14" height="14" 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>
);
const IconFlip = () => (
<svg viewBox="0 0 256 256" width="14" height="14" aria-hidden="true"><g fill="currentColor"><path d="M216 128a88 88 0 1 1-88-88a88 88 0 0 1 88 88" opacity=".2"/><path d="M224 48v48a8 8 0 0 1-8 8h-48a8 8 0 0 1 0-16h28.69l-14.63-14.63a79.56 79.56 0 0 0-56.13-23.43h-.45a79.52 79.52 0 0 0-55.89 22.77a8 8 0 0 1-11.18-11.44a96 96 0 0 1 135 .79L208 76.69V48a8 8 0 0 1 16 0m-37.59 135.29a80 80 0 0 1-112.47-.66L59.31 168H88a8 8 0 0 0 0-16H40a8 8 0 0 0-8 8v48a8 8 0 0 0 16 0v-28.69l14.63 14.63A95.43 95.43 0 0 0 130 222.06h.53a95.36 95.36 0 0 0 67.07-27.33a8 8 0 0 0-11.18-11.44Z"/></g></svg>
);
const IconMail = () => (
<svg viewBox="0 0 256 256" width="15" height="15" aria-hidden="true"><g fill="currentColor"><path d="m224 56l-96 88l-96-88Z" opacity=".2"/><path d="M224 48H32a8 8 0 0 0-8 8v136a16 16 0 0 0 16 16h176a16 16 0 0 0 16-16V56a8 8 0 0 0-8-8m-20.57 16L128 133.15L52.57 64ZM216 192H40V74.19l82.59 75.71a8 8 0 0 0 10.82 0L216 74.19z"/></g></svg>
);
const IconPhone = () => (
<svg viewBox="0 0 256 256" width="15" height="15" aria-hidden="true"><g fill="currentColor"><path d="M223.94 174.08A48.33 48.33 0 0 1 176 216A136 136 0 0 1 40 80a48.33 48.33 0 0 1 41.92-47.94a8 8 0 0 1 8.3 4.8l21.13 47.2a8 8 0 0 1-.66 7.53L89.32 117a7.93 7.93 0 0 0-.54 7.81c8.27 16.93 25.77 34.22 42.75 42.41a7.92 7.92 0 0 0 7.83-.59l25-21.3a8 8 0 0 1 7.59-.69l47.16 21.13a8 8 0 0 1 4.83 8.31" opacity=".2"/><path d="m222.37 158.46l-47.11-21.11l-.13-.06a16 16 0 0 0-15.17 1.4a8 8 0 0 0-.75.56L134.87 160c-15.42-7.49-31.34-23.29-38.83-38.51l20.78-24.71c.2-.25.39-.5.57-.77a16 16 0 0 0 1.32-15.06v-.12L97.54 33.64a16 16 0 0 0-16.62-9.52A56.26 56.26 0 0 0 32 80c0 79.4 64.6 144 144 144a56.26 56.26 0 0 0 55.88-48.92a16 16 0 0 0-9.51-16.62M176 208A128.14 128.14 0 0 1 48 80a40.2 40.2 0 0 1 34.87-40a.6.6 0 0 0 0 .12l21 47l-20.67 24.74a6 6 0 0 0-.57.77a16 16 0 0 0-1 15.7c9.06 18.53 27.73 37.06 46.46 46.11a16 16 0 0 0 15.75-1.14a8 8 0 0 0 .74-.56L168.89 152l47 21.05h.11A40.21 40.21 0 0 1 176 208"/></g></svg>
);
const IconGlobe = () => (
<svg viewBox="0 0 256 256" width="15" height="15" aria-hidden="true"><g fill="currentColor"><path d="M224 128a96 96 0 1 1-96-96a96 96 0 0 1 96 96" opacity=".2"/><path d="M128 24a104 104 0 1 0 104 104A104.12 104.12 0 0 0 128 24m88 104a87.6 87.6 0 0 1-3.33 24h-38.51a157.4 157.4 0 0 0 0-48h38.51a87.6 87.6 0 0 1 3.33 24m-114 40h52a115.1 115.1 0 0 1-26 45a115.3 115.3 0 0 1-26-45m-3.9-16a140.8 140.8 0 0 1 0-48h59.88a140.8 140.8 0 0 1 0 48ZM40 128a87.6 87.6 0 0 1 3.33-24h38.51a157.4 157.4 0 0 0 0 48H43.33A87.6 87.6 0 0 1 40 128m114-40h-52a115.1 115.1 0 0 1 26-45a115.3 115.3 0 0 1 26 45m52.33 0h-35.62a135.3 135.3 0 0 0-22.3-45.6A88.29 88.29 0 0 1 206.37 88Zm-98.74-45.6A135.3 135.3 0 0 0 85.29 88H49.63a88.29 88.29 0 0 1 57.96-45.6M49.63 168h35.66a135.3 135.3 0 0 0 22.3 45.6A88.29 88.29 0 0 1 49.63 168m98.78 45.6a135.3 135.3 0 0 0 22.3-45.6h35.66a88.29 88.29 0 0 1-57.96 45.6"/></g></svg>
);
export default function FlipContactCard({
duration = 600,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#2f6df6",
}: FlipContactCardProps) {
const [back, setBack] = useState(false);
const [following, setFollowing] = useState(false);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
const followClass = following
? `${styles.btn} ${styles.btnFollow} ${styles.isOn}`
: `${styles.btn} ${styles.btnFollow}`;
return (
<div className={styles.fc} style={vars}>
<div className={back ? `${styles.inner} ${styles.isBack}` : styles.inner}>
<div className={`${styles.face} ${styles.faceFront}`} aria-hidden={back ? "true" : undefined}>
<span className={styles.avatar} aria-hidden="true">SA</span>
<p className={styles.name}>오세아</p>
<p className={styles.role}>UX 리서처</p>
<p className={styles.stats}>
<span className={styles.num}>1,204</span> 팔로워 · <span className={styles.num}>62</span> 게시물
</p>
<div className={styles.actions}>
<button
type="button"
className={followClass}
aria-pressed={following}
tabIndex={back ? -1 : undefined}
onClick={(): void => setFollowing(!following)}
>
<span className={styles.fOff}><IconUserPlus />팔로우</span>
<span className={styles.fOn}><IconCheck />팔로잉</span>
</button>
<button
type="button"
className={`${styles.btn} ${styles.btnGhost}`}
aria-pressed={back}
tabIndex={back ? -1 : undefined}
onClick={(): void => setBack(true)}
>
<IconFlip />연락처 보기
</button>
</div>
</div>
<div className={`${styles.face} ${styles.faceBack}`} aria-hidden={back ? undefined : "true"}>
<ul className={styles.list}>
<li className={styles.row}>
<span className={styles.ic}><IconMail /></span>
<span className={styles.meta}><span className={styles.k}>이메일</span><span className={styles.v}>[email protected]</span></span>
</li>
<li className={styles.row}>
<span className={styles.ic}><IconPhone /></span>
<span className={styles.meta}><span className={styles.k}>전화</span><span className={styles.v}>010-1234-5678</span></span>
</li>
<li className={styles.row}>
<span className={styles.ic}><IconGlobe /></span>
<span className={styles.meta}><span className={styles.k}>웹사이트</span><span className={styles.v}>seaoh.dev</span></span>
</li>
</ul>
<button
type="button"
className={`${styles.btn} ${styles.btnGhost}`}
aria-pressed={!back}
tabIndex={back ? undefined : -1}
onClick={(): void => setBack(false)}
>
<IconFlip />앞면 보기
</button>
</div>
</div>
</div>
);
}07 3D 플립 연락처 — 측정 누적면적 43.991% · 움직인 프레임 18/23GODRICH 원작 · zip 에 SCSS·React 포함 · 프로필 카드 디자인 9종 — 복붙하면 되는 스타일 모음
이 데모가 속한 글
프로필 카드 디자인 9종 — 복붙하면 되는 스타일 모음프로필 카드 디자인은 아바타·이름·직함·소셜 스탯·팔로우 버튼이라는 한 덩어리를 어떤 재질로 보여줄지 정하는 일입니다. 아홉 개는 그 안쪽 구조가 전부 같고, 바깥 재질과 움직임만 다릅니다. 이 글의 데모 전부 보기


