What is a CSS transition

A CSS transition is a way to animate the change between two states of a property. You don’t animate “by writing keyframes”. Instead, you say: “When this value changes, please move there smoothly.”

Transitions need two different values:

  • A start value (the default state)
  • An end value (a new state, often triggered by :hover, :focus, .is-open, or :checked)

If nothing changes, nothing transitions.

If you are instead looking for a way to create a continuous animation, check out the CSS Animation Interactive Tutorial.

Your first CSS transition

The most common way to write a transition is the shorthand:

transition: property duration timing-function delay;

Here’s a simple hover example. Hover the card and watch it lift smoothly.

.card {
  transition: transform 250ms ease;
}

.card:hover {
transform: translateY(-10px);
} 
.card {
  transition: transform 600ms ease-in-out;
}

.card:hover {
  transform: translateY(-10px);
}
  
.card {
  transition: transform 250ms ease;
}

.card:hover {
  transform: translateY(-10px) rotate(-1.5deg);
}
  
*,
::before,
::after {
  box-sizing: border-box;
}

.demo {
  display: grid;
  gap: 16px;
  margin: 30px;
  justify-items: start;
  font-family: ui-sans-serif, system-ui, sans-serif;
}

.card {
  width: 320px;
  padding: 18px 20px;
  border: 2px solid #111;
  border-radius: 14px;
  background: #f6f6f6;
  box-shadow: 0px 10px 0px 0px rgba(0, 0, 0, 1);
}

.card p {
  margin: 8px 0px 0px;
  opacity: 0.8;
}

.hint {
  font-size: 14px;
  opacity: 0.8;
}
  
Hover me

Transitions animate changes between states.

Try switching the snippet tabs, then hover again.

Notice something important: the transition is placed on the base state (.card), not on .card:hover. That way, it transitions smoothly both when entering and leaving hover.

CSS transition longhand properties

The shorthand is convenient, but the longhands help you understand what’s actually happening. A transition is made of four parts:

  • transition-property (what should animate)
  • transition-duration (how long it takes)
  • transition-timing-function (the speed curve)
  • transition-delay (how long to wait before starting)

CSS transition-property

transition-property chooses which CSS properties should animate. You can set it to a specific property (like opacity), multiple properties, or even all.

Using all is tempting, but it can animate things you didn’t intend (and it can be less efficient). When possible, be specific.

.badge {
  transition-property: background-color;
  transition-duration: 300ms;
  transition-timing-function: ease;
}

.wrap:hover .badge {
background-color: #ffd34d;
} 
.badge {
  transition-property: transform;
  transition-duration: 300ms;
  transition-timing-function: ease;
}

.wrap:hover .badge {
  transform: translateX(16px);
}
  
.badge {
  transition-property: transform, background-color, letter-spacing;
  transition-duration: 300ms;
  transition-timing-function: ease;
}

.wrap:hover .badge {
  transform: translateX(16px);
  background-color: #ffd34d;
  letter-spacing: 0.08em;
}
  
*,
::before,
::after {
  box-sizing: border-box;
}

.wrap {
  display: grid;
  gap: 10px;
  width: 360px;
  padding: 18px;
  border: 2px solid #111;
  border-radius: 14px;
  background: #f6f6f6;
  font-family: ui-sans-serif, system-ui, sans-serif;
}

.badge {
  display: inline-block;
  width: fit-content;
  padding: 10px 14px;
  border: 2px solid #111;
  border-radius: 999px;
  background: #e9e9e9;
  box-shadow: 0px 8px 0px 0px rgba(0, 0, 0, 1);
}

.note {
  font-size: 14px;
  opacity: 0.85;
}
  
Hover the card
Switch snippets to change which properties are allowed to transition.

If you ever feel like your transition is “ignoring” a change, check whether that property is included in transition-property.

CSS transition-duration

transition-duration is how long the transition runs. It can be written in seconds (s) or milliseconds (ms).

  • 200ms is usually snappy
  • 400ms feels smoother / more deliberate
  • 800ms starts to feel “dramatic”

Let’s use a slider so you can feel the difference instantly.

.panel {
  transition-property: transform;
  transition-duration: 300ms;
  transition-timing-function: ease;
}

.stage:hover .panel {
  transform: translateX(140px);
}
  
