GODRICH
#css애니메이션#버튼#로딩#전환효과#스크롤#대시보드#실전ui#디자인트렌드

CSS 카드 디자인 9종 — 도시·팀원·바로가기부터

CSS 카드 디자인은 화면에 내용을 담아 나열하는 가장 기본적인 그릇을 짜는 일입니다. 클릭은 카드 전체가 아니라 링크에 맡기는 게 이 편의 원칙이고, 도시 소개·팀원·바로가기·자기소개 탭·후기·제품 스펙·이벤트·인트로·블로그 목록까지 아홉 가지 콘텐츠 카드를 라이브러리 없이

자동 재생 · 눌러서 해당 항목으로 · 전부 한 zip

순서는 인기순이 아니라 내용이 카드가 되어 가는 순서입니다. 한 장이 여러 장이 되고(01, 02), 그 카드가 안내판 구실을 하다가(03), 한 장 안에서 본문이 갈라지고(04, 05, 06), 카드에 분위기가 얹히고(07), 카드들이 한 덩어리로 짜이고(08), 마지막에는 카드가 많아져도 읽는 차례까지 지키게 됩니다(09). 이 편 전체의 축은 효과가 아니라 배치와 링크입니다 — 카드 전체가 아니라 링크에 클릭을 맡기는 원칙은 아홉 개 전부에 겹쳐 있습니다. 같은 맥락의 이웃으로 CSS 버튼 디자인과 탭 메뉴 만들기가 있습니다.

01도시 소개 그리드 카드

도시 이름과 한 줄 소개를 담은 카드 셋이 grid-template-columns 로 나란히 서고, 카드마다 라벨 띠가 위로 올라오며 자기 순서에 등장하는, 여행·지역 서비스 소개 섹션용 카드입니다.

도시 소개 그리드 카드
새 창에서 열기

01 · 도시 소개 그리드 카드 — 측정 누적면적 34.122% · 움직인 프레임 20/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

grid-template-columnsanimation-delayanimation-fill-mode

세 칸은 그리드가 정하고 등장은 순서가 정한다

.cg {
  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;
}

카드 하나하나에 폭을 주지 않고 칸 세 개를 먼저 못 박았기 때문에, 카드를 넷으로 늘릴 때는 repeat(3, 1fr) 만 repeat(4, 1fr) 로 바꾸면 카드 안은 손대지 않아도 됩니다.

지연이 걸린 등장에는 fill-mode 를 함께

// 지연 구간에도 시작 상태가 보이도록 — 없으면 첫 프레임에 완성 상태가 한 번 번쩍인다
.cg.is-demo .cg__card { animation-fill-mode: backwards; }
.cg.is-demo .cg__card:nth-child(2) { animation-delay: .12s; }
.cg.is-demo .cg__card:nth-child(3) { animation-delay: .24s; }

animation-delay 로 시차를 준 카드는 지연 구간에도 "올라오기 전 상태"를 보여야 자연스러운데, backwards 가 없으면 지연 동안 완성 상태가 그대로 보였다가 사라졌다가 다시 올라옵니다.

02팀원 소개 카드

이름과 직함 아래 숨어 있던 연락 줄이 카드에 초점이 가면 clip-path 로 펼쳐지고, 링크에는 사람 이름이 그대로 들어 있는, 회사 소개 팀 섹션용 카드입니다.

팀원 소개 카드
새 창에서 열기

02 · 팀원 소개 카드 — 측정 누적면적 1.531% · 움직인 프레임 13/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

clip-pathfocus-withintransition

자리는 잡아 두고 보이는 범위만 바꾼다

.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); }

연락 줄은 처음부터 높이를 차지하고 clip-path 로 아래쪽만 가려 두는 것이라, 높이가 변하지 않으니 펼쳐질 때 옆 카드가 밀리지 않고, 다시 접힐 때 레이아웃이 새로 계산되지도 않습니다.

여는 조건은 호버와 초점 둘 다

:focus-within 을 함께 걸었으므로 마우스 없이 키보드만 쓰는 사용자가 링크에 초점을 옮겨도 같은 상태가 펼쳐지는데, 호버용 인터랙션의 초점 버전을 늘 짝으로 두는 게 이 편의 관례입니다.

03바로가기 타일 카드

정사각 타일 네 개가 aspect-ratio 로 크기를 맞추고, 화살표가 타일 모서리를 향해 밀려 나가며 어디로 가는 길인지 가리키는, 홈 화면 빠른 메뉴용 카드입니다.

바로가기 타일 카드
새 창에서 열기

03 · 바로가기 타일 카드 — 측정 누적면적 1.637% · 움직인 프레임 19/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

