Enter and leave transitions

LynxTransition plays one CSS animation when content appears and another when it disappears. It keeps the content on screen until the leave animation ends. LynxTransitionGroup does the same for each item in a list.

Animate content in and out

src/app/fade-panel.ts
import { Component, signal } from '@angular/core';
import { LYNX_ELEMENTS, LynxTransition } from '@blotch/angular-lynx'; 

@Component({
  selector: 'app-fade-panel',
  imports: [LYNX_ELEMENTS, LynxTransition],
  template: `
    <view (bindtap)="toggle()">
      <text>{{ visible() ? 'Hide' : 'Show' }}</text>
    </view>

    <lynx-transition [show]="visible()" name="fade" [duration]="300">
      <view class="panel">
        <text>Hello!</text>
      </view>
    </lynx-transition>
  `,
  styles: `
    @keyframes fade-enter {
      from {
        opacity: 0;
        transform: translateY(-16px);
      }
      to {
        opacity: 1;
        transform: translateY(0);
      }
    }
    @keyframes fade-leave {
      from {
        opacity: 1;
        transform: translateY(0);
      }
      to {
        opacity: 0;
        transform: translateY(-16px);
      }
    }
    .fade-enter {
      animation: fade-enter 300ms ease both;
    }
    .fade-leave {
      animation: fade-leave 300ms ease both;
    }
  `,
})
export class FadePanel {
  readonly visible = signal(false);

  toggle(): void {
    this.visible.update((v) => !v);
  }
}
  • [show] controls whether the content is rendered.
  • When show turns true, AngularLynx renders the content and adds the fade-enter class.
  • When show turns false, it swaps fade-enter for fade-leave, waits duration milliseconds, then removes the content.
  • The classes go on the <lynx-transition> element, which wraps your content. Write the styles in the component that uses it.
  • No animation on the first render. If show starts as true, the content appears without playing fade-enter.

The class names

name sets the prefix of the two classes. With name="fade":

ClassAdded whenRemoved when
fade-entershow becomes trueshow becomes false
fade-leaveshow becomes falseshow becomes true again
  • The default name is v, which gives v-enter and v-leave.
  • Give each class a @keyframes animation, not a transition.
  • Use both in the animation shorthand (animation-fill-mode: both). The element then holds the first frame before the animation starts and the last frame after it ends. fade-enter stays on while the content is shown, so its last frame is the resting state.

Match duration to your CSS

duration is the animation length in milliseconds, and defaults to 300. LynxTransition can't detect when a CSS animation ends, so it uses duration to know when to remove leaving content.

  • If duration is shorter than the CSS animation, the content disappears before the leave animation finishes.
  • If it's longer, the content stays on its last frame until the time is up.

Run code after an animation

(afterEnter) and (afterLeave) fire when duration has passed:

<lynx-transition
  [show]="visible()"
  name="fade"
  [duration]="300"
  (afterEnter)="onShown()"
  (afterLeave)="onHidden()"
>
  <view class="panel"><text>Hello!</text></view>
</lynx-transition>

(afterLeave) fires as the content is removed.

Animate list items

Angular's @for removes an item's view straight away, which leaves no time for a leave animation. LynxTransitionGroup renders the list itself, so each item can animate in and out:

src/app/animated-list.ts
import { Component, signal } from '@angular/core';
import { LYNX_ELEMENTS, LynxTransitionGroup } from '@blotch/angular-lynx'; 

type Item = { id: number; name: string };

@Component({
  selector: 'app-animated-list',
  imports: [LYNX_ELEMENTS, LynxTransitionGroup],
  template: `
    <view (bindtap)="add()"><text>Add item</text></view>

    <lynx-transition-group
      [each]="items()"
      [trackBy]="byId"
      name="list"
      [duration]="300"
    >
      <ng-template let-item>
        <view (bindtap)="remove(item.id)">
          <text>{{ item.name }}</text>
        </view>
      </ng-template>
    </lynx-transition-group>
  `,
  styles: `
    @keyframes list-enter {
      from {
        opacity: 0;
        transform: translateX(30px);
      }
      to {
        opacity: 1;
        transform: translateX(0);
      }
    }
    @keyframes list-leave {
      from {
        opacity: 1;
        transform: translateX(0);
      }
      to {
        opacity: 0;
        transform: translateX(-30px);
      }
    }
    .list-enter {
      animation: list-enter 300ms ease both;
    }
    .list-leave {
      animation: list-leave 300ms ease both;
    }
  `,
})
export class AnimatedList {
  readonly items = signal<Item[]>([]);
  readonly byId = (item: Item) => item.id;
  #nextId = 1;

  add(): void {
    const id = this.#nextId++;
    this.items.update((list) => [...list, { id, name: `Item ${id}` }]);
  }

  remove(id: number): void {
    this.items.update((list) => list.filter((item) => item.id !== id));
  }
}
  • [each] is the list, and the <ng-template> renders one item. let-item names the item.
  • [trackBy] returns a unique key per item, like track in @for. The default uses the item itself, which only works for strings, numbers and other values you don't recreate.
  • The classes go on each item's first element, here the <view> inside the template.
  • Items in the first render don't animate. Only items added or removed later do.
  • A removed item stays in place while it animates out. If it comes back before the leave animation ends, it stops leaving.
  • (afterEnter) and (afterLeave) emit the item that finished animating.

Limitations

  • No move animations. When items change order, they jump to their new places.
  • Only some properties animate, as with any Lynx animation.