CSS 로딩 스피너 9종 — div 하나로 끝나는
CSS 로딩 스피너는 서버 응답을 기다리는 동안 화면이 멈추지 않았다는 걸 보여주는 작은 반복 애니메이션으로, 이 글은 자바스크립트 없이 CSS만으로 돌아가는 아홉 가지 방식을 모았습니다. 아홉 개 중 세 개는 흔한 갤러리에서 잘 안 보이는 조합입니다.
자동 재생 · 눌러서 해당 항목으로 · 전부 한 zip
- 01 기본 링 스피너
- 02 이중 역회전 링
- 03 3점 바운스
- 04 무한 바
- 05 conic-gradient 스윕
- 06 stroke-dashoffset 원형
- 07 @property 퍼센트 링
- 08 버튼 안 로딩 상태
- 09 AI 앱형 펄스 오브
아홉 개는 인기 순위가 아니라 로딩 스피너가 화면에서 맞닥뜨리는 순서로 놓았습니다. 아무 문맥 없이 도는 기본형(01~03), 다음으로 진행 상황을 실제로 보여주는 것(04~07), 마지막으로 특정 자리에 박아 쓰는 것(08~09) 순입니다. 항목마다 필요한 조각만 실었고, 완결본은 zip 안에 통째로 들어 있습니다.
01기본 링 스피너
원 하나의 테두리 중 위쪽만 진하게 칠하고 계속 돌리면 가장 기본적인 스피너가 됩니다. 페이지 전체 로딩이나 버튼을 누른 뒤의 대기 화면처럼 오래 봐도 부담 없어야 하는 자리에 씁니다.
.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이중 역회전 링
안쪽 링과 바깥 링이 서로 반대 방향으로 돌면서 입체감을 만듭니다. 모달 로딩이나 데이터를 불러오는 대시보드 카드처럼 스피너 하나로는 밋밋한 자리에 씁니다.
.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점 바운스
점 세 개가 시차를 두고 튀어 오르며 뭔가 입력되거나 계산되는 중임을 보여줍니다. 채팅창 입력 대기나 검색 자동완성처럼 짧게 걸리는 대기에 씁니다.
.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으로만 움직이면 레이아웃을 다시 계산하지 않아 더 매끄럽습니다.
.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 하나가 배경 이미지 없이 돌아가면 부드럽게 옅어지는 링을 만들 수 있습니다. 브랜드 컬러를 그대로 스피너에 쓰고 싶을 때, 특히 결제 대기 화면에 어울립니다.
.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 전체를 같이 돌리면 방향까지 살아 있는 스피너가 됩니다.
.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까지 애니메이션됩니다. 실제 업로드 진행률처럼 값이 있는 상황에서 링을 자동으로 채우는 데 씁니다.
@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버튼 안 로딩 상태
버튼을 누르면 글자가 사라지고 그 자리에 작은 스피너가 나타나 처리 중임을 보여줍니다. 저장 버튼이나 결제 버튼처럼 중복 클릭을 막아야 하는 자리에 필수입니다.
.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 챗봇이 응답을 준비 중이거나 음성 비서가 듣는 중임을 알릴 때 씁니다.
.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에서 최신 지원표를 확인하고 폴백이 필요한지 판단하십시오.