*,
::before,
::after {
  box-sizing: border-box;
}

.panel {
  width: 200px;
  padding: 16px;
  border: 2px solid #111;
  border-radius: 14px;
  background: #f6f6f6;
  box-shadow: 0px 10px 0px 0px rgba(0, 0, 0, 1);
  font-family: ui-sans-serif, system-ui, sans-serif;
  cursor: pointer;
  user-select: none;
}

.stage {
  width: 380px;
  padding: 16px;
  border: 2px dashed #111;
  border-radius: 14px;
  background: #ffffff;
}
  
Hover me

A duration of 0ms means “no transition”. If your transition isn’t working, check that your duration isn’t accidentally 0s.

CSS transition-timing-function

The timing function controls the “speed curve” over time. It answers: does the animation start slow, end slow, or move at a constant speed?

Common built-ins:

  • ease (default-ish feeling: gentle start and end)
  • linear (constant speed)
  • ease-in (slow start, fast end)
  • ease-out (fast start, slow end)
  • ease-in-out (slow start and end)
  • steps(n) (jumps in chunks)
.dot {
  transition: transform 800ms ease;
}

.track:hover .dot {
transform: translateX(260px);
} 
.dot {
  transition: transform 800ms linear;
}

.track:hover .dot {
  transform: translateX(260px);
}
  
.dot {
  transition: transform 800ms ease-in;
}

.track:hover .dot {
  transform: translateX(260px);
}
  
.dot {
  transition: transform 800ms ease-out;
}

.track:hover .dot {
  transform: translateX(260px);
}
  
.dot {
  transition: transform 800ms steps(6);
}

.track:hover .dot {
  transform: translateX(260px);
}
  
*,
::before,
::after {
  box-sizing: border-box;
}

.track {
  width: 340px;
  padding: 16px;
  border: 2px solid #111;
  border-radius: 999px;
  background: #f6f6f6;
  font-family: ui-sans-serif, system-ui, sans-serif;
}

.dot {
  width: 44px;
  height: 44px;
  border: 2px solid #111;
  border-radius: 999px;
  background: #ffd34d;
  box-shadow: 0px 8px 0px 0px rgba(0, 0, 0, 1);
}
  

Cubic bezier timing functions

For custom “feel”, you can use cubic-bezier(x1, y1, x2, y2). This gives you a curve editor vibe, but in math form.

Two helpful tools:

Try these common “personality curves”:

  • Snappy: cubic-bezier(0.2, 0.9, 0.2, 1)
  • Overshoot-ish feel: cubic-bezier(0.34, 1.56, 0.64, 1) (can go beyond 1 for a bounce vibe)
.card {
  transition: transform 700ms cubic-bezier(0.2, 0.9, 0.2, 1);
}

.card:hover {
transform: translateY(-12px);
} 
.card {
  transition: transform 700ms cubic-bezier(0.34, 1.56, 0.64, 1);
}

.card:hover {
  transform: translateY(-12px);
}
  
.card {
  transition: transform 700ms cubic-bezier(0.1, 0.7, 0.1, 0.1);
}

.card:hover {
  transform: translateY(-12px);
}
  
*,
::before,
::after {
  box-sizing: border-box;
}

.card {
  width: 320px;
  padding: 18px 20px;
  margin: 30px;
  border: 2px solid #111;
  border-radius: 14px;
  background: #f6f6f6;
  box-shadow: 0px 10px 0px 0px rgba(0, 0, 0, 1);
  font-family: ui-sans-serif, system-ui, sans-serif;
  cursor: pointer;
  user-select: none;
}

.card p {
  margin: 10px 0px 0px;
  opacity: 0.85;
}
  
Hover me

Different cubic-bezier curves feel wildly different.

The new linear() timing function

CSS now has a more advanced easing option called linear(). Instead of a single cubic curve, linear() lets you define multiple points along the timeline. This is great for “hand-shaped” easing, subtle bounces, or very specific motion art direction.

Helpful tools:

Beginner note: if linear() feels like “too much power” right now, totally normal. You can do a lot with ease and a couple of friendly cubic-bezier() presets.

.ball {
  transition: transform 900ms linear(0, 0.9 60%, 1);
}

