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

스켈레톤 UI CSS 9종 — 복붙하면 되는 로딩 화면

스켈레톤 UI CSS는 실제 글자·이미지가 도착하기 전에 그 자리를 회색 막대나 동그라미로 미리 그려 두는 로딩 방식입니다. 빈 화면이나 뱅글뱅글 도는 스피너보다 "곧 뭔가 나온다"는 확신을 먼저 줍니다.

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

아홉 개는 로딩페이지 디자인에서 화면 하나가 겪는 순서로 놓았습니다. 글·카드·목록·표처럼 정적인 자리부터 채우고(01~04), 사진처럼 무거운 자리(05), 상자 크기에 따라 모양이 바뀌는 경우(06), 데이터가 도착한 뒤의 전환(07), 긴 목록에서의 리듬(08), 배색까지 자동으로 맞추는 경우(09) 순입니다. 기다림이 짧아 자리를 미리 그릴 필요가 없을 때는 CSS 로딩 스피너 9종이 더 간단하고, 07처럼 스켈레톤이 물러난 뒤 화면 자체가 바뀌는 순간은 CSS 화면 전환 효과가 따로 다룹니다.

01글줄 반짝임

무엇이 보이나

글 한 줄이 들어갈 자리를 폭이 다른 회색 막대 네 개로 대신하고, 그 위로 빛이 왼쪽에서 오른쪽으로 훑고 지나갑니다.

글줄 반짝임
새 창에서 열기

01 · 글줄 반짝임 — 측정 누적면적 11.078% · 움직인 프레임 21/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

1.4s느리게 2s빛 없이

css 로딩 애니메이션은 어디에 쓰나

댓글·목록처럼 텍스트 줄 수가 대충 예상되는 곳에 씁니다.

.line {
  position: relative; overflow: hidden;
  height: 14px; border-radius: 12px; background: rgba($color, .14);
  &::after {
    content: ""; position: absolute; inset: 0;
    background: linear-gradient(90deg, transparent, rgba($color, .45), transparent);
    transform: translateX(-120%);
    animation: shimmer $duration $easing infinite;
  }
}
@keyframes shimmer { to { transform: translateX(120%); } }

02카드 스켈레톤

화면에서 일어나는 일

이미지·제목·설명 자리를 가진 카드 한 장이고, 세 자리 모두 같은 빛이 훑습니다.

카드 스켈레톤
새 창에서 열기

02 · 카드 스켈레톤 — 측정 누적면적 28.717% · 움직인 프레임 20/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

3장가로형둥글게

쓰는 자리

상품 목록이나 글 카드처럼 이미지와 텍스트가 함께 오는 곳에 씁니다.

%bar {
  position: relative; overflow: hidden;
  border-radius: $r-control; background: rgba($color, .16);
  &::after {
    content: ""; position: absolute; inset: 0;
    background: linear-gradient(90deg, transparent, rgba($stage-paper, .85), transparent);
    transform: translateX(-120%);
    animation: shimmer $duration $easing infinite;
  }
}
.thumb { @extend %bar; aspect-ratio: 2 / 1; }
.title { @extend %bar; height: 16px; width: 70%; }

03아바타 목록

어떤 움직임인가

동그란 아바타와 두 줄 글이 다섯 번 반복되고, 반짝임은 막대마다가 아니라 목록 전체 위로 한 줄기만 지나갑니다.

아바타 목록
새 창에서 열기

03 · 아바타 목록 — 측정 누적면적 44.317% · 움직인 프레임 17/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

5줄작게글 한 줄

언제 쓰나

채팅방이나 팔로워 목록처럼 줄 수가 많은 곳에 씁니다.

.avatar { width: 28px; height: 28px; border-radius: $r-pill; background: rgba($color, .22); }
.sheen {
  position: absolute; inset: 0;
  background: linear-gradient(90deg, transparent, rgba($stage-paper, .4), transparent);
  transform: translateX(-120%);
  animation: shimmer $duration $easing infinite;
  pointer-events: none;
}
@keyframes shimmer { to { transform: translateX(120%); } }

04표 스켈레톤

무엇이 보이나

머리글 글자는 그대로 두고 몸통 셀만 막대로 바꾸며, 헤더와 몸통이 같은 grid-template-columns를 써서 열 폭이 흔들리지 않습니다. 대시보드 표처럼 열이 의미를 가진 곳에 씁니다.

표 스켈레톤
새 창에서 열기

04 · 표 스켈레톤 — 측정 누적면적 45.848% · 움직인 프레임 17/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

6행열 폭 유지줄무늬

코드에서 볼 곳

