---
title: Animating Modals in Angular.js
teaser: The end result is a beautiful transition and zero conditionals.
tags: design,web,javascript
author: Sage Griffin
published_on: 2013-12-17
---

Modal dialogs are popular in rich client-side applications. When you have them,
it's nice to have transitions between them. We could even take it a step
further, and have separate transitions for opening/closing the modal, and
another transition when we switch between them.

## Fancy Directives

This is surprisingly simple to accomplish with [Angular][angular], using a
handful of directives, and a service to encapsulate what we're currently
displaying. We'll start by creating a "modal view" directive, with two options.
One will control whether or not we're displaying the modal, and another will
control what `templateUrl` to display.

    modalView = ->
      restrict: 'E'

      scope:
        _modalShow: '&modalShow'
        _modalContent: '&modalSrc'

      templateUrl: '/templates/modal_view.html'

    app.directive('modalView', modalView)

Our template would look like this:

    .modal-backdrop(ng-if="_modalShow()")
    .modal-container(ng-if="_modalShow()")
      .modal-window
        .modal-dialog
          .modal-content(ng-include="_modalContent()")

We make use of two directives that Angular provides. [ngIf][ng-if] will remove the element from the DOM if its expression evaluates to false. [ngInclude][ng-include] replaces its contents with the template at whatever <abbr title="Uniform Resource Locator">URL</abbr> its expression evaluates to.

Angular directives are small reusable snippets of <abbr title="HyperText Markup Language">HTML</abbr> with some logic attached to them. Often times they simply exist to compose the built-in directives provided by Angular, which are incredibly powerful. Combined with Angular's bindings, they allow us to manipulate the DOM in large ways simply by changing a variables.

Next, we need a service to encapsulate what we're displaying, and a controller to expose that service to our application.

    class Modal
      isOpen: => !!@src

      open: (src) => @src = src

      close: => @src = ''

    app.service('Modal', Modal)

    class ModalCtrl
      constructor: ($scope, Modal) ->
        $scope.modal = Modal

    app.controller('ModalCtrl', ['$scope', 'Modal', ModalCtrl])

Both of these classes are incredibly simple. Our modal service exists to
allow us to share one src variable across the application. Now in our layout,
we would add the following.

    %div(ng-controller="ModalCtrl")
      %modal-view(modal-show="modal.isOpen()" modal-src="modal.src")

Now we can change the modal's content by calling `Modal.open('/whatever.html')`.
If the modal isn't already open, it'll place it on the DOM. If it is open, it'll
change the content. Because these happen on different elements, we can apply
separate animations depending on which element enters the page.

## I'm basically a designer now

Adding the animations requires the [ngAnimate][ng-animate] module. Once we've
added that to our dependencies, we just need to use a little <abbr
title="Cascading Style Sheets">CSS</abbr> to make the animations happen. In this
example, we're using [Sass][sass] and [Bourbon][bourbon] to eliminate vendor
prefixes, and structure things nicely. First, let's fade in the backdrop.

    @mixin fade($opacity) {
      opacity: $opacity;

      &.ng-enter, &.ng-leave.ng-leave-active {
        opacity: 0;
      }
    }

    .modal-backdrop {
      @include fade(0.5);
      @include transition(0.15s linear all);
    }

Next, we'll both fade and slide the modal container.

    @mixin slide($x, $y) {
      @include transform(translate(0, 0));

      &.ng-enter, &.ng-leave.ng-leave-active {
        @include transform(translate($x, $y));
      }
    }

    .modal-container {
      @include fade(1);
      @include slide(0, -25%);
      @include transition(0.3s ease-out all);
    }

Finally, we'll define a flip animation for changes in the content. This one is
a bit more complex than the others.

    @mixin flip-hidden {
      @include transform(rotateY(190deg) scale(1));
    }

    @mixin flip($time) {
      @include backface-visibility(hidden);

      &.ng-enter {
        @include animation($time flipIn);
        @include flip-hidden;
      }

      &.ng-leave {
        @include animation($time flipOut);

        &.ng-leave-active {
          @include flip-hidden;
        }
      }
    }

    @include keyframes(flipOut) {
      0% {
        @include transform(rotateY(0) scale(1));
        @include animation-timing-function(ease-out);
      }

      80% {
        @include transform(rotateY(170deg) scale(1));
        @include animation-timing-function(ease-out);
      }

      100% {
        @include flip-hidden;
        @include animation-timing-function(ease-in);
      }
    }

    @include keyframes(flipIn) {
      0% {
        @include flip-hidden;
        @include animation-timing-function(ease-in);
      }

      60% {
        @include transform(rotateY(360deg) scale(0.95));
        @include animation-timing-function(ease-in);
      }

      100% {
        @include transform(rotateY(360deg) scale(1));
        @include animation-timing-function(ease-in);
      }
    }

    .modal-dialog {
      @include perspective(1000);
      @include transform-style(preserve-3d);
    }

    .modal-content {
      @include flip(0.6s);
    }

We'll skip the styles that actually make the modals into modals. It should be
noted that the `.modal-content` needs to be `position: absolute`, or they will
pop down to the bottom of the screen when the flip occurs (both the old modal
and the new modal will be on the DOM at the same time)

And that's all there is to it. The end result is a beautiful transition, and 0
conditionals in our code to determine which one occurs.

![modal transitions](https://images.thoughtbot.com/angular-modals/transitions.gif)

[angular]: http://angularjs.org/ "Angular.js"
[ng-if]: http://docs.angularjs.org/api/ng.directive:ngIf "API Docs for ngIf"
[ng-include]: http://docs.angularjs.org/api/ng.directive:ngInclude "API Docs for ngInclude"
[ng-animate]: http://docs.angularjs.org/api/ngAnimate "API Docs for ngAnimate"
[sass]: http://sass-lang.com/
[bourbon]: http://bourbon.io/
