073D flip contact
The card turns half a revolution around its vertical axis so the intro on the front becomes email, phone, and website on the back, and both faces hide their reverse so no text shows through.
Best forBusiness-card style team tiles that must hold both an intro and a way to get in touch in one slot
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">Sea Oh</p>
<p class="fc__role">UX researcher</p>
<p class="fc__stats"><span class="fc__num">1,204</span> Followers · <span class="fc__num">62</span> Posts</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>Follow</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>Following</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>View contact
</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">Email</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">Phone</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">Website</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>View front
</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;
}
}// 실제 동작: View contact/앞면 보기가 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 flip contact — Measured area 43.991% · moving frames 18/23Original by GODRICH · SCSS and React ship in the zip · 9 Profile Card Design Styles, Copy-Paste Ready
The post this demo lives in
9 Profile Card Design Styles, Copy-Paste ReadyProfile card design is the job of deciding what material a fixed block of content wears: avatar, name, role, stats, foll See all demos in this post


