Home Module 08 CSS Transitions

1. Introduction

When a button changes color on hover, or a dropdown slides open, that smooth movement is usually a CSS transition. A transition tells the browser: "whenever this CSS property changes, animate the change over a given duration instead of jumping instantly."

Transitions are the simplest form of CSS animation — they animate between exactly two states (the default state and the triggered state). You do not write any frames or define sequences. Just specify which property, how long, and what easing curve.

In this lesson you will learn:

  • The four transition sub-properties and the shorthand
  • Easing functions (timing functions) and how they feel
  • Which properties you can and cannot transition
  • How to transition multiple properties at once
  • Performance considerations — why transform and opacity are special

2. Theory

2.1 The four sub-properties

PropertyWhat it controlsDefault
transition-propertyWhich CSS property to animate (all or a specific name)all
transition-durationHow long the transition takes (e.g., 0.3s, 200ms)0s
transition-timing-functionThe acceleration curve (easing)ease
transition-delayHow long to wait before starting0s

2.2 The shorthand

/* transition: property duration timing-function delay */
.btn {
  transition: background-color 0.3s ease 0s;
}

/* Multiple properties — comma-separated */
.card {
  transition: transform 0.25s ease, box-shadow 0.25s ease;
}

2.3 Where to put the transition rule

Always put transition on the base state (the element itself), not on the hover/focus state. This way the transition plays in both directions — going in and coming out.

/* Good — transition on base state, direction plays both ways */
.btn {
  background-color: royalblue;
  transition: background-color 0.3s ease;
}
.btn:hover {
  background-color: navy;
}

/* Bad — only transitions in, jumps back instantly */
.btn { background-color: royalblue; }
.btn:hover {
  background-color: navy;
  transition: background-color 0.3s ease;
}

2.4 Timing functions

ValueFeelUse when
easeStarts fast, ends slow (default)General UI interactions
linearConstant speedSpinners, progress bars
ease-inStarts slow, ends fastElements leaving the screen
ease-outStarts fast, ends slowElements entering the screen
ease-in-outSlow at both endsBack-and-forth / toggles
cubic-bezier(x1,y1,x2,y2)Custom curveFine-tuned motion design
steps(n)Jumps in n discrete stepsSprite animations, loading dots

2.5 Which properties can be transitioned?

Only properties with numeric or colour values can be transitioned. The browser needs a start value and an end value to interpolate between them.

Transitionable: color, background-color, opacity, transform, width, height, top, left, border-radius, box-shadow, font-size, padding, margin, grid-template-columns (partially)...

Not transitionable: display, visibility (jumps between values), background-image (different URL), font-family, content.

Avoid transition: all in production — it animates every changing property (including layout-triggering ones) and can hurt performance.

2.6 Performance — transform and opacity

The browser rendering pipeline has three stages: layout (size/position) → paint (pixels) → composite (layers). Layout changes are the most expensive.

  • transform and opacity run on the compositor thread — GPU-accelerated, 60 fps even on slow devices.
  • Avoid transitioning width, height, top, left, margin, padding — these cause layout recalculation ("layout thrashing").
/* Avoid — triggers layout */
.box:hover { left: 20px; }

/* Prefer — compositor only */
.box:hover { transform: translateX(20px); }

2.7 The transition shorthand with multiple properties

.nav-link {
  color: #333;
  border-bottom: 2px solid transparent;
  /* Transition color and border separately with different durations */
  transition: color 0.2s ease, border-bottom-color 0.3s ease 0.05s;
}
.nav-link:hover {
  color: royalblue;
  border-bottom-color: royalblue;
}

2.8 Using transition with JavaScript-toggled classes

Transitions work with any CSS state change — not just pseudo-classes. Adding or removing a class with JavaScript also triggers transitions:

/* CSS */
.sidebar {
  transform: translateX(-100%);
  transition: transform 0.35s ease-out;
}
.sidebar.is-open {
  transform: translateX(0);
}

/* JS */
document.querySelector('.menu-btn').addEventListener('click', () => {
  document.querySelector('.sidebar').classList.toggle('is-open');
});

3. Real World Example

Transitions appear everywhere in professional interfaces:

  • Buttons — background color, box-shadow, and scale change on hover/active.
  • Navigation — underline slides in beneath the active link.
  • Dropdowns / off-canvas menus — translate into view from off-screen.
  • Form inputs — border color and box-shadow transition on focus.
  • Cards — subtle lift (box-shadow + translateY) on hover.
  • Modals — opacity fades in while backdrop fades in simultaneously.
  • Tooltips — opacity + scale transition from invisible to visible.

