GODRICH

CSS 로딩 스피너 9종 — div 하나로 끝나는

CSS 로딩 스피너는 서버 응답을 기다리는 동안 화면이 멈추지 않았다는 걸 보여주는 작은 반복 애니메이션으로, 이 글은 자바스크립트 없이 CSS만으로 돌아가는 아홉 가지 방식을 모았습니다. 아홉 개 중 세 개는 흔한 갤러리에서 잘 안 보이는 조합입니다.

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

아홉 개는 인기 순위가 아니라 로딩 스피너가 화면에서 맞닥뜨리는 순서로 놓았습니다. 아무 문맥 없이 도는 기본형(01~03), 다음으로 진행 상황을 실제로 보여주는 것(04~07), 마지막으로 특정 자리에 박아 쓰는 것(08~09) 순입니다. 항목마다 필요한 조각만 실었고, 완결본은 zip 안에 통째로 들어 있습니다.

01기본 링 스피너

원 하나의 테두리 중 위쪽만 진하게 칠하고 계속 돌리면 가장 기본적인 스피너가 됩니다. 페이지 전체 로딩이나 버튼을 누른 뒤의 대기 화면처럼 오래 봐도 부담 없어야 하는 자리에 씁니다.

1초 회전border 4px단색
.spinner {
  width: 56px;
  height: 56px;
  border-radius: 50%;
  border: 6px solid rgba($cream, .22);
  border-top-color: $color;
  animation: spin $duration linear infinite;
}
@keyframes spin {
  to { transform: rotate(360deg); }
}

02이중 역회전 링

안쪽 링과 바깥 링이 서로 반대 방향으로 돌면서 입체감을 만듭니다. 모달 로딩이나 데이터를 불러오는 대시보드 카드처럼 스피너 하나로는 밋밋한 자리에 씁니다.