.row { display: grid; grid-template-columns: 2fr 1fr 1fr 1fr; gap: 8px; padding: 8px 12px; }
.row.head { border-bottom: 1px solid rgba($color, .12); }
.cell { height: 11px; border-radius: $r-control; background: rgba($color, .13); }
.sheen {
  position: absolute; inset: 0;
  background: linear-gradient(90deg, transparent, rgba($subject-blue, .45), transparent);
  animation: shimmer $duration $easing infinite;
}

05이미지 블러업

화면에서 일어나는 일

아주 작은 원본을 20px 흐리게 먼저 깔고, 그 위에 선명한 판을 겹쳐 opacity로 나타났다 사라지길 반복합니다.

이미지 블러업
새 창에서 열기

05 · 이미지 블러업 — 측정 누적면적 49.817% · 움직인 프레임 22/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

20px400ms확대 섞기

쓰는 자리

나타나는 순간 scale(1.04→1)로 확대가 살짝 풀립니다. 히어로 이미지나 갤러리처럼 사진이 무거운 곳에 씁니다.

.placeholder { filter: blur(20px); transform: scale(1.15); }
.real {
  opacity: 0; transform: scale(1.04);
  animation: reveal $duration $easing infinite;
}
@keyframes reveal {
  0%   { opacity: 0; transform: scale(1.04); }
  50%  { opacity: 1; transform: scale(1); }
  100% { opacity: 0; transform: scale(1.04); }
}

06반응형 스켈레톤

어떤 움직임인가

카드 세 장을 담는 상자에 container-type: inline-size를 걸어 두면, 뷰포트가 아니라 이 상자 자신의 폭에 따라 3열·2열·1열이 바뀝니다. 사이드바에 넣든 본문에 넣든 파일 하나로 끝납니다.

반응형 스켈레톤
새 창에서 열기

06 · 반응형 스켈레톤 — 측정 누적면적 56.241% · 움직인 프레임 22/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

3열2열 @ 480px1열 @ 320px

코드에서 볼 곳

.wrap { container-type: inline-size; container-name: skeleton; }
.grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
@container skeleton (max-width: 260px) {
  .grid { grid-template-columns: repeat(2, 1fr); }
}
@container skeleton (max-width: 150px) {
  .grid { grid-template-columns: 1fr; }
}

07스켈레톤 → 내용 전환

무엇이 보이나

막대 세 개와 실제 문단을 같은 칸에 겹쳐 두고, 데이터가 도착하면 막대는 페이드로 사라지고 문단은 6px 위에서 페이드로 내려옵니다.

스켈레톤 → 내용 전환
새 창에서 열기

07 · 스켈레톤 → 내용 전환 — 측정 누적면적 8.035% · 움직인 프레임 7/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

300ms위로 살짝페이드만

어디에 쓰나

스켈레톤을 쓴 모든 화면의 마지막 장면입니다.

.skeleton, .content { grid-area: 1 / 1; }
.skeleton { animation: fade-out $duration $easing infinite; }
.content { animation: fade-in $duration $easing infinite; }
@keyframes fade-out { 0%, 40% { opacity: 1; } 55%, 85% { opacity: 0; } 100% { opacity: 1; } }
@keyframes fade-in {
  0%, 40%  { opacity: 0; transform: translateY(6px); }
  55%, 85% { opacity: 1; transform: translateY(0); }
  100%     { opacity: 0; transform: translateY(6px); }
}

08줄마다 시차

화면에서 일어나는 일

막대 여섯 개가 01과 같은 빛을 받지만, 줄마다 animation-delay를 60ms씩 늦춰 파도처럼 보이게 하고 짝수 줄은 반대 방향으로 훑습니다. 긴 목록을 스크롤할 때 리듬을 줍니다.

줄마다 시차
새 창에서 열기

08 · 줄마다 시차 — 측정 누적면적 13.818% · 움직인 프레임 17/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

60ms역방향2열

코드에서 볼 곳