4. Code Example

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>CSS Transitions Demo</title>
  <style>
    *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
    body { font-family: system-ui, sans-serif; padding: 2rem; background: #f5f5f5; }
    .demo-grid {
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
      gap: 1.5rem;
      max-width: 900px;
      margin: 0 auto;
    }
    .demo-label { font-size: 0.8rem; color: #666; margin-bottom: 0.5rem; }

    /* ---- 1. Button with background + transform ---- */
    .btn-demo {
      display: inline-block;
      padding: 0.75rem 1.5rem;
      background: royalblue;
      color: white;
      border: none;
      border-radius: 6px;
      cursor: pointer;
      font-size: 1rem;
      transition: background-color 0.2s ease, transform 0.15s ease, box-shadow 0.2s ease;
    }
    .btn-demo:hover {
      background-color: #1a3dbf;
      transform: translateY(-2px);
      box-shadow: 0 4px 12px rgba(65, 105, 225, 0.4);
    }
    .btn-demo:active {
      transform: translateY(0);
      box-shadow: none;
    }

    /* ---- 2. Card lift effect ---- */
    .card-demo {
      background: white;
      border-radius: 10px;
      padding: 1.25rem;
      box-shadow: 0 2px 6px rgba(0,0,0,.1);
      transition: transform 0.25s ease, box-shadow 0.25s ease;
    }
    .card-demo:hover {
      transform: translateY(-6px);
      box-shadow: 0 10px 30px rgba(0,0,0,.15);
    }

    /* ---- 3. Input focus ring ---- */
    .input-demo {
      width: 100%;
      padding: 0.6rem 0.8rem;
      border: 2px solid #ccc;
      border-radius: 6px;
      font-size: 1rem;
      outline: none;
      transition: border-color 0.2s ease, box-shadow 0.2s ease;
    }
    .input-demo:focus {
      border-color: royalblue;
      box-shadow: 0 0 0 3px rgba(65,105,225,.25);
    }

    /* ---- 4. Underline nav link ---- */
    .nav-link-demo {
      display: inline-block;
      text-decoration: none;
      color: #333;
      padding-bottom: 4px;
      border-bottom: 2px solid transparent;
      transition: color 0.2s ease, border-bottom-color 0.2s ease;
    }
    .nav-link-demo:hover {
      color: royalblue;
      border-bottom-color: royalblue;
    }

    /* ---- 5. Toggled sidebar ---- */
    .toggle-wrap { position: relative; height: 80px; overflow: hidden; background: #eee; border-radius: 8px; }
    .panel {
      position: absolute;
      top: 0; left: 0;
      width: 200px; height: 100%;
      background: royalblue;
      color: white;
      display: flex; align-items: center; justify-content: center;
      transform: translateX(-100%);
      transition: transform 0.35s ease-out;
    }
    .panel.is-open { transform: translateX(0); }
    .open-btn {
      position: absolute; top: 50%; right: 1rem;
      transform: translateY(-50%);
      background: #333; color: white; border: none; padding: 0.4rem 0.8rem;
      border-radius: 4px; cursor: pointer;
    }
  </style>
</head>
<body>
<div class="demo-grid">

  <div>
    <p class="demo-label">Button (hover me)</p>
    <button class="btn-demo">Click Me</button>
  </div>

  <div class="card-demo">
    <p class="demo-label">Card lift (hover me)</p>
    <p>Hover to see the elevation effect.</p>
  </div>

  <div>
    <p class="demo-label">Input focus</p>
    <input class="input-demo" type="text" placeholder="Click to focus">
  </div>

  <div>
    <p class="demo-label">Nav link underline</p>
    <a class="nav-link-demo" href="#">About Us</a>
  </div>

  <div class="toggle-wrap">
    <div class="panel" id="panel">Sidebar Panel</div>
    <button class="open-btn" onclick="document.getElementById('panel').classList.toggle('is-open')">Toggle</button>
  </div>

</div>
</body>
</html>

5. Code Breakdown

Button transition (lines 20–25)

transition: background-color 0.2s ease, transform 0.15s ease, box-shadow 0.2s ease — three separate properties with different durations to keep the hover feeling snappy yet polished. transform: translateY(-2px) gives a subtle float effect without touching layout.

Card lift (lines 31–39)

A classic pattern: on hover, the card moves up 6 px (translateY(-6px)) and casts a deeper shadow. Because we use transform instead of top/margin-top, the layout of surrounding elements is unaffected and the animation is GPU-accelerated.

Input focus ring (lines 42–51)

outline: none removes the browser default, but we add an accessible focus ring via box-shadow. The semi-transparent ring (rgba(65,105,225,.25)) communicates focus without being visually harsh. Both border-color and box-shadow transition at 0.2s.

Underline nav link (lines 54–63)

The trick is setting border-bottom: 2px solid transparent on the base state. This keeps the element height stable so the underline appearing does not shift surrounding content. On hover the border color transitions from transparent to royalblue.

Toggled panel (lines 66–78)

The panel starts at transform: translateX(-100%) — fully off-screen to the left. Toggling the is-open class changes it to translateX(0). Because transition: transform 0.35s ease-out is on the base state, the animation plays both when opening and closing.

6. Common Mistakes

Mistake 1 — Transitioning layout properties

/* Bad — triggers layout recalculation, janky on low-end devices */
.box:hover { width: 200px; }

/* Good — transform only composites, no layout impact */
.box:hover { transform: scaleX(1.5); }

Mistake 2 — Using transition: all

/* Bad — animates every changing property including layout triggers */
.card { transition: all 0.3s ease; }

/* Good — be explicit */
.card { transition: transform 0.3s ease, box-shadow 0.3s ease; }

Mistake 3 — Forgetting to transition on the base state

/* Bad — only transitions in, jumps back */
.btn:hover { background: navy; transition: background 0.3s; }

/* Good — on base state */
.btn { background: royalblue; transition: background 0.3s; }
.btn:hover { background: navy; }

Mistake 4 — Removing :focus-visible styles without replacement

Never use outline: none without providing an alternative focus indicator (like a box-shadow ring). Keyboard users depend on visible focus states.

Mistake 5 — Animating opacity but not handling visibility

/* Bad — element is invisible but still clickable */
.tooltip { opacity: 0; transition: opacity 0.2s; }
.tooltip.visible { opacity: 1; }

/* Better — combine with visibility */
.tooltip {
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.2s, visibility 0s 0.2s; /* delay visibility removal */
}
.tooltip.visible {
  opacity: 1;
  visibility: visible;
  transition-delay: 0s;
}

Mistake 6 — Ignoring prefers-reduced-motion

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

7. Best Practices

  1. Put transition on the base state so it animates in both directions.
  2. Prefer transform and opacity over position/size properties for performance.
  3. Avoid transition: all — name the specific properties you want to animate.
  4. Keep durations short — UI interactions feel best at 150–350 ms. Use longer durations (400–600 ms) only for page-level transitions.
  5. Use ease or ease-out for elements entering the screen; ease-in for elements leaving.
  6. Respect prefers-reduced-motion — wrap or disable transitions for users who opt out.
  7. Provide a keyboard-accessible focus style — never remove outline without a replacement.
  8. Use cubic-bezier() for custom easing — tools like easings.net let you design and preview curves visually.
  9. Combine opacity and visibility when hiding elements so they are not keyboard-focusable while invisible.
  10. Test on real devices — animations that feel smooth on a desktop can stutter on mid-range phones.

8. Practice Exercise

Build a polished interactive card component.

Requirements

  1. Create a product card with an image area, title, description, and a "Buy" button.
  2. On hover, the card should lift (translateY + deeper box-shadow).
  3. The "Buy" button should change background color and scale up slightly on hover.
  4. Add a "wishlist" heart icon that transitions color and transform: scale() when clicked (use JavaScript to toggle a class).
  5. Ensure the image area has a subtle zoom effect on hover using transform: scale(1.05) (clip with overflow: hidden on the container).
  6. All transitions must use transform or opacity — no animating layout properties.
  7. Add a prefers-reduced-motion media query that disables all transitions.

Bonus

  • Add a tooltip that fades in on hover using the opacity + visibility technique.
  • Animate the underline of a nav bar using the transparent border-bottom technique.

9. Assignment

Add transitions throughout an existing page.

  1. Take any HTML page with links, buttons, and form inputs.
  2. Add a hover transition to every button (background, shadow, transform).
  3. Add a focus transition to every input (border-color and box-shadow ring).
  4. Add hover transitions to all navigation links (color + underline).
  5. Implement an off-canvas sidebar that slides in from the left using a JS-toggled class and transform: translateX.
  6. Add a prefers-reduced-motion override block at the end of your CSS.
  7. Open DevTools → Rendering → Paint flashing. Verify that your transitions do not cause green flashes (which would indicate paint operations).

Deliverable: A single HTML file. Add a comment block at the top listing which transition techniques you used.

10. Interview Questions

  1. What is the difference between a CSS transition and a CSS animation?
    A transition animates between two states (start and end) and is triggered by a property change. An animation (@keyframes) can have multiple intermediate steps and can run automatically without a trigger.
  2. Why should you put the transition property on the base element rather than the :hover state?
    If it is only on :hover, the transition plays going into hover but the property snaps back instantly when hover ends. On the base element, the transition plays in both directions.
  3. Why are transform and opacity preferred over left/top/width for transitions?
    transform and opacity are composited on the GPU, bypassing layout and paint stages. Changing width, height, top, left, etc. triggers layout recalculation, which is expensive and can cause jank.
  4. What is the difference between ease-in and ease-out?
    ease-in starts slowly and accelerates — best for elements leaving the screen. ease-out starts fast and decelerates — best for elements entering the screen, feeling natural as they "land".
  5. How do you make a transition accessible for users who prefer reduced motion?
    Use @media (prefers-reduced-motion: reduce) to disable or minimise transitions for users who have opted into reduced motion in their operating system settings.
  6. Can you transition the display property?
    Not directly — display switches between discrete values with no interpolation. Use opacity + visibility (or transform) instead, with a transition-delay on visibility to keep it timed with the opacity fade.

11. Additional Resources

  • MDN — Using CSS transitions — comprehensive reference
  • MDN — transition shorthand
  • Google web.dev — Animations performance guide — why transform and opacity win
  • easings.net — visual cubic-bezier easing function library
  • cubic-bezier.com — interactive tool for designing custom easing curves
  • Animate.style — CSS animation library to study real-world transition patterns