165

I want a div to slide in from the right in angular 2 using css.

  <div class="note" [ngClass]="{'transition':show}" *ngIf="show">
    <p> Notes</p>
  </div>
  <button class="btn btn-default" (click)="toggle(show)">Toggle</button>

I works fine if i only use [ngClass] to toggle class and utilise opacity. But li don't want that element to be rendered from the beginning so I "hide" it with ngIf first, but then the transition wont't work.

.transition{
  -webkit-transition: opacity 1000ms ease-in-out,margin-left 500ms ease-in-out;
  -moz-transition: opacity 1000ms ease-in-out,margin-left 500ms ease-in-out;
  -ms-transition: opacity 1000ms ease-in-out,margin-left 500ms ease-in-out ;
  -o-transition: opacity 1000ms ease-in-out,margin-left 500ms ease-in-out;
  transition: opacity 1000ms ease-in-out,margin-left 500ms ease-in-out;
  margin-left: 1500px;
  width: 200px;
  opacity: 0;
}

.transition{
  opacity: 100;
  margin-left: 0;
}

8 Answers 8

241

update 4.1.0

Plunker

See also https://github.com/angular/angular/blob/master/CHANGELOG.md#400-rc1-2017-02-24

update 2.1.0

Plunker

For more details see Animations at angular.io

import { trigger, style, animate, transition } from '@angular/animations';

@Component({
  selector: 'my-app',
  animations: [
    trigger(
      'enterAnimation', [
        transition(':enter', [
          style({transform: 'translateX(100%)', opacity: 0}),
          animate('500ms', style({transform: 'translateX(0)', opacity: 1}))
        ]),
        transition(':leave', [
          style({transform: 'translateX(0)', opacity: 1}),
          animate('500ms', style({transform: 'translateX(100%)', opacity: 0}))
        ])
      ]
    )
  ],
  template: `
    <button (click)="show = !show">toggle show ({{show}})</button>

    <div *ngIf="show" [@enterAnimation]>xxx</div>
  `
})
export class App {
  show:boolean = false;
}

original

*ngIf removes the element from the DOM when the expression becomes false. You can't have a transition on a non-existing element.

Use instead hidden:

<div class="note" [ngClass]="{'transition':show}" [hidden]="!show">
Sign up to request clarification or add additional context in comments.

22 Comments

Yes, hidden only makes it invisible but the element still exists. *ngIf removes it entirely from the DOM.
It's like display:none. There is no display:hidden AFAIK.
@GünterZöchbauer yes, opacity is Hardware accellerated so it will suit better.
Nevermind. opacity will not remove the element and will still cover the underneath elements, i suggest to use scale(0) wich will affect the UI such as display:none; but with a nice transition. To answer the OP he can use the angular animations angular.io/docs/ts/latest/guide/animations.html with transform:scale(0) at the void state
The trigger, style, animate and transition items should now be included from @angular/animations . So import { trigger, style, animate, transition } from '@angular/animations';
|
188

According to the latest angular 2 documentation you can animate "Entering and Leaving" elements (like in angular 1).

Example of simple fade animation:

In relevant @Component add:

animations: [
  trigger('fadeInOut', [
    transition(':enter', [   // :enter is alias to 'void => *'
      style({opacity:0}),
      animate(500, style({opacity:1})) 
    ]),
    transition(':leave', [   // :leave is alias to '* => void'
      animate(500, style({opacity:0})) 
    ])
  ])
]

Do not forget to add imports

import {style, state, animate, transition, trigger} from '@angular/animations';

The relevant component's html's element should look like:

<div *ngIf="toggle" [@fadeInOut]>element</div>

I built example of slide and fade animation here.

Explanation on 'void' and '*':

  • void is the state when ngIf is set to false (it applies when the element is not attached to a view).
  • * - There can be many animation states (read more in docs). The * state takes precedence over all of them as a "wildcard" (in my example this is the state when ngIf is set to true).

Notice (taken from angular docs):

Extra declare inside the app module, import { BrowserAnimationsModule } from '@angular/platform-browser/animations';

