1 - CSS-based animations with the AngularTS CSS driver

Write CSS transitions and keyframe animations for AngularTS structural directives using ng-enter, ng-leave, ng-move, and stagger classes.

The CSS animation driver is the default mechanism for animating AngularTS structural directives. When a directive calls $animate.enter(), $animate.leave(), $animate.move(), $animate.addClass(), or $animate.removeClass(), the CSS driver reads the element’s computed transitionDuration and animationDuration after applying the appropriate preparation classes. If it detects a non-zero duration, it manages the full lifecycle: blocking premature transitions, applying active classes after a requestAnimationFrame, listening for transitionend and animationend events, and cleaning up all temporary classes when the animation finishes.

How the CSS driver works

The driver goes through a fixed sequence for every animation:

  1. Preparation classes are added. For a structural event, this means .ng-enter, .ng-leave, or .ng-move. For class-based events, it means .foo-add or .foo-remove.
  2. Transitions are temporarily blocked by applying a large negative transitionDelay inline style. This prevents the browser from computing a transition before the active class is applied.
  3. The driver waits for the next quiet requestAnimationFrame. This forces the browser to flush style calculations, so getComputedStyle() returns accurate timing values.
  4. Active classes are added (e.g., .ng-enter-active). The negative delay is removed at the same time, causing any defined CSS transition to start.
  5. The driver listens for transitionend / animationend. A fallback setTimeout fires at delay + 1.5 * duration to handle browsers that may not fire the event reliably.
  6. Cleanup. All preparation and active classes are removed, inline transition and animation style overrides are reverted, and the AnimateRunner is resolved.

Note: The CSS driver only runs an animation if getComputedStyle() reports a non-zero transitionDuration or animationDuration after the preparation classes are applied. If no duration is detected, the driver skips the animation and immediately resolves the runner.

CSS transitions

The simplest way to animate an element is to define CSS transitions on the preparation classes. The transition must be set on the preparation class (.ng-enter) and the final state on the active class (.ng-enter-active).

.my-element.ng-enter {
  transition: opacity 0.3s ease, transform 0.3s ease;
  opacity: 0;
  transform: translateY(-10px);
}

/* The ending state — applied one rAF later to trigger the transition */
.my-element.ng-enter-active {
  opacity: 1;
  transform: translateY(0);
}

/* Leave animation — reverse the enter */
.my-element.ng-leave {
  transition: opacity 0.3s ease, transform 0.3s ease;
  opacity: 1;
  transform: translateY(0);
}

.my-element.ng-leave-active {
  opacity: 0;
  transform: translateY(10px);
}

CSS keyframe animations

You can also use @keyframes animations. Define the keyframe animation on the preparation class using the animation shorthand property:

  from {
    opacity: 0;
    transform: translateX(-20px);
  }
  to {
    opacity: 1;
    transform: translateX(0);
  }
}

@keyframes slideOut {
  from {
    opacity: 1;
    transform: translateX(0);
  }
  to {
    opacity: 0;
    transform: translateX(20px);
  }
}

.my-element.ng-enter {
  animation: slideIn 0.4s ease forwards;
}

.my-element.ng-leave {
  animation: slideOut 0.4s ease forwards;
}

With keyframe animations the active class (.ng-enter-active) is not required to drive the animation, but you can still use it to override or extend properties applied during the active phase.

Class-based transitions (ng-show / ng-hide)

When ng-show or ng-hide adds or removes the ng-hide class, $animate.addClass() and $animate.removeClass() are called internally. The CSS driver applies .ng-hide-add / .ng-hide-add-active for the hide transition and .ng-hide-remove / .ng-hide-remove-active for the show transition.

.my-panel.ng-hide-add {
  transition: opacity 0.25s ease;
  opacity: 1;
}

.my-panel.ng-hide-add-active {
  opacity: 0;
}

/* Fade in when revealed */
.my-panel.ng-hide-remove {
  transition: opacity 0.25s ease;
  opacity: 0;
}

.my-panel.ng-hide-remove-active {
  opacity: 1;
}

The same pattern applies to any class you add or remove via $animate.addClass() or $animate.removeClass(). If you call $animate.addClass(el, 'highlighted'), the driver applies .highlighted-add and .highlighted-add-active.

Example: animating ng-repeat list items

ng-repeat calls $animate.enter() when a new item is added, $animate.leave() when one is removed, and $animate.move() when the list is reordered.

  <li class="list-item" ng-repeat="item in items track by item.id">
    {{ item.name }}
  </li>
</ul>
.list-item.ng-leave,
.list-item.ng-move {
  transition: all 0.3s ease;
}

.list-item.ng-enter {
  opacity: 0;
  transform: scale(0.9);
}

.list-item.ng-enter-active {
  opacity: 1;
  transform: scale(1);
}

.list-item.ng-leave {
  opacity: 1;
  transform: scale(1);
}

.list-item.ng-leave-active {
  opacity: 0;
  transform: scale(0.9);
}

.list-item.ng-move {
  opacity: 0.5;
}

.list-item.ng-move-active {
  opacity: 1;
}

Example: slide transition with ng-if

ng-if removes and re-inserts the entire element, so you can animate it with ng-enter / ng-leave transitions:

  <!-- drawer content -->
</div>
  overflow: hidden;
}

.drawer.ng-enter {
  transition: max-height 0.35s ease, opacity 0.35s ease;
  max-height: 0;
  opacity: 0;
}

.drawer.ng-enter-active {
  max-height: 500px;
  opacity: 1;
}

.drawer.ng-leave {
  transition: max-height 0.35s ease, opacity 0.35s ease;
  max-height: 500px;
  opacity: 1;
}

.drawer.ng-leave-active {
  max-height: 0;
  opacity: 0;
}

Staggered animations

When multiple elements animate simultaneously under the same parent — common with ng-repeat — the CSS driver detects a stagger class automatically. Define .ng-enter-stagger with transition-delay and zero transition-duration:

  transition: opacity 0.3s ease, transform 0.3s ease;
  opacity: 0;
  transform: translateY(8px);
}

.list-item.ng-enter-active {
  opacity: 1;
  transform: translateY(0);
}

/* Stagger: each subsequent item is delayed by 80ms */
.list-item.ng-enter-stagger {
  transition-delay: 0.08s;
  transition-duration: 0s;
}

The driver reads the transitionDelay from .ng-enter-stagger and multiplies it by the item’s index within the batch. The first item starts immediately; each additional item is offset by the stagger delay.

Enter stagger

.list-item.ng-enter-stagger {
  transition-delay: 0.1s;
  transition-duration: 0s;
}

Leave stagger

.list-item.ng-leave-stagger {
  transition-delay: 0.05s;
  transition-duration: 0s;
}

Move stagger

.list-item.ng-move-stagger {
  transition-delay: 0.08s;
  transition-duration: 0s;
}

Performance tips

CSS animations can be expensive when they trigger layout or paint on every frame. Follow these guidelines to keep animations smooth:

Use transform and opacity

transform and opacity are the only properties that browsers can animate entirely on the GPU compositor thread without triggering layout or paint. Prefer these over width, height, top, left, margin, or padding.

Avoid animating box-model properties

Properties like height, padding, and margin force layout recalculation on every frame. Use transform: scaleY() or max-height tricks as alternatives where possible.

Use will-change sparingly

Adding will-change: transform hints to the browser that the element will be animated, creating a new compositor layer. Use it only on elements you know will animate — overuse increases memory consumption.

Limit simultaneous animations

Use $animateProvider.classNameFilter() or $animateProvider.customFilter() to restrict animations to specific elements. Animating large numbers of DOM nodes simultaneously causes frame drops on low-powered devices.

.my-element.ng-enter {
  transition: opacity 0.3s ease, transform 0.3s ease;
  opacity: 0;
  transform: translateY(-12px);
  will-change: opacity, transform;
}

.my-element.ng-enter-active {
  opacity: 1;
  transform: translateY(0);
}

2 - JavaScript-based animations with the AngularTS JS driver

Register JavaScript animation handlers with $animateProvider, implement enter/leave/move hooks with done callbacks, and use the Web Animations API.

The JavaScript animation driver lets you write fully imperative animations in code. Instead of defining CSS classes, you register a factory function against a CSS class selector. When an element carrying that class goes through a structural or class-based animation event, the driver looks up and invokes the matching handler. This makes the JS driver the right choice when you need precise timing control, want to integrate an animation library such as the Web Animations API, or need to coordinate multiple elements that CSS transitions cannot express.

How the JS driver works

At startup, AnimateJsDriverProvider registers itself with $$animationProvider._drivers. During each animation request, the animation queue calls drivers in reverse registration order — the JS driver is checked before the CSS driver. The driver calls $$animateJs(element, event, classes, options) which inspects the element’s class list against all factories registered via $animateProvider.register(). If a matching factory is found, it retrieves the singleton handler object from the injector and packages the appropriate lifecycle hook as a runnable operation.

The internal $$animateJs service (AnimateJsFn) handles two phases for most events:

  • before* — e.g., beforeAddClass, beforeRemoveClass. Runs synchronously before the DOM change.
  • after* (or the event name itself for enter, move) — runs after the DOM change.

For leave, the hooks are leave (before removal) and afterLeave (after removal). For enter and move, only the after-phase hook is called (named enter / move respectively), because the before-phase does not make sense for elements being inserted.

Note: The JS driver and CSS driver are not mutually exclusive per element, but the animation queue calls invokeFirstDriver() which returns on the first driver that produces a handler. If a JS animation is registered for an element, the CSS driver is skipped for that animation event.

Registering a JS animation

Use $animateProvider.register() during the config phase to associate a factory with a CSS class selector. The selector must begin with .. The factory is an injectable function that returns an object containing lifecycle hook methods.

  $animateProvider.register('.fade', function () {
    return {
      enter: function (element, done) {
        // animate element in, then call done()
        element.style.opacity = '0';
        requestAnimationFrame(function () {
          element.style.transition = 'opacity 0.3s ease';
          element.style.opacity = '1';
          element.addEventListener('transitionend', function onEnd() {
            element.removeEventListener('transitionend', onEnd);
            done();
          });
        });
      },

      leave: function (element, done) {
        element.style.transition = 'opacity 0.3s ease';
        element.style.opacity = '0';
        element.addEventListener('transitionend', function onEnd() {
          element.removeEventListener('transitionend', onEnd);
          done();
        });
      },
    };
  });
}]);

Or use the module-level .animation() shorthand, which calls $animateProvider.register() internally:

  return {
    enter: function (element, done) { /* ... */ done(); },
    leave: function (element, done) { /* ... */ done(); },
  };
});

Animation lifecycle hooks

The object returned by your factory can implement any combination of these hooks. Unimplemented hooks are simply skipped.

HookSignatureWhen it fires
enter(element, done)After element is inserted into the DOM.
leave(element, done)Before element is removed from the DOM.
afterLeave(element, done)After element is removed from the DOM.
move(element, done)After element is moved to a new position.
beforeAddClass(element, className, done)Before the class is added to the element.
addClass(element, className, done)After the class has been added.
beforeRemoveClass(element, className, done)Before the class is removed.
removeClass(element, className, done)After the class has been removed.
beforeSetClass(element, addedClasses, removedClasses, done)Before the atomic add/remove.
setClass(element, addedClasses, removedClasses, done)After the atomic add/remove.
animate(element, from, to, done)For $animate.animate() calls.

The done callback

Every hook receives a done function as its last argument. You must call done() when the animation finishes — whether that is after a transitionend event, a setTimeout, a Web Animations API finish event, or any other mechanism. If done() is never called, the AnimateRunner associated with this animation will never resolve, blocking any chained work.

  // If you return early (e.g., no animation needed), still call done()
  if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
    done();
    return;
  }
  // Otherwise call done() when the animation actually finishes
  runMyAnimation(element).then(done);
}

You may also return a cleanup function from the hook. This function is invoked if the animation is cancelled before it completes:

  const animation = element.animate(
    [{ opacity: 0 }, { opacity: 1 }],
    { duration: 300, easing: 'ease' }
  );

  animation.onfinish = done;

  // Return a cancel handler
  return function (wasCancelled) {
    if (wasCancelled) {
      animation.cancel();
    }
  };
}

Example: Web Animations API

The Web Animations API provides a clean way to drive animations imperatively. It returns a promise-like Animation object with onfinish and oncancel callbacks.

  return {
    enter: function (element, done) {
      const anim = element.animate(
        [
          { opacity: 0, transform: 'scale(0.6) translateY(-8px)' },
          { opacity: 1, transform: 'scale(1) translateY(0)' },
        ],
        {
          duration: 350,
          easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)',
          fill: 'forwards',
        }
      );

      anim.onfinish = done;

      return function (wasCancelled) {
        if (wasCancelled) anim.cancel();
      };
    },

    leave: function (element, done) {
      const anim = element.animate(
        [
          { opacity: 1, transform: 'scale(1) translateY(0)' },
          { opacity: 0, transform: 'scale(0.6) translateY(8px)' },
        ],
        {
          duration: 250,
          easing: 'ease-in',
          fill: 'forwards',
        }
      );

      anim.onfinish = done;

      return function (wasCancelled) {
        if (wasCancelled) anim.cancel();
      };
    },
  };
});

Apply the animation by adding the .pop-in class to any element managed by a structural directive:

  <p>This panel animates in and out with the Web Animations API.</p>
</div>

Example: class-based JS animation

The addClass and removeClass hooks fire when $animate.addClass() or $animate.removeClass() is called — including internally by ng-show and ng-hide. The className argument is the space-separated string of classes being added or removed.

  return {
    addClass: function (element, className, done) {
      if (className === 'active') {
        const anim = element.animate(
          [
            { backgroundColor: 'transparent' },
            { backgroundColor: '#fffbcc' },
          ],
          { duration: 400, fill: 'forwards' }
        );
        anim.onfinish = done;
      } else {
        done();
      }
    },

    removeClass: function (element, className, done) {
      if (className === 'active') {
        const anim = element.animate(
          [
            { backgroundColor: '#fffbcc' },
            { backgroundColor: 'transparent' },
          ],
          { duration: 200, fill: 'forwards' }
        );
        anim.onfinish = done;
      } else {
        done();
      }
    },
  };
});

Example: coordinating multiple elements

When you need to animate related elements in sequence — for example, a leaving element that exits while an entering element waits — you can share state via closure:

  let leaveDeferred = null;

  return {
    leave: function (element, done) {
      leaveDeferred = $q.defer();

      const anim = element.animate(
        [{ opacity: 1, transform: 'translateX(0)' },
         { opacity: 0, transform: 'translateX(-30px)' }],
        { duration: 250, easing: 'ease-in', fill: 'forwards' }
      );

      anim.onfinish = function () {
        leaveDeferred.resolve();
        done();
      };

      return function (wasCancelled) {
        if (wasCancelled) {
          anim.cancel();
          leaveDeferred.resolve();
        }
      };
    },

    enter: function (element, done) {
      const startEnter = function () {
        const anim = element.animate(
          [{ opacity: 0, transform: 'translateX(30px)' },
           { opacity: 1, transform: 'translateX(0)' }],
          { duration: 300, easing: 'ease-out', fill: 'forwards' }
        );
        anim.onfinish = done;
      };

      if (leaveDeferred) {
        leaveDeferred.promise.then(startEnter);
      } else {
        startEnter();
      }
    },
  };
}]);

The $$animateJs service

$$animateJs is the internal function that the JS driver uses to look up and package JS animation handlers. It is available as an injectable service if you need to call it directly — for example, when writing a custom driver or testing animation behavior.

Its signature is:

  element: HTMLElement,
  event: string,
  classes?: string | null,
  options?: AnimationOptions,
): Animator | undefined

It returns an Animator object ({ _willAnimate: true, start(), end() }) if at least one registered handler matches the element’s class list, or undefined if no handler is found. The returned Animator.start() runs the before-phase and after-phase operations, and calls runner.complete(status) when both are done.

Injecting services into animation factories

Animation factory functions participate in dependency injection. List dependencies in the array notation or use $inject:

  return {
    enter: function (element, done) {
      $log.debug('enter animation started');
      // e.g., fetch data to drive animation parameters
      $http.get('/api/animation-config').then(function (response) {
        const duration = response.data.duration || 300;
        element.animate(
          [{ opacity: 0 }, { opacity: 1 }],
          { duration, fill: 'forwards' }
        ).onfinish = done;
      });
    },
  };
}]);

