Tailwind 애니메이션 유틸 9종(v4) — 한 줄 복붙으로
Tailwind 애니메이션은 tailwind.config.js가 아니라 CSS 파일 안 @theme 블록에 --animate- 변수와 @keyframes를 같이 적어서 만드는 Tailwind v4의 새 방식입니다.
자동 재생 · 눌러서 해당 항목으로 · 전부 한 zip
- 01 @theme 커스텀 키프레임
- 02 tw-animate-css 진입 유틸
- 03 페이드+슬라이드 유틸
- 04 임의값 scroll-timeline
- 05 group-hover 아이콘
- 06 data-state 기반 애니메이션(shadcn)
- 07 motion-safe 가드
- 08 spin 유틸
- 09 스태거 유틸
아홉 개는 인기순이 아니라 Tailwind v4 문법을 배우는 순서로 놓았습니다. 먼저 커스텀 애니메이션을 등록하는 법(01~03)을 익히고, 브라우저가 갈리는 기능을 안전하게 흉내 내는 법(04), 상태에 반응시키는 법(05~06), 접근성 가드(07), 기존 유틸을 확장하는 법(08~09) 순서입니다. Tailwind 클래스 조각만 옮겼고, 완결본과 SCSS 판은 압축 파일 하나에 몰아 뒀습니다.
01@theme 커스텀 키프레임
tailwind.config.js 없이 CSS의 @theme 블록 안에 --animate-pop-spin 변수와 @keyframes를 같이 적으면, 그 이름이 곧 animate-pop-spin 유틸 클래스가 됩니다. 배지가 회전하며 튀어나오는 등장 애니메이션에 씁니다.
<style>
@theme {
--animate-pop-spin: pop-spin 900ms cubic-bezier(.34,1.56,.64,1) both;
@keyframes pop-spin {
0% { opacity: 0; transform: scale(.5) rotate(-8deg); }
60% { opacity: 1; transform: scale(1.08) rotate(3deg); }
100% { opacity: 1; transform: scale(1) rotate(0deg); }
}
}
</style>
<span class="animate-pop-spin motion-reduce:animate-none rounded-full bg-[#fff7e6] px-4 py-2 font-bold">NEW</span>
02tw-animate-css 진입 유틸
tw-animate-css 플러그인의 animate-in fade-in slide-in-from-top-8 duration-500 조합을 그대로 쓰면, 카드가 위 32px에서 투명하게 시작해 제자리로 내려오며 선명해집니다. 모달이나 토스트의 첫 등장에 씁니다.
npm install -D tw-animate-css
<!-- app.css 에 @import "tw-animate-css"; 한 줄만 추가하면 끝 -->
<div
class="animate-in fade-in slide-in-from-top-8 duration-500
motion-reduce:animate-none
rounded-2xl bg-[#17141a] p-6"
>
<p class="text-[#fff7e6] font-bold">Deployed</p>
<p class="text-[#fff7e6]/70 text-sm">The new version just went live</p>
</div>
03페이드+슬라이드 유틸
플러그인 없이 @theme에 --animate-fade-slide 하나만 정의해 opacity와 translateY를 한 키프레임에 묶으면, 문단이 아래 16px에서 떠오르며 나타나는 유틸 클래스 하나로 끝납니다. 히어로 문단이나 섹션 타이틀 등장에 씁니다.
<style>
@theme {
--animate-fade-slide: fade-slide 400ms ease-out both;
@keyframes fade-slide {
from { opacity: 0; transform: translateY(16px); }
to { opacity: 1; transform: translateY(0); }
}
}
</style>
<p class="animate-fade-slide text-center font-bold text-2xl">One line is all it takes</p>
04임의값 scroll-timeline
Tailwind는 scroll-timeline 전용 유틸이 없어 임의 속성 [animation-timeline:scroll()]으로 붙이는데, 브라우저마다 지원이 갈려 이 데모는 같은 진행률을 안전한 루프로 재현합니다. 긴 글의 읽기 진행바나 랜딩 스크롤 인디케이터에 씁니다.
<!-- 지원 브라우저에서만: 아래 구문은 caniuse.com 에서 지원 범위를 먼저 확인하세요 -->
<div class="h-2 rounded-full bg-black/10">
<div
class="h-full rounded-full bg-[#2f6df6] origin-left
animate-[fill_1s_linear] [animation-timeline:scroll()] [animation-range:0%_100%]"
></div>
</div>
05group-hover 아이콘
부모에 group을 걸고 아이콘에 group-hover:translate-x-1을 걸면, 카드 어디를 호버하든 안쪽 화살표만 오른쪽으로 4px 미끄러집니다. 카드형 CTA나 리스트 항목의 화살표에 씁니다.
<a class="group inline-flex items-center gap-3 rounded-xl bg-white/8 px-6 py-4" href="/more">
<span class="text-[#fff7e6] font-semibold">더 알아보기</span>
<svg class="size-4 text-[#2f6df6] transition-transform duration-200
group-hover:translate-x-1 group-focus-visible:translate-x-1" viewBox="0 0 24 24">
<path d="M4 12h14M13 6l6 6-6 6" fill="none" stroke="currentColor" stroke-width="2" />
</svg>
</a>
06data-state 기반 애니메이션(shadcn)
shadcn 컴포넌트들처럼 JS가 data-state="open"만 토글하고, CSS는 data-[state=open]: 변형으로 scale-y-100과 opacity-100을 줍니다. width나 height, grid-template-rows로 레이아웃을 다시 계산하게 하지 않는 게 핵심입니다.
<button onclick="p.dataset.state = p.dataset.state === 'open' ? 'closed' : 'open'"
aria-expanded="false" class="w-full rounded-xl bg-[#17141a] px-5 py-4 text-left text-[#fff7e6]">
배송 안내
</button>
<div id="p" data-state="closed"
class="origin-top scale-y-0 opacity-0 transition-all duration-200
data-[state=open]:scale-y-100 data-[state=open]:opacity-100">
<p>결제 후 1~2일 안에 출고됩니다.</p>
</div>
07motion-safe 가드
@media를 따로 안 써도 motion-safe:-translate-y-1과 motion-reduce:shadow-md를 나란히 걸면, 움직임을 줄인 사람에겐 그림자만 옅어지고 뜨는 동작 자체가 처음부터 안 걸립니다. 저장 버튼 같은 프라이머리 CTA의 호버 피드백에 씁니다.
<button
class="rounded-xl bg-[#fff7e6] px-8 py-4 font-bold shadow-md transition-all duration-200
motion-safe:hover:-translate-y-1 motion-safe:hover:shadow-lg
motion-reduce:hover:shadow-md"
>
변경사항 저장
</button>
08spin 유틸
기본 제공 animate-spin(1초)은 그대로 두고 @theme에 --animate-spin-slow: spin 3s linear infinite;만 더하면, 로고와 로딩 링이 같은 회전을 서로 다른 속도로 함께 돕니다. 새 @keyframes를 다시 만들 필요가 없습니다.
<style>
@theme {
--animate-spin-slow: spin 3s linear infinite;
}
</style>
<div class="flex items-center gap-8">
<span class="animate-spin size-14 rounded-full border-4 border-black/10 border-t-[#2f6df6]"></span>
<span class="animate-spin-slow size-14 rounded-full border-4 border-black/10 border-t-[#17141a]"></span>
</div>
09스태거 유틸
리스트 항목마다 [animation-delay:calc(var(--i)*80ms)]으로 --i만 0부터 1씩 늘려 주면, 같은 animate-fade-slide 클래스 하나가 순서대로 80ms씩 늦게 떠오릅니다. 알림 리스트나 대시보드 카드의 순차 로드에 씁니다.
<ul class="flex flex-col gap-2">
<li style="--i:0" class="animate-fade-slide [animation-delay:calc(var(--i)*80ms)] rounded-lg bg-white/8 px-4 py-2">
새 댓글이 달렸습니다
</li>
<li style="--i:1" class="animate-fade-slide [animation-delay:calc(var(--i)*80ms)] rounded-lg bg-white/8 px-4 py-2">
팔로워가 늘었습니다
</li>
<li style="--i:2" class="animate-fade-slide [animation-delay:calc(var(--i)*80ms)] rounded-lg bg-white/8 px-4 py-2">
결제가 완료됐습니다
</li>
</ul>
어디서 깨지는가 — 함정 하나
가장 자주 겪는 함정은 v3에서 v4로 넘어올 때입니다. tailwind.config.js의 theme.extend.keyframes와 animation을 그대로 두고 클래스만 animate-pop-spin으로 바꿔 쓰면 아무 일도 일어나지 않습니다. Tailwind v4는 CSS-first라 설정 파일을 아예 읽지 않고, 정의는 공식 문서가 안내하는 대로 CSS의 @theme 블록 안에 있어야 합니다. 06 data-state 아코디언을 만들 때도 비슷한 함정이 있었습니다. grid-template-rows: 0fr → 1fr로 높이를 접었다 펴는 방법이 흔히 쓰이지만, 이건 브라우저가 매 프레임 트랙 크기를 다시 계산하게 만드는 레이아웃 속성이라 이 편의 transform·opacity 원칙과 어긋납니다. 그래서 06 패널은 scaleY와 transform-origin: top으로 접었습니다. 08 spin 유틸에서도 비슷한 걸 겪었는데, 두 번째 링에 새 @keyframes를 또 만드는 대신 animation-duration만 오버라이드했더니 컴파일된 CSS 크기가 그대로였습니다. 같은 spin 키프레임을 속도만 다르게 공유하는 쪽이 유틸 개수를 늘리지 않는 방법이고, 이 값들이 이미 반영된 아홉 개 파일의 압축 비밀번호는 mtsjkxrf이고, 아래 버튼으로 받을 수 있습니다.
접근성
아홉 개 전부 prefers-reduced-motion을 처리하지만 방식이 둘로 갈립니다. 01·02·03·04·06·08·09는 motion-reduce:animate-none 계열로 애니메이션 자체를 끄고 최종 상태만 보여주고, 05·07은 motion-safe: 변형 안에만 hover 동작을 넣어 애초에 걸지 않습니다. 06의 패널은 움직임을 꺼도 열림 상태가 opacity로는 표시되도록 남겨 뒀습니다. 이 미디어쿼리의 지원 범위는 MDN prefers-reduced-motion에서 확인할 수 있습니다.
다른 애니메이션 유틸·모음은 CSS 애니메이션 카테고리에 있고, 이 사이트가 무엇을 다루는지는 소개에 있습니다.
FAQ
Tailwind v3의 tailwind.config.js 애니메이션 설정은 v4에서 그대로 쓸 수 있나요?
아니요. v4는 설정 파일을 읽지 않는 CSS-first 구조라 theme.extend.keyframes와 animation은 무시됩니다. @theme 블록 안에 --animate-* 변수와 @keyframes로 옮겨야 클래스가 생성됩니다.
tw-animate-css 같은 플러그인 없이도 진입 애니메이션을 만들 수 있나요?
가능합니다. 03처럼 @theme에 커스텀 --animate-* 변수 하나만 정의하면 플러그인 없이도 같은 효과를 낼 수 있습니다. 다만 방향별 유틸(slide-in-from-*)처럼 조합이 많다면 플러그인이 코드량을 줄여 줍니다.
React에서는 data-state 토글을 어떻게 옮기나요?
zip의 react/ 폴더는 useState로 열림 여부를 들고 있다가 data-state 속성에 그대로 반영합니다. SCSS 모듈은 [data-state="open"] 선택자만 보고 움직이므로 컴포넌트 쪽 로직은 상태 하나만 바꾸면 됩니다.