aspect-ratiotranslategrid

정사각은 높이가 아니라 비율로

.st__tile {
  display: grid;
  grid-template-columns: 1fr auto;
  grid-template-rows: auto 1fr auto;
  aspect-ratio: 1 / 1;
  box-sizing: border-box;
  padding: $sp-2;
  border-radius: $r-card;
  background: $subject-cream;
}

높이를 px로 고정하지 않고 aspect-ratio: 1 / 1 로 못 박았기 때문에, 4열이 좁은 화면에서 2열로 바뀌어도 타일은 늘 정사각이고 라벨이 길어질 때 늘어나는 쪽은 줄바꿈뿐입니다.

화살표는 translate 속성으로 민다

.st__go {
  translate: 0 0;
  transition: translate $duration $easing;
}
.st__tile:hover .st__go,
.st__tile:focus-within .st__go { translate: $reach (-$reach); }

transform 단축 속성이 아니라 독립 속성 translate 를 썼기 때문에, 나중에 다른 효과가 scale 이나 rotate 를 얹어도 서로 덮어쓰지 않고 transition 도 그 속성 하나만 정확히 지켜봅니다.

04탭으로 넘기는 자기소개 카드

소개·경력·연락 세 탭을 누르면 본문이 한 프레임에 바뀌고 밑줄 표시가 라디오 입력만으로 그 탭 자리로 미끄러지는, 개인 소개 페이지용 카드입니다.

탭으로 넘기는 자기소개 카드
새 창에서 열기

04 · 탭으로 넘기는 자기소개 카드 — 측정 누적면적 5.933% · 움직인 프레임 8/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

steps(1, end)hastranslate

상태는 라디오가 들고 카드가 :has() 로 읽는다

