02Team member card
A contact row hidden under the name and role unfolds with clip-path when the card takes focus, and the link itself carries the person's name.
Best forAn about-us team section, a hiring page
clip-pathfocus-withintransition

<main class="stage">
<ul class="tm is-demo" id="tm">
<li class="tm__card">
<span class="tm__face" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256"><g fill="currentColor"><path d="M230.14 219.93a8 8 0 0 1-6.14 2.07H32a8 8 0 0 1-6.14-2.07a8 8 0 0 1-1.63-8.28C33.79 187.16 55 168.51 80.6 159.86a4 4 0 0 1 3.38.31a75.24 75.24 0 0 0 88 0a4 4 0 0 1 3.43-.31c25.62 8.65 46.81 27.3 56.37 51.79a8 8 0 0 1-1.64 8.28M168 88a40 40 0 1 1-40-40a40 40 0 0 1 40 40" opacity=".2"/><path d="M230.92 212c-15.23-26.33-38.7-45.21-66.09-54.16a72 72 0 1 0-73.66 0c-27.39 8.94-50.86 27.82-66.09 54.16a8 8 0 1 0 13.85 8c18.84-32.56 52.14-52 89.07-52s70.23 19.44 89.07 52a8 8 0 1 0 13.85-8M72 96a56 56 0 1 1 56 56a56.06 56.06 0 0 1-56-56"/></g></svg></span>
<h3 class="tm__name">Seoyeon Kim</h3>
<p class="tm__role">Product designer</p>
<p class="tm__contact">
<span class="tm__ic" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256"><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>
<a class="tm__link" href="#seoyeon">Email Seoyeon</a>
</p>
</li>
<li class="tm__card">
<span class="tm__face" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256"><g fill="currentColor"><path d="M230.14 219.93a8 8 0 0 1-6.14 2.07H32a8 8 0 0 1-6.14-2.07a8 8 0 0 1-1.63-8.28C33.79 187.16 55 168.51 80.6 159.86a4 4 0 0 1 3.38.31a75.24 75.24 0 0 0 88 0a4 4 0 0 1 3.43-.31c25.62 8.65 46.81 27.3 56.37 51.79a8 8 0 0 1-1.64 8.28M168 88a40 40 0 1 1-40-40a40 40 0 0 1 40 40" opacity=".2"/><path d="M230.92 212c-15.23-26.33-38.7-45.21-66.09-54.16a72 72 0 1 0-73.66 0c-27.39 8.94-50.86 27.82-66.09 54.16a8 8 0 1 0 13.85 8c18.84-32.56 52.14-52 89.07-52s70.23 19.44 89.07 52a8 8 0 1 0 13.85-8M72 96a56 56 0 1 1 56 56a56.06 56.06 0 0 1-56-56"/></g></svg></span>
<h3 class="tm__name">Doyun Park</h3>
<p class="tm__role">Frontend developer</p>
<p class="tm__contact">
<span class="tm__ic" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256"><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>
<a class="tm__link" href="#doyun">Email Doyun</a>
</p>
</li>
<li class="tm__card">
<span class="tm__face" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256"><g fill="currentColor"><path d="M230.14 219.93a8 8 0 0 1-6.14 2.07H32a8 8 0 0 1-6.14-2.07a8 8 0 0 1-1.63-8.28C33.79 187.16 55 168.51 80.6 159.86a4 4 0 0 1 3.38.31a75.24 75.24 0 0 0 88 0a4 4 0 0 1 3.43-.31c25.62 8.65 46.81 27.3 56.37 51.79a8 8 0 0 1-1.64 8.28M168 88a40 40 0 1 1-40-40a40 40 0 0 1 40 40" opacity=".2"/><path d="M230.92 212c-15.23-26.33-38.7-45.21-66.09-54.16a72 72 0 1 0-73.66 0c-27.39 8.94-50.86 27.82-66.09 54.16a8 8 0 1 0 13.85 8c18.84-32.56 52.14-52 89.07-52s70.23 19.44 89.07 52a8 8 0 1 0 13.85-8M72 96a56 56 0 1 1 56 56a56.06 56.06 0 0 1-56-56"/></g></svg></span>
<h3 class="tm__name">Hajun Lee</h3>
<p class="tm__role">Data analyst</p>
<p class="tm__contact">
<span class="tm__ic" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256"><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>
<a class="tm__link" href="#hajun">Email Hajun</a>
</p>
</li>
</ul>
</main>@use "../../../../site/static/demo-tokens" as *;
// 팀원 소개 카드 — 숨은 연락 줄을 clip-path 로 아래에서 위로 걷어낸다.
// 자리는 처음부터 잡혀 있고(높이가 변하지 않는다) 보이는 범위만 바뀌므로 옆 카드가 밀리지 않는다.
// 여는 조건은 :hover 와 :focus-within 둘 다 — 키보드로 링크에 닿아도 같은 상태가 보인다.
// 카드 전체를 클릭 영역으로 만들지 않는다. 링크 글자가 "누구에게" 가는 길인지 스스로 말한다.
$duration: $dur-enter;
$easing: $ease-spring;
$color: $subject-blue;
$edge: rgba($subject-ink, .14);
@include stage($stage-yellow);
.tm {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: $sp-2;
box-sizing: border-box; width: min(100%, 440px);
margin: 0; padding: 0; list-style: none;
}
.tm__card {
display: flex; flex-direction: column; align-items: flex-start; gap: $sp-1;
box-sizing: border-box;
padding: $sp-3;
border-radius: $r-card;
background: $stage-paper;
}
// 인물 사진이 들어갈 자리. 지금은 토큰 색 원 + 아이콘 (run/081/_이미지후보.A.json 에 기록)
.tm__face {
display: grid; place-items: center;
width: clamp(28px, 9vw, 40px); height: clamp(28px, 9vw, 40px);
border-radius: 50%;
background: $color; color: $subject-ink;
font-size: clamp(18px, 6vw, 26px);
}
.tm__name {
margin: 0;
color: $subject-ink;
font-weight: 700; font-size: clamp(12px, 3.4vw, 15px); line-height: 1.25;
}
.tm__role {
margin: 0;
color: rgba($subject-ink, .72);
font-weight: 500; font-size: clamp(8px, 2.4vw, 10px); line-height: 1.3;
}
// ── 숨은 연락 줄. 높이는 늘 차지하고 clip-path 만 움직인다
.tm__contact {
display: flex; align-items: center; gap: $sp-1;
width: 100%; margin: 0; padding-top: $sp-1;
border-top: 1px solid $edge;
clip-path: inset(0 0 100% 0);
transition: clip-path $duration $easing;
}
.tm__card:hover .tm__contact,
.tm__card:focus-within .tm__contact { clip-path: inset(0 0 0 0); }
.tm__ic { display: inline-grid; place-items: center; flex: 0 0 auto; color: $color; font-size: 12px; }
.tm__link {
color: $subject-ink;
font-weight: 700; font-size: clamp(8px, 2.4vw, 10px); line-height: 1.3;
text-decoration: underline; text-underline-offset: 2px;
}
.tm__link:focus-visible { outline: 2px solid $subject-ink; outline-offset: 2px; border-radius: $r-xs; }
// ── 격자 미리보기 루프 (2초). 한 장씩 열렸다 닫히며 세 장을 다 보여준다.
// 같은 키프레임을 0 / .6 / 1.2초로 물려 세 카드가 차례를 나눠 쓴다 —
// animation-delay 를 쓰면 animation-fill-mode: backwards 가 반드시 함께 간다.
.tm.is-demo .tm__contact {
animation-name: tmOpen;
animation-duration: $dur-loop;
animation-timing-function: $easing;
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.tm.is-demo .tm__card:nth-child(2) .tm__contact { animation-delay: .6s; }
.tm.is-demo .tm__card:nth-child(3) .tm__contact { animation-delay: 1.2s; }
// clip-path 위프는 이징 짝으로 둔다 — steps 타이밍과 섞으면 보간이 죽어 컷이 된다
@keyframes tmOpen {
0% { clip-path: inset(0 0 100% 0); }
8% { clip-path: inset(0 0 0 0); }
22% { clip-path: inset(0 0 0 0); }
30%, 100% { clip-path: inset(0 0 100% 0); }
}
// ── 좁은 화면: 세 칸을 지키고 여백만 줄인다
@media (max-width: 340px) {
.tm { gap: $sp-1; }
.tm__card { padding: $sp-1; }
.tm__ic { font-size: 10px; }
}
@media (prefers-reduced-motion: reduce) {
.tm.is-demo .tm__contact {
animation: none !important;
clip-path: inset(0 0 0 0) !important;
transition: none !important;
}
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffd23f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.tm {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
box-sizing: border-box;
width: min(100%, 440px);
margin: 0;
padding: 0;
list-style: none;
}
.tm__card {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 4px;
box-sizing: border-box;
padding: 12px;
border-radius: 16px;
background: #ffffff;
}
.tm__face {
display: grid;
place-items: center;
width: clamp(28px, 9vw, 40px);
height: clamp(28px, 9vw, 40px);
border-radius: 50%;
background: #2f6df6;
color: #17141a;
font-size: clamp(18px, 6vw, 26px);
}
.tm__name {
margin: 0;
color: #17141a;
font-weight: 700;
font-size: clamp(12px, 3.4vw, 15px);
line-height: 1.25;
}
.tm__role {
margin: 0;
color: rgba(23, 20, 26, 0.72);
font-weight: 500;
font-size: clamp(8px, 2.4vw, 10px);
line-height: 1.3;
}
.tm__contact {
display: flex;
align-items: center;
gap: 4px;
width: 100%;
margin: 0;
padding-top: 4px;
border-top: 1px solid rgba(23, 20, 26, 0.14);
clip-path: inset(0 0 100% 0);
transition: clip-path 600ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.tm__card:hover .tm__contact,
.tm__card:focus-within .tm__contact {
clip-path: inset(0 0 0 0);
}
.tm__ic {
display: inline-grid;
place-items: center;
flex: 0 0 auto;
color: #2f6df6;
font-size: 12px;
}
.tm__link {
color: #17141a;
font-weight: 700;
font-size: clamp(8px, 2.4vw, 10px);
line-height: 1.3;
text-decoration: underline;
text-underline-offset: 2px;
}
.tm__link:focus-visible {
outline: 2px solid #17141a;
outline-offset: 2px;
border-radius: 4px;
}
.tm.is-demo .tm__contact {
animation-name: tmOpen;
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.tm.is-demo .tm__card:nth-child(2) .tm__contact {
animation-delay: 0.6s;
}
.tm.is-demo .tm__card:nth-child(3) .tm__contact {
animation-delay: 1.2s;
}
@keyframes tmOpen {
0% {
clip-path: inset(0 0 100% 0);
}
8% {
clip-path: inset(0 0 0 0);
}
22% {
clip-path: inset(0 0 0 0);
}
30%, 100% {
clip-path: inset(0 0 100% 0);
}
}
@media (max-width: 340px) {
.tm {
gap: 4px;
}
.tm__card {
padding: 4px;
}
.tm__ic {
font-size: 10px;
}
}
@media (prefers-reduced-motion: reduce) {
.tm.is-demo .tm__contact {
animation: none !important;
clip-path: inset(0 0 0 0) !important;
transition: none !important;
}
}// 사람이 만지면 미리보기 루프를 뗀다 — 그 뒤로는 hover 와 :focus-within 이 Contact 줄을 연다
var teamList = document.getElementById('tm');
['pointerdown', 'focusin', 'pointerover'].forEach(function (evt) {
teamList.addEventListener(evt, function () { teamList.classList.remove('is-demo'); });
});// TeamMemberCard — vanilla 판(run/081/demo/02-content-card-team-member/index.html) 의 React 포트.
// 숨은 연락 줄을 clip-path 로 걷어낸다. 높이는 늘 차지하므로 옆 카드가 밀리지 않는다.
// 여는 조건은 :hover 와 :focus-within 둘 다 — 키보드로 링크에 닿아도 같은 상태가 보인다.
import { useState } from "react";
import type { CSSProperties } from "react";
import styles from "./TeamMemberCard.module.scss";
export type TeamMemberCardProps = {
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
type Member = { name: string; role: string; mail: string; href: string };
// 링크 글자는 조각으로 잇지 않는다 — 언어팩 사전의 키가 문장 전체여야 en·ja 판이 반쪽으로 안 나간다
const MEMBERS: Member[] = [
{ name: "김서연", role: "프로덕트 디자이너", mail: "김서연에게 메일", href: "#seoyeon" },
{ name: "박도윤", role: "프런트엔드 개발자", mail: "박도윤에게 메일", href: "#doyun" },
{ name: "이하준", role: "데이터 분석가", mail: "이하준에게 메일", href: "#hajun" },
];
function UserMark() {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256">
<g fill="currentColor">
<path d="M230.14 219.93a8 8 0 0 1-6.14 2.07H32a8 8 0 0 1-6.14-2.07a8 8 0 0 1-1.63-8.28C33.79 187.16 55 168.51 80.6 159.86a4 4 0 0 1 3.38.31a75.24 75.24 0 0 0 88 0a4 4 0 0 1 3.43-.31c25.62 8.65 46.81 27.3 56.37 51.79a8 8 0 0 1-1.64 8.28M168 88a40 40 0 1 1-40-40a40 40 0 0 1 40 40" opacity=".2" />
<path d="M230.92 212c-15.23-26.33-38.7-45.21-66.09-54.16a72 72 0 1 0-73.66 0c-27.39 8.94-50.86 27.82-66.09 54.16a8 8 0 1 0 13.85 8c18.84-32.56 52.14-52 89.07-52s70.23 19.44 89.07 52a8 8 0 1 0 13.85-8M72 96a56 56 0 1 1 56 56a56.06 56.06 0 0 1-56-56" />
</g>
</svg>
);
}
function EnvelopeMark() {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256">
<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>
);
}
export default function TeamMemberCard(props: TeamMemberCardProps) {
const duration = props.duration ?? 600;
const easing = props.easing ?? "cubic-bezier(.2, .8, .2, 1)";
const color = props.color ?? "#2f6df6";
const [preview, setPreview] = useState<boolean>(true);
const vars: Vars = { "--duration": duration + "ms", "--easing": easing, "--color": color };
const stop = () => setPreview(false);
return (
<main className={styles.stage}>
<ul
className={styles.tm + (preview ? " " + styles.isDemo : "")}
style={vars}
onPointerDown={stop}
onPointerOver={stop}
onFocus={stop}
>
{MEMBERS.map((member) => (
<li className={styles.card} key={member.name}>
<span className={styles.face} aria-hidden="true"><UserMark /></span>
<h3 className={styles.name}>{member.name}</h3>
<p className={styles.role}>{member.role}</p>
<p className={styles.contact}>
<span className={styles.ic} aria-hidden="true"><EnvelopeMark /></span>
<a className={styles.link} href={member.href}>{member.mail}</a>
</p>
</li>
))}
</ul>
</main>
);
}02 Team member card — Measured area 1.531% · moving frames 13/23Original by GODRICH · SCSS and React ship in the zip · CSS Card Design Examples — 9 Content Card Types
The post this demo lives in
CSS Card Design Examples — 9 Content Card TypesCSS card design is the craft of building containers that lay out content. The rule across all nine: put the click on the See all demos in this post