.zone:hover .ball {
transform: translateX(260px);
} 
.ball {
  transition: transform 900ms linear(0, 0.85 55%, 1.05 75%, 1);
}

.zone:hover .ball {
  transform: translateX(260px);
}
  
*,
::before,
::after {
  box-sizing: border-box;
}

.zone {
  width: 340px;
  padding: 16px;
  border: 2px solid #111;
  border-radius: 999px;
  background: #f6f6f6;
  font-family: ui-sans-serif, system-ui, sans-serif;
}

.ball {
  width: 44px;
  height: 44px;
  border: 2px solid #111;
  border-radius: 999px;
  background: #ffd34d;
  box-shadow: 0px 8px 0px 0px rgba(0, 0, 0, 1);
}
  

If a browser doesn’t support linear() (check support here), it may ignore that timing function. In real projects, you can provide a fallback by writing a normal timing function first, then overriding it later (supported browsers use the later rule).

CSS transition-delay

transition-delay tells the browser to wait before starting the transition. This is useful for staggered effects or “wait… now go!” interactions.

Let’s control delay with a slider.

.tile {
  transition-property: transform;
  transition-duration: 500ms;
  transition-timing-function: ease;
  transition-delay: 0ms;
}

.tile:hover {
  transform: translateY(-14px);
}
  
*,
::before,
::after {
  box-sizing: border-box;
}

.tile {
  width: 320px;
  padding: 18px 20px;
  margin: 30px;
  border: 2px solid #111;
  border-radius: 14px;
  background: #f6f6f6;
  box-shadow: 0px 10px 0px 0px rgba(0, 0, 0, 1);
  font-family: ui-sans-serif, system-ui, sans-serif;
  cursor: pointer;
  user-select: none;
}

.tile p {
  margin: 10px 0px 0px;
  opacity: 0.85;
}
  
Hover me

Delay can make motion feel intentional (or annoying if overused).

Tip: you can also use a negative delay (like -200ms) in some animation contexts, but for transitions it’s usually simpler to keep delay at 0ms unless you have a clear reason.

CSS transition transform

transform is a superstar for transitions: it’s smooth, performant, and doesn’t force layout recalculations as often as size changes.

Here are a few common transform transitions: translate, scale, and rotate.

.box {
  transition: transform 300ms ease;
}

.box:hover {
transform: translateY(-16px);
} 
.box {
  transition: transform 300ms ease;
}

.box:hover {
  transform: scale(1.08);
}
  
.box {
  transition: transform 300ms ease;
}

.box:hover {
  transform: rotate(6deg);
}
  
.box {
  transition: transform 450ms ease-in-out;
}

.box:hover {
  transform: translateY(-10px) scale(1.06) rotate(-3deg);
}
  
*,
::before,
::after {
  box-sizing: border-box;
}

.box {
  width: 220px;
  margin: 30px;
  aspect-ratio: 1 / 1;
  display: grid;
  place-items: center;
  border: 2px solid #111;
  border-radius: 18px;
  background: #ffd34d;
  box-shadow: 0px 12px 0px 0px rgba(0, 0, 0, 1);
  font-family: ui-sans-serif, system-ui, sans-serif;
  cursor: pointer;
  user-select: none;
}
  
Hover me

Small pro tip: if something looks “jittery” during transform transitions, try avoiding sub-pixel borders or very thin outlines. Also keep your transforms modest. A little motion goes a long way.

CSS transition opacity

Opacity transitions are perfect for fades, tooltips, overlays, and subtle UI feedback.

We’ll use a checkbox toggle so you can turn the state on and off without needing JavaScript.

.toggle:checked ~ .panel {
  opacity: 1;
  transform: translateY(0px);
}

.panel {
opacity: 0;
transform: translateY(10px);
transition: opacity 300ms ease, transform 300ms ease;
} 
.toggle:checked ~ .panel {
  opacity: 1;
}

.panel {
  opacity: 0;
  transition: opacity 900ms ease-in-out;
}
  
*,
::before,
::after {
  box-sizing: border-box;
}

.demo {
  width: 380px;
  padding: 18px;
  border: 2px solid #111;
  border-radius: 14px;
  background: #f6f6f6;
  font-family: ui-sans-serif, system-ui, sans-serif;
}

