CSS Transitions

This section documents general HTML5 and CSS concepts — it is not tied to any specific framework or library. This content was generated with the assistance of AI. Verify it against current MDN documentation and browser-support tables (caniuse.com) before relying on it in production, since HTML/CSS features and browser support continue to evolve.

A CSS transition lets an element change smoothly from one style to another instead of jumping instantly between the two. This page covers the transition shorthand and its longhand properties, the strict order the shorthand expects its values in, the newer transition-behavior: allow-discrete property for animating otherwise non-animatable ("discrete") properties such as display, and the @starting-style rule that lets a transition run on an element’s very first render — including elements moving out of display: none. Complex, self-running animations built with @keyframes are covered separately in CSS Animations, Keyframes & Performance; position and z-index, which the source material for this page also touches on, are covered in CSS Positioning.

The transition property

A transition is triggered whenever a CSS property’s computed value changes on an element — most commonly because a pseudo-class like :hover, :focus, or :checked starts or stops applying, or because a class is toggled with JavaScript. Instead of describing the transition on the changed state, it is declared on the element’s original (resting) state:

.target {
  font-size: 14px;
  transition: font-size 4s 1s;
}

.target:hover {
  font-size: 36px;
}

A few things to note about this example:

  • transition is declared on .target, the element’s original rule — not on .target:hover, the state it transitions to.

  • The first time value after the property name (4s) is the duration; if a second time value follows (1s), it is the delay before the transition starts.

  • If more than one CSS property changes on the same state change, and no property name is given in transition, every animatable property that changed is transitioned:

.target {
  font-size: 14px;
  color: red;
  transition: 4s ease-in-out 1s;
}

.target:hover {
  font-size: 36px;
  color: blue;
}

Here both font-size and color transition together, using the same 4s duration, ease-in-out timing function, and 1s delay, because no specific transition-property was named.

Only transition the properties that actually change. Leaving transition (or transition-property) at its default of all forces the browser to watch every property of the element for changes, which is wasted work and can measurably slow down an application with many animated elements.

Transition properties

The following table lists every longhand that makes up the transition shorthand:

Property Type of value Description

transition (shorthand)

set of values

Combines all the properties below into a single declaration.

transition-property

CSS property name (or all, none)

The name of the CSS property the transition effect applies to.

transition-duration

seconds or milliseconds

How long the transition effect takes to complete.

transition-timing-function

ease | ease-in | ease-out | ease-in-out | linear | step-start | step-end | cubic-bezier(n,n,n,n)

The speed curve of the transition effect.

transition-delay

seconds or milliseconds

How long to wait, after the value change occurs, before the transition effect begins.

transition-behavior

normal | allow-discrete

Whether the transition should also run for properties whose animation behavior is discrete (see transition-behavior and allow-discrete below).

transition-timing-function accepts either one of the predefined keyword curves or a custom cubic-bezier(n,n,n,n) function. ease-in-out, used throughout this page’s examples, starts slowly, accelerates through the middle of the transition, and decelerates again toward the end, which tends to read as the most natural-feeling curve for UI state changes.

The transition shorthand and property order

The transition shorthand always expects its values in the same order: property name, then duration, then timing function, then delay, then behavior. Any value that is omitted falls back to its default (all duration 0s, ease timing, 0s delay, normal behavior):

transition: opacity 2s ease-in 0.5s normal;
/*           ^property ^duration ^timing  ^delay ^behavior */

Because the parser tells the values apart by type (a property name, two time values, a keyword/function, another keyword) rather than by position alone, the duration and delay are distinguished only by which time value comes first: the first time value found is always the duration, and a second one, if present, is the delay — 

.menu li {
  /* duration 0.5s, default (ease) timing, no delay */
  transition: background-color 0.5s;
}

.menu li a {
  /* duration 0.5s, default timing, delay 0.1s */
  transition: font-weight 0.5s 0.1s;
}

The shorthand also accepts a comma-separated list to transition several properties independently, each with its own duration/timing/delay:

ul li {
  transition:
    background-color 0.5s ease-in-out,
    opacity 0.5s ease-in-out,
    transform 0.7s ease-in-out;
}

Per-item delays are commonly layered on top with :nth-child() to stagger a group of elements into a cascading entrance, rather than having every item animate in unison:

ul li:nth-child(1) { transition-delay: 0.1s; }
ul li:nth-child(2) { transition-delay: 0.2s; }
ul li:nth-child(3) { transition-delay: 0.3s; }
When a longhand like transition-delay or transition-behavior is set after the transition shorthand in the same rule (or in a rule with equal specificity that comes later in the cascade), it wins over the value the shorthand set for that same longhand — ordinary cascade/specificity rules apply. Keep longhand overrides such as transition-behavior: allow-discrete at the end of the block to avoid accidentally being reset by a transition shorthand declared afterward.

transition-behavior and allow-discrete

Most CSS properties are naturally animatable along a continuous range (numbers, lengths, colors). A handful of properties, though, only ever have one of a fixed set of values with nothing meaningful in between — display: none vs display: block, or visibility: hidden vs visibility: visible. The browser calls this a discrete animation: instead of interpolating, it flips from the old value to the new one at a single point in the timeline (by default, at the very end of the transition).

By default, transition ignores discrete properties entirely. Setting transition-behavior: allow-discrete (directly, or as the final value in the transition shorthand) tells the browser to include them:

.modal {
  opacity: 0;
  display: none;
  transition:
    opacity 0.3s ease-in-out,
    display 0.3s allow-discrete;
}

.modal.is-open {
  opacity: 1;
  display: block;
}

Here display flips from none to block at the start of the transition when the discrete value is "growing more visible" (so the element is present in the layout while opacity fades in), and flips back to none only at the end of the transition when closing — avoiding the usual problem where a display: none element can’t visually transition at all because it’s removed from rendering immediately.

transition-behavior is a relatively new addition to CSS and does not work in older browsers by default; check current support on caniuse.com before relying on it for a required effect, and make sure the UI still degrades acceptably (e.g. the modal simply appears/disappears without the fade) where it is unsupported.

The @starting-style rule

Even when a property is allowed to transition, CSS transitions still do not run in two situations by default:

  • On an element’s very first style update after it is added to the DOM (there is no "previous" value to transition from).

  • When a property changes away from display: none (the element wasn’t being rendered a moment ago, so there is nothing to animate from).

The @starting-style at-rule solves this by giving the browser an explicit "before" state to transition from, for values that otherwise have no prior state. It can be written two ways.

Standalone form

@starting-style {
  selector {
    property: value;
  }
}

Nested form

selector {
  property: value;

  @starting-style {
    property: starting-value;
  }
}

Both forms are equivalent; the nested form simply keeps the starting values next to the rule they belong to.

@starting-style is especially useful for entry/exit transitions on elements that pop to the front of the page (popovers, modal dialogs), on elements moving to or from display: none, and on elements freshly added to or removed from the DOM.

Example: staggered menu entrance

The following combines @starting-style with per-item transition-delay to slide a list of menu items in from the left when the page loads, each one slightly after the previous:

@starting-style {
  ul li {
    opacity: 0;
    transform: translateX(-280px);
  }
}

ul li {
  opacity: 1;
  transform: translateX(0);
  transition:
    opacity 0.5s ease-in-out,
    transform 0.7s ease-in-out;
}

ul li:nth-child(1) { transition-delay: 0.1s; }
ul li:nth-child(2) { transition-delay: 0.2s; }
ul li:nth-child(3) { transition-delay: 0.3s; }

The @starting-style block supplies the "before" values (invisible, shifted left); the plain ul li rule is the resting "after" state the element transitions to as soon as it is first rendered, following the transition declared alongside it. Setting transform: translateX(0) simply restores the element to its normal, undisplaced position.

Example: transitioning out of display: none

Combining @starting-style with transition-behavior: allow-discrete lets a hidden element both appear and animate in on the very first frame it becomes visible:

.tooltip {
  opacity: 0;
  display: none;
  transition:
    opacity 0.3s ease-in-out,
    display 0.3s allow-discrete;

  @starting-style {
    opacity: 0;
  }
}

.tooltip.is-visible {
  opacity: 1;
  display: block;
}

Without the @starting-style block, the very first time .tooltip.is-visible is applied there is no prior opacity value to transition from, so the tooltip would simply snap to opacity: 1 instead of fading in.

@starting-style only affects CSS transitions — it has no effect on @keyframes-driven CSS animations, which already define their own starting state via a 0%/from keyframe (see CSS Animations, Keyframes & Performance). As with transition-behavior, check current browser support before depending on it for required behavior.