GODRICH

Tailwind 애니메이션 유틸 9종(v4) — 한 줄 복붙으로

Tailwind 애니메이션은 tailwind.config.js가 아니라 CSS 파일 안 @theme 블록에 --animate- 변수와 @keyframes를 같이 적어서 만드는 Tailwind v4의 새 방식입니다.

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

아홉 개는 인기순이 아니라 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 유틸 클래스가 됩니다. 배지가 회전하며 튀어나오는 등장 애니메이션에 씁니다.

cubic-bezier 오버슈트@keyframes 동봉900ms
<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에서 투명하게 시작해 제자리로 내려오며 선명해집니다. 모달이나 토스트의 첫 등장에 씁니다.

@keyframes slide-intranslateY(-32px)500ms
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에서 떠오르며 나타나는 유틸 클래스 하나로 끝납니다. 히어로 문단이나 섹션 타이틀 등장에 씁니다.

@keyframes fade-slidetranslateY+opacity400ms
<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()]으로 붙이는데, 브라우저마다 지원이 갈려 이 데모는 같은 진행률을 안전한 루프로 재현합니다. 긴 글의 읽기 진행바나 랜딩 스크롤 인디케이터에 씁니다.

@keyframes fill-loop임의 속성caniuse 확인
<!-- 지원 브라우저에서만: 아래 구문은 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나 리스트 항목의 화살표에 씁니다.

.card:hover 자식translateX(4px)200ms
<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-100opacity-100을 줍니다. width나 height, grid-template-rows로 레이아웃을 다시 계산하게 하지 않는 게 핵심입니다.

[data-state=open]scaleY(1)attr 토글 JS
<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-1motion-reduce:shadow-md를 나란히 걸면, 움직임을 줄인 사람에겐 그림자만 옅어지고 뜨는 동작 자체가 처음부터 안 걸립니다. 저장 버튼 같은 프라이머리 CTA의 호버 피드백에 씁니다.

prefers-reduced-motion: no-preferencereduce 는 box-shadow 만220ms
<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를 다시 만들 필요가 없습니다.

@keyframes spinrotate(360deg) 공유linear infinite
<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씩 늦게 떠오릅니다. 알림 리스트나 대시보드 카드의 순차 로드에 씁니다.

var(--i)animation-delay 계산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.jstheme.extend.keyframesanimation을 그대로 두고 클래스만 animate-pop-spin으로 바꿔 쓰면 아무 일도 일어나지 않습니다. Tailwind v4는 CSS-first라 설정 파일을 아예 읽지 않고, 정의는 공식 문서가 안내하는 대로 CSS의 @theme 블록 안에 있어야 합니다. 06 data-state 아코디언을 만들 때도 비슷한 함정이 있었습니다. grid-template-rows: 0fr → 1fr로 높이를 접었다 펴는 방법이 흔히 쓰이지만, 이건 브라우저가 매 프레임 트랙 크기를 다시 계산하게 만드는 레이아웃 속성이라 이 편의 transform·opacity 원칙과 어긋납니다. 그래서 06 패널은 scaleYtransform-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.keyframesanimation은 무시됩니다. @theme 블록 안에 --animate-* 변수와 @keyframes로 옮겨야 클래스가 생성됩니다.

tw-animate-css 같은 플러그인 없이도 진입 애니메이션을 만들 수 있나요?

가능합니다. 03처럼 @theme에 커스텀 --animate-* 변수 하나만 정의하면 플러그인 없이도 같은 효과를 낼 수 있습니다. 다만 방향별 유틸(slide-in-from-*)처럼 조합이 많다면 플러그인이 코드량을 줄여 줍니다.

React에서는 data-state 토글을 어떻게 옮기나요?

zip의 react/ 폴더는 useState로 열림 여부를 들고 있다가 data-state 속성에 그대로 반영합니다. SCSS 모듈은 [data-state="open"] 선택자만 보고 움직이므로 컴포넌트 쪽 로직은 상태 하나만 바꾸면 됩니다.

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

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