label {
  display: inline-flex;
  gap: 10px;
  align-items: center;
  cursor: pointer;
  user-select: none;
}

.panel {
  margin-top: 14px;
  padding: 14px;
  border: 2px solid #111;
  border-radius: 12px;
  background: #ffffff;
}

.small {
  font-size: 14px;
  opacity: 0.85;
}
  
Hello!
Opacity transitions are great for fades.

If you only fade with opacity, the element is still “there” and clickable. In real projects, people often combine opacity with pointer-events or visibility. Just note that visibility doesn’t transition smoothly by itself (it flips), so you typically transition opacity and toggle visibility at the right moment.

.popover {
  opacity: 0;
  visibility: hidden;
  transform: translateY(10px);
  transition: opacity 250ms ease, transform 250ms ease, visibility 0ms linear 250ms;
}
.toggle:checked ~ .popover {
  opacity: 1;
  visibility: visible;
  transform: translateY(0px);
  transition: opacity 250ms ease, transform 250ms ease, visibility 250ms linear;
}
.popover {
  opacity: 0;
  pointer-events: none;
  transform: translateY(10px);
  transition:
    opacity 250ms ease,
    transform 250ms ease;
}
.toggle:checked ~ .popover {
  opacity: 1;
  pointer-events: auto;
  transform: translateY(0px);
}
  
*,
::before,
::after {
  box-sizing: border-box;
}

.demo {
  width: 410px;
  padding: 18px;
  border: 2px solid #111;
  border-radius: 14px;
  background: #f6f6f6;
  font-family: ui-sans-serif, system-ui, sans-serif;
}

label {
  display: inline-flex;
  gap: 10px;
  align-items: center;
  cursor: pointer;
  user-select: none;
}

.popover {
  margin-top: 10px;
  width: 340px;
  padding: 14px;
  border: 2px solid #111;
  border-radius: 12px;
  background: #ffd34d;
  box-shadow: 0px 10px 0px 0px rgba(0, 0, 0, 1);
}

.popover p {
  margin: 10px 0px 0px;
  opacity: 0.9;
}

.small {
  margin-top: 10px;
  font-size: 14px;
  opacity: 0.85;
}

kbd {
  padding: 2px 6px;
  border: 1px solid #111;
  border-radius: 6px;
  background: #ffffff;
  font-size: 12px;
}
  
Tip: try clicking where the popover was when it’s hidden.
Popover content

Opacity fades, but visibility flips. Pointer-events prevents “invisible clicks”.

The trick is visibility 0ms linear 250ms so it hides after the fade-out.

Switch snippets:
  • Snippet 1: opacity + visibility
  • Snippet 2: opacity + pointer-events

Why the weird line visibility 0ms linear 250ms in the first snippet? Because we’re not trying to animate visibility. We’re delaying the flip to hidden until the opacity transition finishes.

CSS transition width

Yes, you can transition width. But size changes often trigger layout work, so for big complex layouts, prefer transforms when you can.

This example transitions width on hover.

.bar {
  width: 140px;
  transition: width 400ms ease;
}

.wrap:hover .bar {
width: 320px;
} 
.bar {
  width: 140px;
  transition: width 900ms cubic-bezier(0.2, 0.9, 0.2, 1);
}

.wrap:hover .bar {
  width: 320px;
}
  
*,
::before,
::after {
  box-sizing: border-box;
}

.wrap {
  width: 380px;
  padding: 18px;
  border: 2px solid #111;
  border-radius: 14px;
  background: #f6f6f6;
  font-family: ui-sans-serif, system-ui, sans-serif;
}

.bar {
  height: 46px;
  border: 2px solid #111;
  border-radius: 999px;
  background: #ffd34d;
  box-shadow: 0px 10px 0px 0px rgba(0, 0, 0, 1);
}

.note {
  margin-top: 12px;
  font-size: 14px;
  opacity: 0.85;
}
  
Hover the card to expand the bar width.

CSS transition height

You can transition height too, but there’s a common trap: transitioning from height: 0 to height: auto does not animate smoothly, because auto isn’t a concrete numeric value the browser can “tween”.

Beginner-friendly workaround: animate max-height instead. It’s not mathematically perfect, but it’s practical.

.toggle:checked ~ .content {
  max-height: 220px;
}

