C.W.K.
Stream
Lesson 06 of 07 · published

Animation Primitives — Transitions, Keyframes, Tailwind Utilities

~12 min · animation, transition, keyframes, tailwind

Level 0React Novice
0 XP0/54 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
Most UI animation isn't a library — it's three CSS primitives wrapped in Tailwind utilities. Knowing the primitives means you reach for Framer Motion only when you actually need it.

The three primitives

  1. Transitions — interpolate between two states (e.g. hover:bg-brand-strong transition-colors). Cheap, GPU-friendly, the default for state-driven UI.
  2. Keyframes — predefined animation sequences (@keyframes) you reference with the animation property. For pulses, spinners, anything that loops or has multiple stops.
  3. Web Animations API — JavaScript-driven, used for orchestration that can't be expressed in CSS (sequencing, dynamic durations). Frame-perfect, but rarely needed.

Tailwind's built-in utilities

Tailwind v4 ships transition, duration-200, ease-in-out, plus built-in keyframe animations: animate-spin, animate-pulse, animate-bounce, animate-ping. Combined with your own @keyframes + utility class (lesson 3), this covers 90% of needs.

When to reach for Framer Motion

Complex interaction: drag-and-drop, layout animations (FLIP), shared element transitions between routes, gesture handling. If you're sequencing multiple elements with different easings and timings, Framer Motion's animate + variants API saves you. For everything else — transitions and keyframes.

Respect prefers-reduced-motion

Some users (vestibular disorders, motion sensitivity) prefer minimal animation. Wrap motion-heavy effects in a media query that disables them when the user has set the preference.

Transitions feel free; keyframes feel expensive. Browsers optimize transitions on transform/opacity to GPU compositing — they don't repaint the layer. Animations that touch layout (width, height, top, left) cause layout thrash. Prefer transform: translate over top/left; opacity over display.

Code

Transition — state-driven, the default·tsx
function CardLink({ href, children }: { href: string; children: React.ReactNode }) {
  return (
    <a
      href={href}
      className={cn(
        "block rounded-card bg-bg-elevated p-4 shadow-sm",
        // Two state-driven changes, smoothed by transition utilities.
        "transition-all duration-200 ease-out",
        "hover:shadow-md hover:translate-y-[-1px]"
      )}
    >
      {children}
    </a>
  );
}
Keyframes — recurring or multi-stop animations·css
@layer utilities {
  .animate-fade-in {
    animation: fade-in 0.3s ease-out both;
  }
  .animate-slide-up {
    animation: slide-up 0.4s ease-out both;
  }
}

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

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

/* Respect user preference */
@media (prefers-reduced-motion: reduce) {
  .animate-fade-in, .animate-slide-up {
    animation: none;
  }
}

External links

Exercise

Add a <Toast> component that fades in and slides up when it appears. Use keyframes in @layer utilities, apply the utility class on mount via a tiny useEffect that adds the class after the element mounts (or via Tailwind's data- attributes if you're using a UI library). Confirm: animation runs once on mount, respects prefers-reduced-motion, and doesn't trigger layout thrash (DevTools → Performance → check the Layout column).
Hint
Mount-only animation pattern: have the element start with opacity-0, then after mount toggle to the visible utility. The transition smooths the swap. Or use the keyframe form above with animation: ... both; so the end-state persists.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue
💛 by Ttoriwarm

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.