역방향1.2초 · 0.9초두 겹
.ring--outer {
  border-top-color: $color;
  border-bottom-color: $color;
  animation: spin $duration-outer linear infinite;
}
.ring--inner {
  inset: 13px;
  border-left-color: $accent;
  border-right-color: $accent;
  animation: spin-rev $duration-inner linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
@keyframes spin-rev { to { transform: rotate(-360deg); } }

033점 바운스

점 세 개가 시차를 두고 튀어 오르며 뭔가 입력되거나 계산되는 중임을 보여줍니다. 채팅창 입력 대기나 검색 자동완성처럼 짧게 걸리는 대기에 씁니다.

지연 0.16초점 3개translateY
.dots span {
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: $color;
  animation: bounce $duration ease-in-out infinite;
}
.dots span:nth-child(2) { animation-delay: $delay; }
.dots span:nth-child(3) { animation-delay: $delay * 2; }
@keyframes bounce {
  0%, 80%, 100% { transform: translateY(0); opacity: .5; }
  40%           { transform: translateY(-14px); opacity: 1; }
}

04무한 바

목표 퍼센트를 모를 때는 짧은 막대가 트랙 안을 오가는 것만으로 충분합니다. width 대신 transform으로만 움직이면 레이아웃을 다시 계산하지 않아 더 매끄럽습니다.

가로 100%1.5초ease-in-out
.bar__fill {
  position: absolute;
  inset: 0;
  background: $color;
  transform-origin: left center;
  animation: sweep $duration ease-in-out infinite;
}
@keyframes sweep {
  0%   { transform: translateX(-100%) scaleX(.3); }
  50%  { transform: translateX(20%) scaleX(.6); }
  100% { transform: translateX(100%) scaleX(.3); }
}

05conic-gradient 스윕

conic-gradient 하나가 배경 이미지 없이 돌아가면 부드럽게 옅어지는 링을 만들 수 있습니다. 브랜드 컬러를 그대로 스피너에 쓰고 싶을 때, 특히 결제 대기 화면에 어울립니다.

conic-gradientmask 처리그라디언트
.spinner {
  border-radius: 50%;
  background: conic-gradient($color 0deg 270deg, rgba($cream, .18) 270deg 360deg);
  mask: radial-gradient(farthest-side, transparent calc(100% - 8px), #000 calc(100% - 8px));
  animation: spin $duration linear infinite;
}
@keyframes spin {
  to { transform: rotate(360deg); }
}

06stroke-dashoffset 원형

SVG 원 하나의 stroke-dasharray와 dashoffset만 바꾸면 궤적이 늘었다 줄었다 하는 느낌을 낼 수 있습니다. SVG 전체를 같이 돌리면 방향까지 살아 있는 스피너가 됩니다.

stroke-dasharraySVG1.4초
.arc {
  stroke: $color;
  stroke-linecap: round;
  stroke-dasharray: 113;
  stroke-dashoffset: 90;
  animation: dash $duration-dash ease-in-out infinite;
}
.spinner { animation: spin $duration-spin linear infinite; }
@keyframes dash {
  0%   { stroke-dashoffset: 100; }
  50%  { stroke-dashoffset: 20; }
  100% { stroke-dashoffset: 100; }
}
@keyframes spin { to { transform: rotate(360deg); } }

07@property 퍼센트 링

@property로 등록한 숫자 커스텀 속성은 CSS만으로 0에서 100까지 애니메이션됩니다. 실제 업로드 진행률처럼 값이 있는 상황에서 링을 자동으로 채우는 데 씁니다.

@property0~100%conic-gradient
@property --percent {
  syntax: "<number>";
  inherits: false;
  initial-value: 0;
}
.ring {
  background: conic-gradient($color calc(var(--percent) * 1%), rgba($ink, .18) 0);
  animation: fill $duration ease-in-out infinite;
}
@keyframes fill {
  0%   { --percent: 0; }
  50%  { --percent: 100; }
  100% { --percent: 0; }
}

08버튼 안 로딩 상태

버튼을 누르면 글자가 사라지고 그 자리에 작은 스피너가 나타나 처리 중임을 보여줍니다. 저장 버튼이나 결제 버튼처럼 중복 클릭을 막아야 하는 자리에 필수입니다.

버튼 폭 고정0.9초disabled 처리
.btn__label, .btn__spin {
  grid-area: 1 / 1;
  transition: opacity $duration, transform $duration;
}
.btn__spin {
  opacity: 0;
  transform: scale(.6);
  animation: spin $spin linear infinite;
}
.btn.is-loading .btn__label { opacity: 0; transform: translateY(6px); }
.btn.is-loading .btn__spin  { opacity: 1; transform: scale(1); }
@keyframes spin { to { transform: rotate(360deg); } }

09AI 앱형 펄스 오브

부드러운 그라디언트 구체 뒤에 더 크고 흐린 후광을 겹쳐 두고 둘을 다른 타이밍으로 오르내리게 하면 숨 쉬는 느낌이 납니다. AI 챗봇이 응답을 준비 중이거나 음성 비서가 듣는 중임을 알릴 때 씁니다.

blur 그라디언트2초 펄스box-shadow 글로우
.orb::before {
  background: radial-gradient(circle, rgba($color, .55), transparent 70%);
  filter: blur(6px);
  animation: glow $duration ease-in-out infinite;
}
.orb::after {
  background: radial-gradient(circle at 35% 35%, $cream, $color 75%);
  animation: core $duration ease-in-out infinite;
}
@keyframes core { 0%, 100% { transform: scale(.88); opacity: .88; } 50% { transform: scale(1.08); opacity: 1; } }
@keyframes glow { 0%, 100% { transform: scale(.8); opacity: .35; } 50% { transform: scale(1.25); opacity: .65; } }

어디서 깨지는가 — 함정 하나

아홉 개 중 05 conic-gradient 스윕만 유독 손이 많이 갔습니다. 그라디언트 자체는 고정해 두고 요소를 통째로 돌리는 방식이라 링 모양을 내려면 mask로 가운데를 뚫어야 하는데, MDN mask 문서를 보면 이 속성은 사파리에서 오래도록 -webkit-mask 접두어 없이는 조용히 무시됐습니다. 접두어를 빼고 렌더링했더니 링이 아니라 꽉 찬 원반이 나왔고, 두 줄을 나란히 넣고 나서야 사파리에서도 같은 링이 보였습니다. 실제로 지원 여부가 바뀌었는지는 프로젝트에 쓰기 전에 caniuse.com에서 다시 확인하는 편이 안전합니다. 이렇게 접두어까지 확인을 마친 아홉 개 파일의 압축 비밀번호는 이 문단 중간의 qda5pgmf 이며, 계속 읽지 않아도 아래에서 zip으로 바로 받을 수 있습니다.

접근성

기기의 동작 줄이기 설정을 켜 둔 사람에게는 아홉 개 스피너 전부에 prefers-reduced-motion: reduce 블록이 따로 적용됩니다. 회전이나 펄스 같은 반복 동작만 멈추고, 로딩 중이라는 사실 자체는 role="status"aria-label로 스크린 리더에 계속 전달됩니다. 04 무한 바와 07 퍼센트 링은 애니메이션이 멈춰도 진행 중인 상태가 화면에 절반쯤 채워진 모습으로 남도록 고정값을 정해 뒀습니다. prefers-reduced-motion의 문법과 지원 범위는 MDN 문서에서 확인할 수 있습니다.

@media (prefers-reduced-motion: reduce) {
  .spinner, .ring--outer, .ring--inner, .dots span, .bar__fill, .arc, .orb::before, .orb::after {
    animation: none !important;
  }
}

다른 로딩·스켈레톤 애니메이션은 CSS 애니메이션 카테고리에서 더 볼 수 있고, 이 사이트를 운영하는 이유는 소개 페이지에 적혀 있습니다.

FAQ

CSS 로딩 스피너에 꼭 SVG가 필요합니까?

아니요. 01 기본 링부터 05 conic-gradient 스윕까지는 순수 CSS 도형만으로 만들었습니다. SVG는 06 stroke-dashoffset 원형처럼 궤적 자체를 그려야 할 때만 필요합니다.

스피너 색은 브랜드 컬러로 바꿔도 됩니까?

네, 아홉 개 전부 $color 변수 하나만 바꾸면 됩니다. 다만 배경과 대비가 약하면 04 무한 바처럼 얇은 형태는 잘 안 보이니 대비를 먼저 확인하십시오.

@property는 어느 브라우저에서 지원됩니까?

이 글을 쓰는 시점 기준 대부분의 최신 브라우저가 지원하지만, 실제 프로젝트에 쓰기 전에 caniuse.com에서 최신 지원표를 확인하고 폴백이 필요한지 판단하십시오.

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

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