.content {
max-height: 0px;
overflow: hidden;
transition: max-height 500ms ease;
} 
.toggle:checked ~ .content {
  max-height: 220px;
  opacity: 1;
}

.content {
  max-height: 0px;
  overflow: hidden;
  opacity: 0;
  transition: max-height 600ms ease, opacity 300ms ease;
}
  
*,
::before,
::after {
  box-sizing: border-box;
}

.accordion {
  width: 410px;
  padding: 18px;
  border: 2px solid #111;
  border-radius: 14px;
  background: #f6f6f6;
  font-family: ui-sans-serif, system-ui, sans-serif;
}

.header {
  display: inline-block;
  margin: 20px 0px;
  padding: 12px 14px;
  border: 2px solid #111;
  border-radius: 12px;
  background: #ffffff;
  cursor: pointer;
  user-select: none;
}

.header span {
  font-size: 14px;
  opacity: 0.8;
}

.content {
  margin-top: 12px;
  padding: 14px;
  border: 2px solid #111;
  border-radius: 12px;
  background: #ffd34d;
}

.content p {
  margin: 10px 0px 0px;
}

.small {
  font-size: 14px;
  opacity: 0.9;
}
  
This is the content area.

Height-to-auto doesn’t transition smoothly, so we animate max-height.

You can also combine it with opacity for a nicer feel.

If you want a “perfect” height animation without guessing a max-height, you usually reach for JavaScript (measure content height and animate to that number), or you use different layout tricks (See this Chrome Dev article on animating width and height to auto, and this Keith J Grant article). But for many beginner UI patterns, max-height is a solid starting point.

CSS transition multiple properties

You can animate multiple properties in a few ways:

  • Use multiple transitions separated by commas
  • Provide comma-separated lists for each longhand
  • Use transition-property: all (easy, but sometimes messy)

The comma-separated shorthand is the most readable for beginners.

.button {
  transition:
    transform 200ms ease,
    background-color 200ms ease,
    letter-spacing 200ms ease;
}

.button:hover {
transform: translateY(-6px);
background-color: #ffd34d;
letter-spacing: 0.08em;
} 
.button {
  transition:
    transform 500ms cubic-bezier(0.2, 0.9, 0.2, 1),
    background-color 200ms ease,
    letter-spacing 350ms ease;
}

.button:hover {
  transform: translateY(-6px);
  background-color: #ffd34d;
  letter-spacing: 0.08em;
}
  
.button {
  transition: all 300ms ease;
}

.button:hover {
  transform: translateY(-6px);
  background-color: #ffd34d;
  letter-spacing: 0.08em;
}
  
*,
::before,
::after {
  box-sizing: border-box;
}

.button {
  display: inline-block;
  padding: 14px 18px;
  border: 2px solid #111;
  border-radius: 12px;
  background: #f6f6f6;
  box-shadow: 0px 10px 0px 0px rgba(0, 0, 0, 1);
  font-family: ui-sans-serif, system-ui, sans-serif;
  cursor: pointer;
  user-select: none;
}

.wrap {
  margin: 30px;
  display: grid;
  gap: 12px;
  justify-items: start;
}
  
Hover me
Switch snippets to compare styles.

Performance note (beginner-friendly version): transitions on transform and opacity are usually smoother than transitions on layout-heavy properties (like width/height). It’s not a strict rule, but it’s a good default instinct.

CSS transition vs animation

Transitions and animations both create motion, but they solve different problems:

  • Transitions animate changes between two states (great for hover, toggles, opening UI, focusing inputs)
  • Animations run through a timeline (keyframes), and can loop forever (great for loaders, attention grabbers, repeating effects)

Here’s the same “move” idea done with a transition (triggered by hover) and an animation (runs continuously).

.ball {
  transition: transform 700ms ease-in-out;
}

.track:hover .ball {
transform: translateX(260px);
} 
.ball {
  animation: slide 700ms ease-in-out infinite alternate;
}

@keyframes slide {
  to {
    transform: translateX(260px);
  }
}
  
*,
::before,
::after {
  box-sizing: border-box;
}

.track {
  width: 340px;
  padding: 16px;
  border: 2px solid #111;
  border-radius: 999px;
  background: #f6f6f6;
  font-family: ui-sans-serif, system-ui, sans-serif;
}