.ab__ink {
  position: absolute; left: 0; bottom: -1px;
  width: $tab-w; height: 3px; border-radius: $r-pill;
  background: $color;
  translate: 0 0;
  transition: translate $duration $easing;
}
.ab:has(#abT2:checked) .ab__ink { translate: $step 0; }
.ab:has(#abT3:checked) .ab__ink { translate: $step * 2 0; }

라디오가 상태를 저장하고 카드가 :has() 로 그 상태를 읽어 밑줄을 옮기고 본문을 바꾸는, JS 없이 두 요소가 같은 값을 따르게 하는 장치입니다.

밑줄 폭은 실측한 칸 폭과 같은 값

밑줄이 고정 px로 움직이므로 탭 칸도 고정 폭(flex: 0 0 auto, 72px)인데, 칸 폭은 짐작하지 않고 getBoundingClientRect 로 실측한 값이라 라벨이 언어마다 길어져도 자리가 밀리지 않습니다.

05핀 보드 후기 카드

길이가 제각각인 후기 카드가 column-count 로 흘러 붙고, 카드가 한 장씩 자리를 잡으며 빈틈 없는 벽을 만드는, 후기 모음 페이지용 카드입니다.

핀 보드 후기 카드
새 창에서 열기

05 · 핀 보드 후기 카드 — 측정 누적면적 28.106% · 움직인 프레임 12/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

column-countbreak-insideanimation-delay

다단에서 카드가 잘리지 않는 한 줄

.pin {
  box-sizing: border-box; width: 100%;
  column-count: 3;
  column-gap: $sp-3;
}
.pin__card {
  // 카드가 칸 경계에서 잘리지 않게 — 다단 조판의 핵심 한 줄
  break-inside: avoid;
  box-sizing: border-box;
  margin-bottom: $sp-3;
  padding: $sp-2;
  border-left: 3px solid $color;
  background: $card-bg;
}

다단 조판은 글줄을 칸에서 칸으로 넘기기 때문에 아무것도 하지 않으면 후기 카드 한 장이 두 칸에 걸쳐 아래쪽에서 잘리는데, break-inside: avoid 한 줄로 "이 상자는 쪼개지 말라"고 못 박으면 카드가 통째로 다음 칸으로 넘어갑니다.

칸 안 기준의 빈틈없음

빈틈 없는 벽은 칸 안에서 카드 사이에 틈이 없다는 뜻이고 칸 아래쪽은 마지막 카드의 길이에 따라 자연히 남습니다 — 다단 조판에서 칸 아래 끝까지 억지로 채우면 카드가 늘어나거나 잘리므로, 칸을 끝까지 채우고 싶다면 09 의 그리드 배치가 맞는 도구입니다.

06스펙이 자라는 제품 카드

제품 카드 한 장이 grid-template-rows 0fr 에서 1fr 로 열리며 숨어 있던 스펙 표가 밀려 내려오고, 옆 카드는 밀리지 않는 제품 비교 목록용 카드입니다.

스펙이 자라는 제품 카드
새 창에서 열기

06 · 스펙이 자라는 제품 카드 — 측정 누적면적 5.152% · 움직인 프레임 9/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

grid-template-rowsminmaxoverflow

max-height 대신 칸을 자라게 한다

.sg__grow {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows $duration $easing;
}
// 0fr 칸에서 실제로 접히게 하는 두 줄 — 이게 없으면 표가 그대로 보인다
.sg__inner { overflow: hidden; min-height: 0; }
.sg__card.is-open .sg__grow { grid-template-rows: 1fr; }

max-height 로 여닫으면 늘 "적당히 큰 값"을 짐작해야 하고 내용이 길어지면 잘리지만, 0fr → 1fr 전환은 칸의 실제 내용 높이까지만 자라므로 값을 짐작할 일이 없습니다.

접히는 두 줄과 옆 카드가 안 밀리는 까닭

0fr 칸의 자식은 자기 크기를 그대로 들고 있으므로 overflow: hidden + min-height: 0 두 줄이 없으면 접혀도 표가 삐져나오고, 바깥 격자가 align-items: start 가 아니라 기본값 stretch 라면 한 카드가 자라는 순간 같은 줄의 나머지 카드 높이가 같이 늘어납니다.

07분위기를 먼저 보이는 이벤트 카드

날짜 숫자가 카드 한쪽을 크게 차지하고 제목 줄이 아래에서 올라오며, 카드 배경의 색 띠가 천천히 기울어 행사 분위기를 내는, 공연·전시 일정 목록용 카드입니다.

분위기를 먼저 보이는 이벤트 카드
새 창에서 열기

07 · 분위기를 먼저 보이는 이벤트 카드 — 측정 누적면적 7.618% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

rotatemix-blend-modeanimation-delay

띠는 이미지가 아니라 곱해진 색이다

.em__card {
  position: relative;
  // 카드가 자기 합성 그룹이 된다 — 이게 없으면 띠가 카드 면이 아니라 무대(주황)와 곱해진다
  isolation: isolate;
  overflow: hidden;
}
.em__mood {
  mix-blend-mode: multiply;
  rotate: -#{$tilt};
}

색 띠는 mix-blend-mode: multiply 로 카드 면과 곱해진 색이라 카드 배경색을 바꾸면 띠 색도 따라 바뀌는데, 카드에 isolation: isolate 가 없으면 곱셈이 카드를 뚫고 무대(주황)까지 가서 띠가 무대색과 섞여 버립니다.

띠가 지나가도 글자 대비는 지킨다

띠가 지나가면 면색이 크림에서 곱해진 색으로 바뀌는데 가장 어두워지는 조합(크림 × 주황 = #ff4b1c)에서도 잉크 글자가 5.50:1 이라 WCAG AA 를 지키고, 글자는 전부 잉크 한 색으로 두어 흐린 글자(알파) 대신 굵기와 크기로만 층을 나눴습니다.

08인트로 그리드 섹션 카드

큰 카드 하나와 작은 카드 넷이 grid-area 로 짜인 자리에 놓이고, 큰 카드의 제목이 먼저 뜬 뒤 작은 카드가 시차를 두고 따라 올라오는, 서비스 첫 화면 요약용 카드입니다.

인트로 그리드 섹션 카드
새 창에서 열기

08 · 인트로 그리드 섹션 카드 — 측정 누적면적 18.81% · 움직인 프레임 18/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

grid-areagrid-template-areasanimation-delay

도면을 먼저 그리고 카드에 이름표를 단다

.ig {
  display: grid;
  grid-template-columns: 1.25fr 1fr 1fr;
  grid-template-areas:
    "hero a b"
    "hero c d";
  grid-template-rows: repeat(2, 1fr);
  gap: $sp-2;
}
// 칸 이름표 — 카드가 HTML 어디에 있든 자기 칸으로 간다
.ig__hero { grid-area: hero; }
.ig__cell--a { grid-area: a; }

grid-template-areas 가 도면이고 grid-area 가 "이 카드는 저 칸"이라는 이름표라, 도면이 글자로 보이니 좁은 화면용 도면을 한 줄씩 다시 그리는 것만으로 배치가 통째로 바뀌고 카드의 순서(HTML)나 클래스는 손대지 않아도 됩니다.

큰 카드가 먼저, 작은 카드가 시차로

.ig.is-demo .ig__title { animation-name: igTitle; animation-fill-mode: backwards; }
.ig.is-demo .ig__cell--a { animation-delay: .14s; }
.ig.is-demo .ig__cell--b { animation-delay: .24s; }
.ig.is-demo .ig__cell--c { animation-delay: .34s; }
.ig.is-demo .ig__cell--d { animation-delay: .44s; }

시선이 가야 할 곳(큰 카드의 제목)이 먼저 뜨고 딸리는 정보가 시차를 두고 따라오는데, 시차를 준 요소 전부에 animation-fill-mode: backwards 가 함께 있어야 지연 구간에 완성 상태가 번쩍이지 않습니다.

09읽는 순서를 지키는 워터폴

높이가 다른 카드를 grid-auto-flow dense 로 채워 빈칸을 없애면서도 번호 순서를 지켜, 화면에서 읽는 차례와 코드 차례가 어긋나지 않게 하는 블로그 목록용 카드입니다.

읽는 순서를 지키는 워터폴
새 창에서 열기

09 · 읽는 순서를 지키는 워터폴 — 측정 누적면적 39.506% · 움직인 프레임 11/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

grid-auto-flowgrid-roworder

세 속성이 각자 다른 일을 한다

.wf {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  grid-auto-rows: 46px;
  // 남는 칸을 되짚어 채운다. 아래 grid-row 명시 배치와 짝이어야 순서가 안 어긋난다
  grid-auto-flow: row dense;
  gap: $sp-2;
}
.wf__card--s2 { grid-row: 1 / span 2; grid-column: 2; }
.wf__card--tall { grid-row: span 2; }
.wf__card--pin { order: -1; }

dense 는 자동 배치가 앞에 남은 칸을 되짚어 채우므로 빈칸이 안 남지만 그 칸을 채우는 건 언제나 뒤 카드라 화면에서 읽는 차례가 번호와 어긋나는데, 키 큰 카드의 줄을 grid-row 로 명시 배치하면 자동으로 흐르는 카드들이 남은 칸에 번호 순서 그대로 떨어집니다 — 애초에 되짚을 빈칸이 생기지 않는 것입니다.

고정 글은 order 로만 앞으로

고정한 글(1번)은 HTML 맨 끝에 붙는데 order: -1 하나로 배치 순서만 맨 앞으로 보내면 되므로, 마크업은 그대로 두고 화면 차례만 바꿀 수 있습니다.

카드 디자인 UI 는 무엇부터 정해야 하나?

카드를 만들 때 무엇부터 정할지에는 순서가 있습니다. 먼저 칸의 수와 폭(grid-template-columns)을 정하고, 다음 카드의 면(배경·모서리·테두리), 그 다음 링크의 주인(카드 전체인가, 제목 링크인가)을 정합니다. 순서가 바뀌면 앞의 결정이 뒤에서 무너집니다 — 링크를 먼저 카드 전체로 정하면 나중에 카드 안에 두 번째 링크를 넣는 순간 클릭 영역이 겹칩니다.

이 아홉 개는 모두 칸 먼저였습니다. 01 은 repeat(3, 1fr), 03 은 정사각 aspect-ratio, 08 은 grid-template-areas 도면, 09 는 dense 배치 — 배치가 먼저 서니 카드 안의 효과(라벨 띠·연락 줄·색 띠)는 자리가 흔들리지 않는 범위 안에서만 움직일 수 있었습니다. css 카드 효과나 css 카드 애니메이션을 찾아 온 분에게도 답은 같습니다 — 효과는 배치 위에 얹히는 것이지 배치를 대신하지 못합니다.

링크의 주인에 대한 원칙도 이 편 전체에 겹쳐 있습니다. 카드 전체가 아니라 링크에 클릭을 맡기면 링크 글자만 읽어도 어디로 가는지 알 수 있습니다(02 의 "김서연에게 메일", 03 의 "주문 내역", 08 의 "서비스 둘러보기"). 스크린 리더는 카드 덩어리를 한 줄로 읽지 않고 링크를 하나씩 거치므로, 링크가 자기 목적지를 말하는지가 접근성의 절반입니다.

어디서 깨지는가 — dense 가 순서를 바꾸고 0fr 이 내용을 삐져낸다

dense 만 켜면 읽는 차례가 깨진다

09 를 만들면서 실측한 함정입니다. grid-auto-flow: row dense 만 넣고 키 큰 카드의 줄을 못 박지 않으면, 2번 카드가 두 줄을 차지하느라 다음 줄로 밀리고 그 빈칸을 뒤의 3번이 되짚어 채웁니다. 눈은 번호를 따라가는데 화면의 배열은 1, 3, 4, 2 순서가 되어 있었습니다. 명시 배치(grid-row: 1 / span 2)를 짝으로 넣어야 되짚을 빈칸 자체가 없어집니다. 자동 배치 규칙의 원문은 MDN grid-auto-flow 문서에 있습니다.

0fr 칸은 혼자 접히지 않는다

06 의 표가 처음에는 접혀 있어야 하는데 그대로 보였습니다. grid-template-rows: 0fr 은 칸의 높이를 0 으로 만들 뿐 자식의 크기를 바꾸지 않습니다. 자식에 overflow: hidden 과 min-height: 0 을 함께 주고 나서야 표가 칸 안으로 들어갔습니다. 이 두 줄이 없는 0fr 은 접히는 게 아니라 내용이 삐져나온 채로 쌓여 있는 상태입니다.

카드가 가운데 정렬이면 열 때마다 통째로 뛴다

06 에서 한 번 더 겪은 일입니다. 무대가 place-items: center 라 보드가 자라는 순간 세 카드가 통째로 위로 올라갔습니다 — "옆 카드가 안 밀린다"는 이 카드의 주장이 화면에서 거짓이 되는 순간이었습니다. 보드를 무대 높이만큼 늘려 두고(align-self: stretch) 줄을 위에서부터 쌓아서(align-content: start) 자라는 칸이 가운데 하나뿐이게 만들었습니다. 실측에서 열림 전후 보드 위치 변화가 0px로 닫혔습니다.

아홉 개 데모와 글 원문 전체를 암호로 묶었습니다. 카드 아홉 종의 HTML·SCSS·React 판과 이 글 전체의 압축 비밀번호는 75rkpw3y 이고, 격자에서 본 그대로를 파일로 받아 붙여 쓸 수 있습니다. 위의 함정 세 가지는 전부 이 zip 안 소스에서 그대로 확인할 수 있습니다 — 09 의 grid-row 명시 배치 줄을 지우면 순서가 깨지고, 06 의 overflow: hidden 두 줄을 지우면 표가 삐져나오는 걸 직접 재현할 수 있습니다.

접근성 — 움직임을 뺐을 때 남는 것

아홉 개 전부 prefers-reduced-motion: reduce 에서는 애니메이션이 꺼지고 카드가 제자리에 그대로 있습니다. 01 의 라벨 띠는 이미 올라온 상태로 보이고, 02 의 연락 줄은 clip-path 트랜지션만 끊길 뿐 호버하면 그대로 펼쳐지며, 04 는 밑줄이 컷으로 옮겨가고 본문 교체는 원래 컷이라 차이가 없습니다. 05 의 시차 등장도 사라질 뿐 후기 전부가 처음부터 보입니다. 움직임이 정보를 설명하는 유일한 수단이 되지 않도록, 어느 카드든 정지 상태만으로 누가 어디로 가는지 다 읽히게 짰습니다. 초점 링은 아홉 개 전부 :focus-visible 로 따로 두었고, 색 띠가 지나가는 07 의 글자 대비(5.50:1)도 위에서 이미 계산해 두었습니다.

FAQ

CSS 카드 디자인 예제를 그대로 복사해도 되나요?

네. 위 코드는 전부 그대로 복사해 쓰도록 짜여 있습니다. 단 03 의 타일 라벨과 07 의 행사 이름처럼 화면 문구는 한국어로 적혀 있으니 본인 문구로 바꾸고, 칩 아이콘은 각자 쓰는 아이콘 세트로 교체하면 됩니다.

카드형 레이아웃에서 카드 높이가 제각각인데 어떻게 맞추나요?

높이를 맞추는 게 아니라 어긋나도 좋게 두는 방법을 먼저 보세요. 05 는 다단(column-count)으로 어긋난 높이를 그대로 쓰고, 09 는 dense 배치로 어긋난 카드가 빈칸을 메웁니다. 반듯하게 맞춰야 한다면 08 처럼 grid-template-rows 로 줄 높이를 못 박거나 01 처럼 1fr 칸으로 균등 분할하는 것이 답입니다.

grid-template-areas 는 오래된 브라우저에서도 되나요?

grid-template-areas 와 grid-area 는 2017년 이후 그리드를 지원하는 모든 브라우저에서 됩니다. 이 편에서 상대적으로 새로운 것은 04 의 :has()(2023년부터 주요 브라우저 전부)와 03 의 aspect-ratio(2021년)이니, 이 두 항목은 대상 브라우저를 확인하고 쓰세요.

압축 비밀번호를 입력하세요

비밀번호는 이 글 본문 안에 있습니다. 읽다 보면 나옵니다.