@for $i from 1 through 6 {
  .bar:nth-child(#{$i})::after { animation-delay: -($i - 1) * 60ms; }
}
.bar:nth-child(even)::after { animation-name: shimmer-reverse; }
@keyframes shimmer { to { transform: translateX(120%); } }
@keyframes shimmer-reverse {
  from { transform: translateX(120%); }
  to   { transform: translateX(-120%); }
}

09다크모드 자동 색

어떤 움직임인가

카드와 막대 색을 light-dark() 함수 하나로 정의해 두면 사이트의 color-scheme이 바뀔 때 별도 클래스 없이 배경·막대 색이 저절로 맞춰집니다. 다크모드가 있는 모든 사이트에 씁니다.

다크모드 자동 색
새 창에서 열기

09 · 다크모드 자동 색 — 측정 누적면적 15.289% · 움직인 프레임 9/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

light-dark()대비 높게회색 대신 파랑

코드에서 볼 곳

.card {
  color-scheme: light dark;
  background: light-dark(#ffffff, #17141a);
}
.bar {
  background: light-dark(rgba(23, 20, 26, .16), rgba(255, 247, 230, .22));
}

어디서 깨지는가 — 함정 셋

레이아웃을 다시 재지 않는 이유

아홉 개 대부분이 위치가 아니라 transform과 opacity만 움직이는 이유는, 이 둘만 브라우저가 레이아웃을 다시 재지 않고 합성 단계에서 처리하기 때문입니다. MDN의 CSS 애니메이션 성능 문서가 이 둘을 권하는 이유이기도 합니다. 06 반응형 스켈레톤만 예외인데, 컨테이너 쿼리는 요소의 실제 박스 크기에만 반응하고 transform으로 만든 시각적 확대·축소에는 반응하지 않아서 진짜 width를 바꿔야 열 수가 바뀝니다. 이 사실을 모르고 카드 안쪽 요소를 scale()로만 줄였다가 컨테이너 쿼리가 한 번도 안 걸린 채로 배포한 적이 있습니다.

흐린 판과 선명한 판

같은 실수를 하나 더 겪었습니다. 05 이미지 블러업에서 흐린 판과 선명한 판에 똑같이 매끈한 그라데이션만 썼더니, 두 판의 색이 너무 비슷해서 크로스페이드가 거의 안 보였습니다. 결이 있는 무늬(가는 줄무늬)를 넣고 나서야 흐림이 지워버리는 디테일이 눈에 들어왔습니다. 04 표 스켈레톤에서는 훑는 빛 레이어에 position: relative를 몸통에 안 걸었더니 빛이 표 바깥 엉뚱한 자리에 그려졌는데, 이 아홉 개 파일은 이미 그 값이 맞게 들어간 채로 압축돼 있고 비밀번호는 a3rjf9rd이며 아래 버튼으로 받을 수 있습니다.

07의 전환이 한 카드가 아니라 화면 전체를 넘기는 규모가 되면 이 함정들은 그대로 커지는데, 그 규모의 전환은 CSS 페이지 전환 애니메이션에서 따로 다뤘습니다.

접근성

동작 줄이기를 켰을 때

시스템의 "동작 줄이기"가 켜졌을 때 아홉 개 전부 prefers-reduced-motion: reduce를 처리하지만, 끄는 방식은 항목마다 다릅니다. 01·02·03·04·08의 훑는 빛은 멈추고 옅은 밝기만 남으며, 05는 흐림 없이 선명한 최종 상태 하나로 고정되고, 06은 폭이 가장 넓은 3열 상태에서 멈춥니다. 07은 스켈레톤을 아예 숨기고 내용만 보여주며, 09는 어두운 배색 한 장으로 고정됩니다. 어느 경우도 정보 자체가 사라지지는 않고, 반복되는 움직임만 빠집니다. 이 미디어 쿼리 자체의 지원 범위는 MDN prefers-reduced-motion 문서에 있습니다.

@media (prefers-reduced-motion: reduce) {
  .line::after, .sheen { animation: none !important; opacity: .4; }
}

페이지 로딩 애니메이션과 카드 주제는 CSS 애니메이션 카테고리에 모여 있고, 이 사이트가 무엇을 다루는지는 소개에 있습니다.

FAQ

로딩 중 css 스켈레톤과 스피너 중 뭘 써야 하나요?

기다리는 시간이 1초를 넘고 화면 구조를 미리 보여줄 수 있으면 스켈레톤이 낫습니다. 사용자가 "무엇이 오는지" 미리 짐작할 수 있어 체감 대기 시간이 줄어듭니다. 반대로 구조가 매번 크게 달라지거나 대기 시간이 아주 짧으면 스피너 하나로 충분합니다.

아홉 개를 한 페이지에 다 써도 되나요?

한 페이지 안에서는 실제로 그 자리에 올 콘텐츠 모양과 맞는 것 한두 개만 쓰는 게 좋습니다. 04 표 스켈레톤과 02 카드 스켈레톤을 한 목록에 같이 쓰면 사용자가 어떤 화면이 오는지 헷갈립니다. 06 반응형 스켈레톤은 카드형 자리라면 여러 화면 폭에서 하나로 대신할 수 있습니다.

React에서는 로딩 상태를 어떻게 넘기나요?

zip의 react/ 폴더는 로딩 여부를 클래스 토글이 아니라 props로 받습니다. 07은 loading, 05는 이미지의 onLoad 콜백이 내부 상태를 바꿉니다. 06은 컨테이너 쿼리만으로 동작해서 별도 로딩 상태가 필요 없습니다. 같은 loading 값을 버튼 안쪽에도 물려 쓰고 싶다면, 눌린 버튼이 스스로 로딩으로 바뀌는 항목이 CSS 버튼 애니메이션 9종에 들어 있습니다.

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

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