.ball {
  width: 44px;
  height: 44px;
  border: 2px solid #111;
  border-radius: 999px;
  background: #ffd34d;
  box-shadow: 0px 8px 0px 0px rgba(0, 0, 0, 1);
}
  

Rule of thumb: if the motion should happen because the user did something, start with a transition. If the motion should happen on its own timeline, use an animation.

CSS transition not working and debugging

When transitions don’t work, it’s usually one of a few classic causes. Here’s your checklist.

1) Check that a value actually changes

  • You need a start value and an end value.
  • If the value never changes, there’s nothing to animate.

2) Put transition on the base state

  • Put transition on .thing, not only on .thing:hover.
  • Otherwise it may animate in one direction and snap back in the other.

3) Make sure duration is not zero

  • transition-duration: 0s; means “no animation”.
  • Also check you didn’t typo units (like writing 300 without ms).

4) Verify transition-property includes what you’re changing

  • If transition-property is set, it must include the property you’re animating.
  • Example: if you only allow opacity but you change transform, transform will snap.

5) Some properties don’t transition

Not every property is smoothly animatable. A lot of “discrete” properties flip (like display), or can’t interpolate nicely. Common gotchas:

  • display does not transition (it flips)
  • height: auto does not transition smoothly (use max-height or JS)
  • Some color formats and some complex values can behave differently depending on browser support

6) Watch out for specificity and overrides

  • Another rule might be overriding your transition or your end state.
  • Check DevTools “computed styles” to see what’s winning.

7) Hover is not always available

On touch devices, hover behaves differently. If your UI relies on hover to reveal essential controls, consider also using :focus or click/tap patterns.

Practical patterns for real projects

Respect reduced motion

Some users prefer less motion. You can reduce or remove transitions when prefers-reduced-motion is enabled.

.card {
  transition: transform 250ms ease;
}

.card:hover {
transform: translateY(-10px);
}

@media (prefers-reduced-motion: reduce) {
.card {
transition: none;
}
} 
*,
::before,
::after {
  box-sizing: border-box;
}

.card {
  width: 320px;
  padding: 18px 20px;
  margin: 30px;
  border: 2px solid #111;
  border-radius: 14px;
  background: #f6f6f6;
  box-shadow: 0px 10px 0px 0px rgba(0, 0, 0, 1);
  font-family: ui-sans-serif, system-ui, sans-serif;
  cursor: pointer;
  user-select: none;
}

.card p {
  margin: 10px 0px 0px;
  opacity: 0.85;
}
  
Hover me

Reduced motion users may prefer no transitions.

Focus states can transition too

Transitions aren’t just for hover. Use them for :focus and :focus-visible to make keyboard navigation feel polished.

.field {
  transition: transform 180ms ease, box-shadow 180ms ease;
}

.field:focus-visible {
transform: translateY(-4px);
box-shadow: 0px 14px 0px 0px rgba(0, 0, 0, 1);
} 
.field {
  transition: box-shadow 250ms ease;
}

.field:focus-visible {
  box-shadow: 0px 0px 0px 6px rgba(0, 0, 0, 0.25);
}
  
*,
::before,
::after {
  box-sizing: border-box;
}

.field {
  width: 360px;
  padding: 14px 14px;
  margin: 30px;
  border: 2px solid #111;
  border-radius: 14px;
  background: #f6f6f6;
  font-family: ui-sans-serif, system-ui, sans-serif;
  outline: none;
  box-shadow: 0px 10px 0px 0px rgba(0, 0, 0, 1);
}
  

  

Wrap-up CSS transition cheat sheet

  • Use transition to animate changes between states.
  • Prefer transform and opacity for smooth, friendly motion.
  • Put transition on the base selector, not only on the hover selector.
  • Use transition-property to control what animates (avoid accidental all when possible).
  • Timing functions shape the “feel”: ease, linear, cubic-bezier(), and (newer) linear().
  • If a transition isn’t working, check: value change, duration, allowed properties, and whether the property can animate.

CSS Transition Conclusion

CSS transitions are a powerful tool for adding motion to your web projects. By understanding the key properties and common patterns, you can create smooth, engaging interactions that enhance user experience.