Skip to main content
Tailwind CSS Mastery·Lesson 5 of 5

Animations & Transitions

Smooth transitions and animations give users visual feedback and make interfaces feel responsive. Tailwind provides utilities for both simple transitions and complex keyframe animations.

Transition Utilities

The transition utility enables CSS transitions on an element. Pair it with a duration and optional easing:

<!-- Basic hover transition -->
<button class="bg-blue-600 text-white px-4 py-2 rounded-lg
               transition-colors duration-200 ease-in-out
               hover:bg-blue-700">
  Save
</button>

<!-- Scale on hover -->
<div class="bg-white rounded-xl shadow p-6
            transition-transform duration-300 ease-out
            hover:scale-105">
  <h3 class="font-semibold">Hover me</h3>
  <p class="text-gray-500 text-sm">I scale up smoothly</p>
</div>

<!-- Combined transitions -->
<a href="#" class="inline-block px-6 py-3 bg-gray-900 text-white rounded-lg
                   transition-all duration-300
                   hover:bg-gray-800 hover:shadow-lg hover:-translate-y-0.5">
  Get Started
</a>

Transition Property Utilities

UtilityProperties transitioned
transition-noneNone
transition-allAll properties
transition-colorsColor, background-color, border-color, text-decoration-color, fill, stroke
transition-opacityOpacity
transition-shadowBox shadow
transition-transformTransform
transitionColor, background, border, text-decoration, fill, stroke, opacity, box-shadow, transform, filter

Duration & Easing

<!-- Durations: 75, 100, 150, 200, 300, 500, 700, 1000 -->
<div class="transition duration-300">300ms</div>

<!-- Timing functions -->
<div class="transition ease-linear">Linear</div>
<div class="transition ease-in">Ease in (slow start)</div>
<div class="transition ease-out">Ease out (slow end)</div>
<div class="transition ease-in-out">Ease in-out</div>

Built-In Animations

Tailwind ships with four animations out of the box:

<!-- Spin: rotating loading indicator -->
<svg class="animate-spin h-5 w-5 text-blue-600" viewBox="0 0 24 24">
  <circle class="opacity-25" cx="12" cy="12" ="10" stroke="currentColor" stroke-width="4" fill="none"/>
  <path class="opacity-75" fill="currentColor" ="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/>
</svg>

<!-- Ping: notification badge pulse -->
<span class="relative flex h-3 w-3">
  <span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-400 opacity-75"></span>
  <span class="relative inline-flex rounded-full h-3 w-3 bg-red-500"></span>
</span>

<!-- Pulse: skeleton loading placeholder -->
<div class="animate-pulse space-y-4">
  <div class="h-4 bg-gray-300 rounded w-3/4"></div>
  <div class="h-4 bg-gray-300 rounded w-1/2"></div>
  <div class="h-4 bg-gray-300 rounded w-5/6"></div>
</div>

<!-- Bounce: attention-drawing indicator -->
<div class="animate-bounce text-4xl">arrow down</div>

Custom Keyframe Animations

Define custom animations in your CSS using @theme and standard @keyframes:

@theme {
  --animate-fade-in: fade-in 0.5s ease-out forwards;
  --animate-slide-up: slide-up 0.4s ease-out forwards;
  --animate-slide-in-right: slide-in-right 0.3s ease-out forwards;
  --animate-scale-in: scale-in 0.2s ease-out forwards;
}

@keyframes fade-in {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

@keyframes slide-up {
  from {
    opacity: 0;
    transform: translateY(20px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@keyframes slide-in-right {
  from {
    opacity: 0;
    transform: translateX(100%);
  }
  to {
    opacity: 1;
    transform: translateX(0);
  }
}

@keyframes scale-in {
  from {
    opacity: 0;
    transform: scale(0.95);
  }
  to {
    opacity: 1;
    transform: scale(1);
  }
}

Use them in your markup:

<!-- Fade in a hero section -->
<section class="animate-fade-in">
  <h1 class="text-5xl font-bold">Welcome</h1>
</section>

<!-- Stagger children with animation delay -->
<div class="space-y-4">
  <div class="animate-slide-up" style="animation-delay: 0ms">First item</div>
  <div class="animate-slide-up" style="animation-delay: 100ms">Second item</div>
  <div class="animate-slide-up" style="animation-delay: 200ms">Third item</div>
</div>

<!-- Toast notification sliding in -->
<div class="animate-slide-in-right fixed bottom-4 right-4 bg-white shadow-lg rounded-lg p-4 border">
  <p class="font-medium">Changes saved successfully.</p>
</div>

<!-- Modal with scale animation -->
<div class="animate-scale-in bg-white rounded-xl shadow-2xl p-8 max-w-md mx-auto">
  <h2 class="text-xl font-bold">Confirm Action</h2>
  <p class="mt-2 text-gray-600">Are you sure you want to proceed?</p>
</div>

Transition with JavaScript State

In React, combine Tailwind transitions with state for dynamic UI:

import { useState } from "react";

export function Accordion({ title, children }: { title: string; children: React.ReactNode }) {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <div className="border border-gray-200 rounded-lg">
      <button
        onClick={()=> setIsOpen(!isOpen)}
        className="flex items-center justify-between w-full px-4 py-3 text-left font-medium"
      >
        {title}
        <svg
          className={`w-5 h-5 transition-transform duration-200 ${isOpen ? "rotate-180" : ""}`}
          fill="none"
          viewBox="0 0 24 24"
          stroke="currentColor"
        >
          <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} ="M19 9l-7 7-7-7" />
        </svg>
      </button>
      <div
        className={`overflow-hidden transition-all duration-300 ${
          isOpen ? "max-h-96 opacity-100" : "max-h-0 opacity-0"
        }`}
      >
        <div className="px-4 pb-4 text-gray-600">{children}</div>
      </div>
    </div>
  );
}

Motion-Safe and Motion-Reduced

Some users have vestibular disorders and prefer reduced motion. Tailwind provides variants to respect this:

<!-- Only animate if the user hasn't requested reduced motion -->
<div clas="motion-safe:animate-slide-up motion-reduce:opacity-100">
  Content that slides up for most users, but appears instantly
  for users who prefer reduced motion.
</div>

<!-- Simplify transitions for reduced motion users -->
<button clas="transition-all duration-300 hover:scale-105
               motion-reduce:transition-none motion-reduce:hover:scale-100">
  Hover Effect
</button>

A good practice is to wrap all non-essential animations with motion-safe::

<div class="motion-safe:animate-fade-in">
  <h1 class="text-4xl font-bold">Dashboard</h1>
</div>

<div class="motion-safe:transition-transform motion-safe:duration-300 motion-safe:hover:-translate-y-1">
  <div class="bg-white rounded-lg shadow p-6">
    Hover to lift
  </div>
</div>

Practical Example: Animated Notification

<div class="fixed bottom-4 right-4 flex flex-col gap-3">
  <!-- Success toast -->
  <div class="motion-safe:animate-slide-in-right flex items-center gap-3
              bg-white dark:bg-gray-800 shadow-lg rounded-lg px-4 py-3
              border-l-4 border-green-500">
    <svg class="w-5 h-5 text-green-500 shrink-0" fill="currentColor" viewBox="0 0 20 20">
      <path d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"/>
    </svg>
    <div>
      <p class="font-medium text-gray-900 dark:text-white text-sm">Success</p>
      <p class="text-gray-500 dark:text-gray-400 text-xs">Your file has been uploaded.</p>
    </div>
    <button class="ml-4 text-gray-400 hover:text-gray-600 transition-colors">
      <svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
        <path d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"/>
      </svg>
    </button>
  </div>
</div>