CSS 提供了一套強大的工具,讓你在應用程式中建立美觀且吸引人的動畫。
若你從未寫過任何原生 CSS 動畫,有許多優秀的指南可以帶你入門。以下列出幾篇:
MDN 的 CSS Animations 指南
W3Schools CSS3 Animations 指南
The Complete CSS Animations Tutorial
CSS Animation for Beginners
以及幾部影片:
Learn CSS Animation in 9 Minutes
Net Ninja CSS Animation Tutorial Playlist
先看看其中一些指南與教學,再回來繼續閱讀本指南。
你可以使用 @keyframes 建立可在整個應用程式共用的可重複使用動畫。把關鍵影格動畫定義在共用的 CSS 檔案中,你就能在應用程式的任何地方重複使用這些關鍵影格動畫。
@keyframes sharedAnimation {
to {
height: 0;
opacity: 1;
background-color: 'red';
}
}
.animated-class {
animation: sharedAnimation 1s;
}
把 animated-class 類別加到元素上,就會觸發該元素的動畫。
你可能想在兩種不同狀態之間加入動畫,例如元素開啟或關閉時。你可以用 CSS 類別搭配關鍵影格動畫或轉場樣式來達成。
.open {
height: '200px';
opacity: 1;
background-color: 'yellow';
transition: all 1s;
}
.closed {
height: '100px';
opacity: 0.8;
background-color: 'blue';
transition: all 1s;
}
觸發 open 或 closed 狀態的方法,是在元件中切換元素上的類別。你可以在我們的範本指南中找到做法範例。
範本指南中也有直接為樣式加入動畫的類似範例。
加入動畫時,常需要調整時間、延遲與緩動行為。這可以透過多個 CSS 屬性或簡寫屬性完成。
在 CSS 中為關鍵影格動畫指定 animation-duration、animation-delay 與 animation-timing-function,或改用 animation 簡寫屬性。
.example-element {
animation-duration: 1s;
animation-delay: 500ms;
animation-timing-function: ease-in-out;
}
.example-shorthand {
animation: exampleAnimation 1s ease-in-out 500ms;
}
同樣地,對於不使用 @keyframes 的動畫,你可以使用 transition-duration、transition-delay、transition-timing-function 以及 transition 簡寫屬性。
.example-element {
transition-duration: 1s;
transition-delay: 500ms;
transition-timing-function: ease-in-out;
transition-property: margin-right;
}
.example-shorthand {
transition: margin-right 1s ease-in-out 500ms;
}
只要切換 CSS 樣式或類別,就能觸發動畫。當類別出現在元素上時,動畫就會發生。移除類別後,元素會還原為該元素所定義的 CSS。以下是一個範例:
import {Component, signal} from '@angular/core';
@Component({
selector: 'app-open-close',
templateUrl: 'open-close.html',
styleUrls: ['open-close.css'],
})
export class OpenClose {
isOpen = signal(true);
toggle() {
this.isOpen.update((isOpen) => !isOpen);
}
}
<h2>Open / Close Example</h2>
<button type="button" class="toggle-btn" (click)="toggle()">Toggle Open/Close</button>
<div class="open-close-container" [class.open]="isOpen()">
<p>The box is now {{ isOpen() ? 'Open' : 'Closed' }}!</p>
</div>
:host {
display: block;
margin-top: 1rem;
}
.open-close-container {
border: 1px solid #dddddd;
margin-top: 1em;
padding: 20px 20px 0px 20px;
font-weight: bold;
font-size: 20px;
height: 100px;
opacity: 0.8;
background: #3b82f6;
color: #ebebeb;
transition-property: height, opacity, background-color, color;
transition-duration: 1s;
}
.toggle-btn {
background: transparent;
border: 1px solid var(--primary-contrast, black);
color: var(--primary-contrast, black);
padding: 10px 24px;
border-radius: 8px;
cursor: pointer;
}
.open {
transition-duration: 0.5s;
height: 200px;
opacity: 1;
background: #475569;
color: #f9fafb;
}
你可以使用 CSS Grid 為自動高度加入動畫。
import {Component, signal} from '@angular/core';
@Component({
selector: 'app-auto-height',
templateUrl: 'auto-height.html',
styleUrls: ['auto-height.css'],
})
export class AutoHeight {
isOpen = signal(true);
toggle() {
this.isOpen.update((isOpen) => !isOpen);
}
}
<h2>Auto Height Example</h2>
<button type="button" class="toggle-btn" (click)="toggle()">Toggle Open/Close</button>
<div class="container" [class.open]="isOpen()">
<div class="content">
<p>The box is now {{ isOpen() ? 'Open' : 'Closed' }}!</p>
</div>
</div>
.container {
display: grid;
grid-template-rows: 0fr;
overflow: hidden;
transition: grid-template-rows 1s;
}
.container.open {
grid-template-rows: 1fr;
}
.container .content {
min-height: 0;
transition: visibility 1s;
padding: 0 20px;
visibility: hidden;
margin-top: 1em;
font-weight: bold;
font-size: 20px;
background: #3b82f6;
color: #ebebeb;
overflow: hidden;
}
.container.open .content {
visibility: visible;
}
.toggle-btn {
background: transparent;
border: 1px solid var(--primary-contrast, black);
color: var(--primary-contrast, black);
padding: 10px 24px;
border-radius: 8px;
cursor: pointer;
}
若你不需要顧慮支援所有瀏覽器,也可以看看 calc-size(),那是為自動高度加入動畫的真正解法。更多資訊請見 MDN 的文件與這篇教學。
你可以為項目進入檢視或離開檢視時建立動畫。先看看如何為進入檢視的元素加入動畫。我們會用 animate.enter 來做,它會在元素進入檢視時套用動畫類別。
import {Component, signal} from '@angular/core';
@Component({
selector: 'app-insert',
templateUrl: 'insert.html',
styleUrls: ['insert.css'],
})
export class Insert {
isShown = signal(false);
toggle() {
this.isShown.update((isShown) => !isShown);
}
}
<h2>Insert Element Example</h2>
<nav>
<button type="button" class="toggle-btn" (click)="toggle()">Toggle Element</button>
</nav>
@if (isShown()) {
<div class="insert-container" animate.enter="enter-animation">
<p>The box is inserted</p>
</div>
}
:host {
display: block;
}
.insert-container {
border: 1px solid #dddddd;
margin-top: 1em;
padding: 20px;
font-weight: bold;
font-size: 20px;
}
.insert-container p {
margin: 0;
}
.enter-animation {
animation: slide-fade 1s;
}
@keyframes slide-fade {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.toggle-btn {
background: transparent;
border: 1px solid var(--primary-contrast, black);
color: var(--primary-contrast, black);
padding: 10px 24px;
border-radius: 8px;
cursor: pointer;
}
為離開檢視的元素加入動畫,與為進入檢視的元素加入動畫相似。使用 animate.leave 指定元素離開檢視時要套用的 CSS 類別。
import {Component, signal} from '@angular/core';
@Component({
selector: 'app-remove',
templateUrl: 'remove.html',
styleUrls: ['remove.css'],
})
export class Remove {
isShown = signal(false);
toggle() {
this.isShown.update((isShown) => !isShown);
}
}
<h2>Remove Element Example</h2>
<nav>
<button type="button" class="toggle-btn" (click)="toggle()">Toggle Element</button>
</nav>
@if (isShown()) {
<div class="insert-container" animate.leave="deleting">
<p>The box is inserted</p>
</div>
}
:host {
display: block;
}
.insert-container {
border: 1px solid #dddddd;
margin-top: 1em;
padding: 20px;
font-weight: bold;
font-size: 20px;
opacity: 1;
transition: opacity 200ms ease-in;
@starting-style {
opacity: 0;
}
}
.insert-container p {
margin: 0;
}
.deleting {
opacity: 0;
transform: translateY(20px);
transition:
opacity 500ms ease-out,
transform 500ms ease-out;
}
.toggle-btn {
background: transparent;
border: 1px solid var(--primary-contrast, black);
color: var(--primary-contrast, black);
padding: 10px 24px;
border-radius: 8px;
cursor: pointer;
}
NOTE: 子元素的 animate.leave 動畫只會在同一個元件範本內觸發。當父元素被移除時,巢狀元件的 animate.leave 動畫不會觸發。
更多關於 animate.enter 與 animate.leave 的資訊,請見進入與離開動畫指南。
為遞增與遞減加入動畫,是應用程式中常見的模式。以下範例示範如何達成這種行為。
import {Component, ElementRef, OnInit, signal, viewChild} from '@angular/core';
@Component({
selector: 'app-increment-decrement',
templateUrl: 'increment-decrement.html',
styleUrls: ['increment-decrement.css'],
})
export class IncrementDecrement implements OnInit {
num = signal(0);
el = viewChild<ElementRef<HTMLParagraphElement>>('el');
ngOnInit() {
this.el()?.nativeElement.addEventListener('animationend', (ev) => {
if (ev.animationName.endsWith('decrement') || ev.animationName.endsWith('increment')) {
this.animationFinished();
}
});
}
modify(n: number) {
const targetClass = n > 0 ? 'increment' : 'decrement';
this.num.update((v) => (v += n));
this.el()?.nativeElement.classList.add(targetClass);
}
animationFinished() {
this.el()?.nativeElement.classList.remove('increment', 'decrement');
}
ngOnDestroy() {
this.el()?.nativeElement.removeEventListener('animationend', this.animationFinished);
}
}
<h3>Increment and Decrement Example</h3>
<section>
<p #el>Number {{ num() }}</p>
<div class="controls">
<button type="button" (click)="modify(1)">+</button>
<button type="button" (click)="modify(-1)">-</button>
</div>
</section>
:host {
display: block;
font-size: 32px;
margin: 20px;
text-align: center;
}
section {
border: 1px solid lightgray;
border-radius: 50px;
}
p {
display: inline-block;
margin: 2rem 0;
text-transform: uppercase;
}
.increment {
animation: increment 300ms;
}
.decrement {
animation: decrement 300ms;
}
.controls {
padding-bottom: 2rem;
}
button {
font: inherit;
border: 0;
background: lightgray;
width: 50px;
border-radius: 10px;
}
button + button {
margin-left: 10px;
}
@keyframes increment {
33% {
color: green;
transform: scale(1.3, 1.2);
}
66% {
color: green;
transform: scale(1.2, 1.2);
}
100% {
transform: scale(1, 1);
}
}
@keyframes decrement {
33% {
color: red;
transform: scale(0.8, 0.9);
}
66% {
color: red;
transform: scale(0.9, 0.9);
}
100% {
transform: scale(1, 1);
}
}
若你想停用已指定的動畫,有多種做法。
- 建立一個自訂類別,強制把 animation 與 transition 設為
none。
.no-animation {
animation: none !important;
transition: none !important;
}
把這個類別套用到元素上,就能避免該元素觸發任何動畫。你也可以把作用範圍設定為整個 DOM 或 DOM 的某個區段,來強制這種行為。不過,這會讓動畫事件無法觸發。若你正在等待動畫事件來移除元素,這個做法就行不通。替代方案是把持續時間設為 1 毫秒。
使用 prefers-reduced-motion media query,確保偏好減少動畫的使用者不會看到動畫播放。
避免以程式方式加入動畫類別
若你有想在動畫的特定時間點執行的動作,有多個可監聽的事件可用。以下列出其中幾個。
OnAnimationStart
OnAnimationEnd
OnAnimationIteration
OnAnimationCancel
OnTransitionStart
OnTransitionRun
OnTransitionEnd
OnTransitionCancel
Web Animations API 還提供許多額外功能。請參閱文件,了解所有可用的動畫 API。
NOTE: 使用這些回呼時請注意事件冒泡問題。若你同時為子元素與父元素加入動畫,事件會從子元素向上冒泡到父元素。請考慮停止事件傳播,或查看事件中的更多細節,確認你回應的是想要的事件目標、而不是從子節點冒泡上來的事件。你可以檢查 animationname 屬性或正在轉場的屬性,確認處理的是正確的節點。
動畫通常不只是簡單的淡入或淡出。你可能有許多想執行的複雜動畫序列。以下來看看其中一些可能的情境。
一個常見的效果,是讓清單中每個項目的動畫錯開播放,形成瀑布般的效果。這可以利用 animation-delay 或 transition-delay 達成。以下範例示範這類 CSS 可能的寫法。
import {Component, signal} from '@angular/core';
@Component({
selector: 'app-stagger',
templateUrl: './stagger.html',
styleUrls: ['stagger.css'],
})
export class Stagger {
show = signal(true);
items = [1, 2, 3];
refresh() {
this.show.set(false);
setTimeout(() => {
this.show.set(true);
}, 10);
}
}
<h1>Stagger Example</h1>
<button type="button" class="toggle-btn" (click)="refresh()">Refresh</button>
<div class="items-container">
@if (show()) {
<ul class="items">
@for (item of items; track $index) {
<li class="item" style="--index: {{ $index }}">{{ item }}</li>
}
</ul>
}
</div>
.items-container {
min-height: 4.5rem;
}
.items {
list-style: none;
padding: 0;
margin: 0;
}
.items .item {
transition-property: opacity, transform;
transition-duration: 500ms;
transition-delay: calc(200ms * var(--index));
@starting-style {
opacity: 0;
transform: translateX(-10px);
}
}
.toggle-btn {
background: transparent;
border: 1px solid var(--primary-contrast, black);
color: var(--primary-contrast, black);
padding: 10px 24px;
border-radius: 8px;
cursor: pointer;
}
你可以使用 animation 簡寫屬性,同時把多個動畫套用到一個元素上。每個動畫都可以有各自的持續時間與延遲。這讓你能把動畫組合在一起,創造出複雜的效果。
.target-element {
animation:
rotate 3s,
fade-in 2s;
}
在這個範例中,rotate 與 fade-in 動畫會同時觸發,但持續時間不同。
@for 迴圈中的項目會被移除再重新加入,進入動畫會透過 @starting-styles 觸發。或者,你也可以用 animate.enter 達成相同行為。使用 animate.leave 讓元素在被移除時播放動畫,如下例所示。
import {Component, signal} from '@angular/core';
@Component({
selector: 'app-reorder',
templateUrl: './reorder.html',
styleUrls: ['reorder.css'],
})
export class Reorder {
show = signal(true);
items = ['stuff', 'things', 'cheese', 'paper', 'scissors', 'rock'];
randomize() {
const randItems = [...this.items];
const newItems = [];
for (let i of this.items) {
const max: number = this.items.length - newItems.length;
const randNum = Math.floor(Math.random() * max);
newItems.push(...randItems.splice(randNum, 1));
}
this.items = newItems;
}
}
<h1>Reordering List Example</h1>
<button type="button" class="toggle-btn" (click)="randomize()">Randomize</button>
<ul class="items">
@for (item of items; track item) {
<li class="item" animate.leave="fade">{{ item }}</li>
}
</ul>
.items {
list-style: none;
padding: 0;
margin: 0;
}
.items .item {
transition-property: opacity, transform;
transition-duration: 500ms;
@starting-style {
opacity: 0;
transform: translateX(-10px);
}
}
.items .item.fade {
animation: fade-out 500ms;
}
@keyframes fade-out {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
.toggle-btn {
background: transparent;
border: 1px solid var(--primary-contrast, black);
color: var(--primary-contrast, black);
padding: 10px 24px;
border-radius: 8px;
cursor: pointer;
}
你可以使用 Element.getAnimations() 直接從元素取得動畫。它會回傳該元素上每一個 Animation 組成的陣列。你可以用 Animation API 做到比 animations 套件的 AnimationPlayer 更多的事。由此你可以 cancel()、play()、pause()、reverse() 等等。這個原生 API 應該能提供你控制動畫所需的一切。
你可能也會對以下內容有興趣: