Topic 10 / 14

Transitions & Animations

~10 min read  //  CSS Series  //  Coding India

Transitions — Animate State Changes

A transition smooths a property change between two states (hover, focus, class toggles):

.btn {
  background: #6366F1;
  transform: translateY(0);
  transition: background 0.2s ease, transform 0.2s ease;
}
.btn:hover {
  background: #4F46E5;
  transform: translateY(-2px);
}

Syntax: transition: property duration easing delay. List specific properties — transition: all animates things you didn’t intend and costs performance.

Easing

transition-timing-function: ease;          /* default — fine */
transition-timing-function: ease-out;      /* fast start, gentle stop — best for UI */
transition-timing-function: cubic-bezier(0.16, 1, 0.3, 1);  /* springy, custom */

UI rule of thumb: 150–250ms with ease-out. Under 100ms feels broken, over 400ms feels slow.

Animate Only transform & opacity

Animating width, height, top, or margin forces the browser to recalculate layout every frame — janky on weak devices. transform and opacity run on the GPU compositor and stay at 60fps:

/* janky */            /* smooth — same visual */
left: 20px;            transform: translateX(20px);
width: 110%;           transform: scale(1.1);
top: -8px;             transform: translateY(-8px);

Keyframe Animations — Multi-Step Motion

Transitions need a trigger; animations run on their own:

@keyframes fadeUp {
  from { opacity: 0; transform: translateY(24px); }
  to   { opacity: 1; transform: translateY(0); }
}

.hero h1 {
  animation: fadeUp 0.8s ease-out both;
}

/* staggered entrance */
.card:nth-child(2) { animation-delay: 0.1s; }
.card:nth-child(3) { animation-delay: 0.2s; }
@keyframes pulse {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.4; }
}
.live-dot {
  animation: pulse 2s ease-in-out infinite;
}

@keyframes spin {
  to { transform: rotate(360deg); }
}
.spinner {
  animation: spin 0.8s linear infinite;
}

Key properties: animation-fill-mode: both (hold the first/last frame — prevents the “flash before animation” bug), animation-iteration-count: infinite, animation-direction: alternate.

Scroll-Triggered Reveals

The classic pattern pairs CSS with a tiny IntersectionObserver:

.reveal {
  opacity: 0;
  transform: translateY(24px);
  transition: opacity 0.6s ease-out, transform 0.6s ease-out;
}
.reveal.visible {
  opacity: 1;
  transform: translateY(0);
}
const io = new IntersectionObserver((entries) => {
  entries.forEach(e => e.isIntersecting && e.target.classList.add("visible"));
});
document.querySelectorAll(".reveal").forEach(el => io.observe(el));

Respect Reduced Motion

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}

Some users get motion sickness from animation. This query is part of shipping professional CSS, not an optional extra.