Angular animations are built on top of the standard Web Animations API and run natively on browsers that support it. For other browsers, a polyfill is required. Grab web-animations.min.js from GitHub and add it to your page.

3 Comments

Need to import the BrowserAnimationsModule to use angular animations. If I'm not wrong, the animation module was found in the core module of angular 2 before being moved to its own module, hence why you find many plunker examples without the import. Here's an updated plnkr with the import: Link
When using such approach the leave animation is not taking place , because component is removed from the DOM by *ngIf prior to that.
This should be the accepted answer, It actually gives the solution to those who want to use ngIf and not other workarounds.
21
    trigger('slideIn', [
      state('*', style({ 'overflow-y': 'hidden' })),
      state('void', style({ 'overflow-y': 'hidden' })),
      transition('* => void', [
        style({ height: '*' }),
        animate(250, style({ height: 0 }))
      ]),
      transition('void => *', [
        style({ height: '0' }),
        animate(250, style({ height: '*' }))
      ])
    ])

Comments

18

CSS only solution for modern browsers

@keyframes slidein {
    0%   {margin-left:1500px;}
    100% {margin-left:0px;}
}
.note {
    animation-name: slidein;
    animation-duration: .9s;
    display: block;
}

1 Comment

Good alternative for enter CSS-only transition. Used this as a temporal solution for migrating from ng-enter class usage.
5

One way is to use a setter for the ngIf property and set the state as part of updating the value.

StackBlitz example

fade.component.ts

 import {
    animate,
    AnimationEvent,
    state,
    style,
    transition,
    trigger
  } from '@angular/animations';
  import { ChangeDetectionStrategy, Component, Input } from '@angular/core';

  export type FadeState = 'visible' | 'hidden';

  @Component({
    selector: 'app-fade',
    templateUrl: './fade.component.html',
    styleUrls: ['./fade.component.scss'],
    animations: [
      trigger('state', [
        state(
          'visible',
          style({
            opacity: '1'
          })
        ),
        state(
          'hidden',
          style({
            opacity: '0'
          })
        ),
        transition('* => visible', [animate('500ms ease-out')]),
        transition('visible => hidden', [animate('500ms ease-out')])
      ])
    ],
    changeDetection: ChangeDetectionStrategy.OnPush
  })
  export class FadeComponent {
    state: FadeState;
    // tslint:disable-next-line: variable-name
    private _show: boolean;
    get show() {
      return this._show;
    }
    @Input()
    set show(value: boolean) {
      if (value) {
        this._show = value;
        this.state = 'visible';
      } else {
        this.state = 'hidden';
      }
    }

    animationDone(event: AnimationEvent) {
      if (event.fromState === 'visible' && event.toState === 'hidden') {
        this._show = false;
      }
    }
  }

fade.component.html

 <div
    *ngIf="show"
    class="fade"
    [@state]="state"
    (@state.done)="animationDone($event)"
  >
    <button mat-raised-button color="primary">test</button>
  </div>

example.component.css

:host {
  display: block;
}
.fade {
  opacity: 0;
}

Comments

3

Am using angular 5 and for an ngif to work for me that is in a ngfor, I had to use animateChild and in the user-detail component I used the *ngIf="user.expanded" to show hide user and it worked for entering a leaving

 <div *ngFor="let user of users" @flyInParent>
  <ly-user-detail [user]= "user" @flyIn></user-detail>
</div>

//the animation file


export const FLIP_TRANSITION = [ 
trigger('flyInParent', [
    transition(':enter, :leave', [
      query('@*', animateChild())
    ])
  ]),
  trigger('flyIn', [
    state('void', style({width: '100%', height: '100%'})),
    state('*', style({width: '100%', height: '100%'})),
    transition(':enter', [
      style({
        transform: 'translateY(100%)',
        position: 'fixed'
      }),
      animate('0.5s cubic-bezier(0.35, 0, 0.25, 1)', style({transform: 'translateY(0%)'}))
    ]),
    transition(':leave', [
      style({
        transform: 'translateY(0%)',
        position: 'fixed'
      }),
      animate('0.5s cubic-bezier(0.35, 0, 0.25, 1)', style({transform: 'translateY(100%)'}))
    ])
  ])
];