Warning: Long-running asynchronous work inside animation hooks (such as HTTP requests) can make your UI feel sluggish. Prefer pre-fetching animation configuration and caching it rather than fetching it inside a hook on every animation.

Combining JS and CSS animations

If you want the JS driver to apply classes and then let CSS transitions handle the visual animation, you can manipulate classes directly in the hook and detect completion via a transitionend listener. This approach gives you the control of JS hooks with the performance of CSS compositing:

  return {
    enter: function (element, done) {
      element.classList.add('is-entering');

      requestAnimationFrame(function () {
        element.classList.add('is-entering-active');
        element.addEventListener('transitionend', function onEnd(e) {
          if (e.target !== element) return;
          element.removeEventListener('transitionend', onEnd);
          element.classList.remove('is-entering', 'is-entering-active');
          done();
        });
      });
    },
  };
});
  transition: opacity 0.3s ease, transform 0.3s ease;
  opacity: 0;
  transform: scale(0.95);
}

.hybrid.is-entering-active {
  opacity: 1;
  transform: scale(1);
}

Tip: When coordinating JS and CSS this way, always use transform and opacity for the animated properties so the browser can run the transition on the compositor thread without layout recalculation.

3 - AngularTS animations: CSS and JavaScript drivers overview

Overview of the AngularTS animation system — the $animate service, CSS and JS drivers, structural directive hooks, and class-based transitions.

AngularTS ships a first-class animation system built into the core framework. When you use structural directives such as ng-if, ng-repeat, ng-show, ng-hide, ng-include, or ng-view, the framework automatically coordinates with the $animate service to apply CSS class hooks and invoke registered JavaScript animation handlers at the exact moment DOM changes occur — before and after insertion, removal, or class toggling.

How animations are triggered

Animations in AngularTS are not triggered by calling an animation API directly. Instead, they are a side-effect of normal directive activity. When ng-if removes an element, it calls $animate.leave() internally. When ng-repeat inserts a new item, it calls $animate.enter(). This means you never need to change your directive usage — you only need to provide CSS rules or a registered JavaScript animation for the matching class names.

The $animate service sits between directives and the animation drivers. It queues animation work, deduplicates competing animations on the same element, and dispatches to whichever driver is configured. All animation requests are deferred until after the current digest cycle completes, so DOM changes and class mutations are always applied in a stable, predictable order.

The two animation drivers

AngularTS provides two built-in drivers that are consulted in sequence. The JS driver is checked first; if it returns a handler, the CSS driver is skipped for that element. If no JS handler matches, the CSS driver reads the element’s computed styles to detect transitions or keyframe animations.

CSS driver

Reads transitionDuration, animationDuration, and related computed style properties after applying preparation classes. Handles staggering, delays, and both CSS transitions and @keyframes animations with no JavaScript required.

JS driver

Invokes factory functions registered via $animateProvider.register(). Each factory returns an object with lifecycle hooks (enter, leave, move, addClass, removeClass, setClass, animate) that receive a done callback. Suitable for Web Animations API, GSAP, or any imperative animation library.

CSS class hooks

The CSS driver applies a pair of classes for every animation event. The first class (the preparation class) is added immediately; the second class (the active class) is added one requestAnimationFrame later so the browser can compute a transition between the two states. Both classes are removed when the animation completes.

EventPreparation classActive class
enter.ng-enter.ng-enter-active
leave.ng-leave.ng-leave-active
move.ng-move.ng-move-active
addClass foo.foo-add.foo-add-active
removeClass foo.foo-remove.foo-remove-active

During structural animations (enter, leave, move), the element also receives .ng-animate for the full duration of the animation.

For staggered animations — such as list items entering one after another — define a stagger delay class:

.my-list-item.ng-enter-stagger {
  transition-delay: 0.1s;
  transition-duration: 0s;
}

The CSS driver detects .ng-enter-stagger automatically when more than one element is being animated simultaneously under the same parent.

The $animate service API

