skip to content

In CSS transitions, what does the `transitionend` event fire for on an element that transitions several properties at once, and name two situations where it never fires at all.

level: middleimportance: should knowfreq 42%

answer

  1. one event per property, not per element
  2. longhand names, not shorthands
  3. it bubbles from descendants
  4. not guaranteed to arrive
  5. a sibling event covers interruption

basics

~20 s

transitionend fires once per transitioned longhand property, not once per element, and it bubbles — so a three-property transition dispatches three events, distinguished by the event's propertyName. It never fires when no transition was generated (zero duration, or no computed-value change) or when the transition is cancelled, which dispatches transitioncancel instead.

solid answer

~50 s

Each transitioned property gets its own event, so `transition: opacity 300ms, translate 300ms, background-color 300ms` ends with three `transitionend` events, and you read `event.propertyName` to tell them apart. The name is always a longhand — a `padding` transition reports `padding-top`, `padding-right` and so on. The event also carries `elapsedTime`, which excludes the delay, and `pseudoElement` when the transition ran on `::before` or `::after`. It bubbles, so a handler on a container receives events from descendants and should check `event.target` before acting. Two cases where it never arrives: no transition was generated at all — zero combined duration, a property not listed in `transition-property`, or a change that did not alter the computed value — and a transition that gets interrupted or whose element is hidden or removed, which fires `transitioncancel` instead. Code that hides a panel only in `transitionend` is exactly what breaks.

go deeper

for a junior

Know that transitionend signals a finished transition and that the event object's propertyName tells you which property it was.

for a middle

Explain that one event fires per longhand property, that it bubbles so event.target needs checking, and that elapsedTime excludes the delay.

for a senior

Demonstrate defensive handling: the event is not guaranteed, so cleanup needs a fallback, transitioncancel handling, or a property filter — and be able to describe the bug when it is missing.

for a principal

Own the pattern across the codebase: a single shared helper for "run this after the transition, or after a timeout", so no team reinvents a cleanup path that can strand elements in the DOM.

## One event per property A transition is not an element-level thing; it is created per property. If a rule says ```css .card { transition: opacity 300ms, translate 300ms, background-color 500ms; } ``` and all three values change, the browser creates three independent transitions, each with its own clock. Three `transitionend` events are dispatched — two at 300ms and one at 500ms. This surprises people who write `el.addEventListener('transitionend', cleanup)` and find `cleanup` running three times. The event object identifies which transition finished: - `propertyName` — the property that ended, always a **longhand**. A `transition: padding 200ms` on an element whose padding changes on all sides yields separate events named `padding-top`, `padding-right`, `padding-bottom`, `padding-left`. - `elapsedTime` — how many seconds the transition actually ran, **excluding** the delay. For a completed 300ms transition it is 0.3 regardless of any delay. - `pseudoElement` — the string `::before` or `::after` when the transition ran on a pseudo-element, and empty otherwise. A pseudo-element's transitions dispatch on the originating element, so this is how you tell them apart. ## It bubbles `transitionend` bubbles up the tree. A handler on a list container receives the end events of every animating descendant. Guard with `if (event.target !== el) return;` when you only care about the element itself — otherwise a child's hover transition silently triggers your close logic. ## The full event family Four events exist, in this order for a transition that runs to completion: 1. `transitionrun` — the transition has been created; fires **before** the delay elapses. 2. `transitionstart` — the interpolation is actually beginning, after the delay. 3. `transitionend` — it finished normally. And instead of the third, when things go wrong: 4. `transitioncancel` — the transition stopped before completing. The `run`/`start` split is what lets you distinguish "a transition is pending" from "it is moving", which matters when delays are long. ## Case one: no transition is generated Nothing fires because nothing was created. This happens when: - the combined duration is zero — the default `transition-duration` is `0s`, so a rule with easing but no time does nothing; - the property is not covered by `transition-property`, or `transition-property: none` is in effect; - the computed value did not actually change — reapplying `color: #333` to an element that already computes to `#333` is not a change; - the two values are not interpolable, such as a length to `auto`; - the element is being rendered for the first time, so there is no before-change value to start from. ## Case two: the transition is cancelled A running transition is cancelled when its element becomes `display: none`, is removed from the document, or when the property stops being transitionable — and in these cases `transitioncancel` fires and `transitionend` does not. Interruption by a *new* value behaves differently: the running transition is replaced by a fresh one that starts from the current interpolated value, so you get a later `transitionend` for the new transition, not for the one you were waiting on. ## Why this matters in practice The dangerous pattern is making cleanup conditional on the event: ```javascript panel.classList.remove('open'); panel.addEventListener('transitionend', () => panel.remove(), { once: true }); ``` If a stylesheet change drops the duration to zero, if a user setting removes motion, or if the element is hidden mid-flight, the event never arrives and the panel stays in the DOM forever. Robust code either treats the event as an optimisation with a timeout fallback, listens for `transitioncancel` as well, or filters on `event.propertyName` so only the property it cares about triggers the work. ## What interviewers are listening for That the event is per-property and reports longhands; that it bubbles; that `elapsedTime` excludes the delay; and above all that it is **not guaranteed** to fire, so it must never be the only path to a state change. Candidates who have shipped animated UI mention the cancellation case unprompted.

  • How do you make a handler run only when one specific property has finished?
    Check `event.propertyName` against the longhand you care about, and check `event.target` because the event bubbles. For a shorthand like `padding` you must compare against a specific side such as `padding-top`, since the shorthand name itself never appears in the event.
  • What does `elapsedTime` report for a transition with a 200ms delay and a 500ms duration that ran to completion?
    0.5 — the value is in seconds and excludes the delay entirely. It reports how long the interpolation itself ran, which is why an interrupted transition reports less than the full duration on `transitioncancel`.
  • If a transition is interrupted halfway by a new target value, which events fire?
    The running transition is replaced rather than cancelled outright: a new transition is created from the current interpolated value toward the new target, and you get `transitionend` for that new one when it completes. `transitioncancel` is reserved for cases where the transition stops without being replaced, such as the element becoming `display: none` or leaving the document.

saying these in an interview costs you the question

  • Assumes one transitionend per element regardless of property count
  • Treats the event as guaranteed and hangs cleanup on it
  • Expects the shorthand name in propertyName
  • Forgets the event bubbles from child elements
  • Thinks elapsedTime includes the transition delay

context