Comments

1

In my case I declared the animation on the wrong component by mistake.

app.component.html

  <app-order-details *ngIf="orderDetails" [@fadeInOut] [orderDetails]="orderDetails">
  </app-order-details>

The animation needs to be declared on the component where the element is used in (appComponent.ts). I was declaring the animation on OrderDetailsComponent.ts instead.

Hopefully it will help someone making the same mistake

Comments

0

--- Update (Angular v20.2.0): ---

First of all, use of @angular/animations is discouraged by angular team, thankfully we now have the tools required to do it without the animation package.

TL;DR

Angular animation api allows for dynamically assigning classes to an element on enter on leave - no imports required.

HTML:

@if(isFooOpen()){
    <p
      class="foo-element"
      animate.enter="enter-transition"
      animate.leave="leave-transition"
    >
      Quo natus ab veritatis!
    </p>
}

CSS:

.foo-element {
  transition: transform 0.5s ease-in-out;
}

.enter-transition {
  transform: translateX(0);

  @starting-style {
    transform: translateX(100%);
  }
}

.leave-transition {
  transform: translateX(100%);
}

If you want to play around here is stack blitz example.

Since its a normal attribute binding, you can even do this from inside the component using host binding:

@Component({
  ...
  host: {
    'animate.enter': 'enter-transition',
    'animate.leave': 'leave-transition',
  },
})
...

Stack blitz example.


--- Original answer (Angular v17.0.0): ---

Here are my two go to solutions:

Case 1: Light elements (e.g., tooltips)

Solution for elements I don´t mind just hiding instead of destroying.

CSS now supports transitioning and animating display: none. For this to work you have to indicate that one of properties you want to apply transition to is display and set transition-behavior: allow-discrete

<button (click)="toggleOpenFoo()">Toggle Component Display</button>

<app-foo class="app-foo" [ngClass]="{'app-foo--open': isFooOpen}" />
.app-foo {
    display: none;
    position: relative;
    opacity: 0;
    left: 5rem;

    transition-property: display opacity;
    transition-duration: 0.2s;
    transition-behavior: allow-discrete;
  }

  .app-foo--open {
    display: inline;
    opacity: 1;
    left: 0;

    @starting-style {
      opacity: 0;
      left: 5rem;
    }
  }

Here is a stack blitz example. Here is a great explanation video.

Case 2: Heavy elements (e.g., maps) or lifecycle-bound components

Solution for elements that should not exist while not in use.

For this one we will have to somehow animate *ngIf / @if. Animating an element inside template conditional on appearance is no problem, the issue comes when it’s destroyed. CSS has no way of knowing the element is about to be removed, so element ends up just popping out of existence. To fix this, we have to give CSS time to react. For that we can use a helper variable and some flavour of timeout, like setTimeout or rxjs timer.

To solve this case we will build upon case 1.

The trick is to have one normal state, like isOpen, and another debounced state that updates only when the animation should finish. First variable you pass to css so it knows when animation should start. And the other one you pass to @if so it reacts with delay.

Please note that delay is only required during destruction, appearance animation should work fine as is.

Here is a simplified example:

<button (click)="toggleOpenFoo()">Toggle Component Display</button>

@if(isFooOpenDebounced){
    <app-foo class="app-foo" [ngClass]="{'app-foo--open': isFooOpen}" />
}
export class App {
  isFooOpen = true;
  isFooOpenDebounced = this.isFooOpen;

  toggleOpenFoo() {
    this.isFooOpen = !this.isFooOpen;
    setTimeout(
      () => (this.isFooOpenDebounced = this.isFooOpen),
      // Animation on appearance works just fine as it is, we need to add delay only on destruction
      this.isFooOpen ? 0 : 1000
    );
  }
}

Here are two stack blitz examples, a simple one and a more advanced one.

A logical next step would be to create a custom pipe that would debounce a value for you regardless of where it comes from. This way you can hide the helper variable and other implementation logic.


In case you are interested in animating router transition, you can check this response.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.