The $animate service is injectable and provides the full animation API. Every method returns an AnimateRunner that you can use to react to completion or cancel the animation early.

  static $inject = ['$animate', '$element'];

  constructor(
    private $animate: ng.AnimateService,
    private $element: HTMLElement,
  ) {}

  showPanel(panelEl: HTMLElement) {
    // Insert panelEl after this.$element; triggers ng-enter
    this.$animate.enter(panelEl, this.$element.parentElement, this.$element);
  }

  hidePanel(panelEl: HTMLElement) {
    // Remove panelEl after the leave animation completes
    this.$animate.leave(panelEl);
  }

  highlight(el: HTMLElement) {
    // Add 'highlighted' class with an animation
    this.$animate.addClass(el, 'highlighted');
  }

  updateClasses(el: HTMLElement) {
    // Add and remove classes atomically
    this.$animate.setClass(el, 'active', 'inactive');
  }

  morphStyle(el: HTMLElement) {
    // Animate from one set of inline styles to another
    this.$animate.animate(
      el,
      { opacity: 0, transform: 'scale(0.8)' },
      { opacity: 1, transform: 'scale(1)' },
      'my-morph',
    );
  }
}

Full method signatures

MethodDescription
enter(element, parent?, after?, options?)Insert element into the DOM and trigger an enter animation.
leave(element, options?)Trigger a leave animation, then remove the element.
move(element, parent, after?, options?)Move element within the DOM and trigger a move animation.
addClass(element, className, options?)Add one or more CSS classes with an animation.
removeClass(element, className, options?)Remove one or more CSS classes with an animation.
setClass(element, add, remove, options?)Add and remove classes as a single atomic animation.
animate(element, from, to, className?, options?)Animate from one set of inline styles to another.
cancel(runner)Cancel a running animation; the end state is still applied.

Observing animation completion

Every $animate method returns an animation handle. Use done() when code needs to run after an animation settles, or pass onStart, onDone, and onCancel callbacks in native animation options when the callback belongs to one request.

const handle = $animate.enter(panelEl, hostEl);

handle.done((completed) => {
  console.log("Enter animation settled", completed);
});

Filtering animations

Two provider-level hooks let you restrict which elements can be animated. Both are configured during the config phase via $animateProvider.

$animateProvider.classNameFilter(regex) — only animate elements whose class list matches the given regular expression:

  // Only animate elements that have an 'animate-' prefixed class
  $animateProvider.classNameFilter(/\banimate-/);
}]);

$animateProvider.customFilter(fn) — supply an arbitrary predicate that receives (node, event, options) and returns true to allow the animation:

  $animateProvider.customFilter(function (node, event) {
    // Skip all animations when in reduced-motion mode
    return !window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  });
}]);

Tip: Keep both filter functions as lean as possible. They are called for every DOM operation performed by animation-aware directives.

Registering JavaScript animations

To register a JavaScript animation, call $animateProvider.register() during the config phase or use the module-level .animation() shorthand. The name must be a CSS class selector starting with .:

  $animateProvider.register('.fade-animation', ['$q', function ($q) {
    return {
      enter(element, done) {
        // run enter animation, call done() when finished
        done();
      },
      leave(element, done) {
        done();
      },
    };
  }]);
}]);

The registered animation is matched against the element’s class list. If the element has .fade-animation when an enter event fires, the enter hook is invoked.

ng-animate-swap

The ng-animate-swap directive swaps between transcluded blocks as a watched expression changes. The previous element is removed with a leave animation and the new element is inserted with an enter animation, making it simple to animate between different states without manual DOM management.

  <div class="panel">{{ currentView }}</div>
</div>

The directive runs at priority 550, after ng-if (600) but before most others, so it cooperates correctly with other structural directives.

ng-animate-children

By default, when a parent structural animation (enter, leave, or move) is running, child animations on descendants are suppressed. This prevents visual chaos when an entire subtree is animated at once. The ng-animate-children attribute overrides this behavior for a specific container.

<div ng-if="showPanel" ng-animate-children="true">
  <div ng-repeat="item in items" class="list-item">{{ item }}</div>
</div>

Setting ng-animate-children to "on", "true", or an empty string enables child animations. Setting it to any other value (or omitting it) defers to the default suppression behavior.