متحرکسازی Application با CSS
CSS مجموعه قدرتمندی از ابزارها را در اختیار شما میگذارد تا animationهای زیبا و جذاب داخل application خود ایجاد کنید.
نوشتن animationها با CSS native
اگر تا حالا animation native با CSS ننوشتهاید، guideهای بسیار خوبی برای شروع وجود دارند. چند نمونه: MDN's CSS Animations guide W3Schools CSS3 Animations guide The Complete CSS Animations Tutorial CSS Animation for Beginners
و چند ویدیو: Learn CSS Animation in 9 Minutes Net Ninja CSS Animation Tutorial Playlist
چند مورد از این guideها و tutorialها را ببینید و سپس به این راهنما برگردید.
ساخت Animationهای قابل استفاده مجدد
میتوانید با استفاده از @keyframes، animationهای قابل استفاده مجدد بسازید که در سراسر application شما share شوند. keyframe animationها را در یک فایل CSS مشترک تعریف کنید تا بتوانید هر جا در application خواستید دوباره از آنها استفاده کنید.
/* #docregion animation-shared */
@keyframes sharedAnimation {
to {
height: 0;
opacity: 1;
background-color: 'red';
}
}
.animated-class {
animation: sharedAnimation 1s;
}
/* #enddocregion animation-shared */
/* #docregion animation-states */
.open {
height: '200px';
opacity: 1;
background-color: 'yellow';
transition: all 1s;
}
.closed {
height: '100px';
opacity: 0.8;
background-color: 'blue';
transition: all 1s;
}
/* #enddocregion animation-states */
/* #docregion animation-timing */
.example-element {
animation-duration: 1s;
animation-delay: 500ms;
animation-timing-function: ease-in-out;
}
.example-shorthand {
animation: exampleAnimation 1s ease-in-out 500ms;
}
/* #enddocregion animation-timing */
/* #docregion transition-timing */
.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;
}
/* #enddocregion transition-timing */افزودن class مربوط به animated-class به یک element، animation را روی همان element trigger میکند.
متحرکسازی یک Transition
متحرکسازی State و Styleها
ممکن است بخواهید بین دو state متفاوت animate کنید، برای مثال وقتی یک element باز یا بسته میشود. میتوانید این کار را با CSS classها انجام دهید، چه با keyframe animation و چه با transition styling.
/* #docregion animation-shared */
@keyframes sharedAnimation {
to {
height: 0;
opacity: 1;
background-color: 'red';
}
}
.animated-class {
animation: sharedAnimation 1s;
}
/* #enddocregion animation-shared */
/* #docregion animation-states */
.open {
height: '200px';
opacity: 1;
background-color: 'yellow';
transition: all 1s;
}
.closed {
height: '100px';
opacity: 0.8;
background-color: 'blue';
transition: all 1s;
}
/* #enddocregion animation-states */
/* #docregion animation-timing */
.example-element {
animation-duration: 1s;
animation-delay: 500ms;
animation-timing-function: ease-in-out;
}
.example-shorthand {
animation: exampleAnimation 1s ease-in-out 500ms;
}
/* #enddocregion animation-timing */
/* #docregion transition-timing */
.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;
}
/* #enddocregion transition-timing */Trigger کردن state مربوط به open یا closed با toggle کردن classها روی element در کامپوننت شما انجام میشود. میتوانید مثالهای انجام این کار را در template guide ببینید.
در template guide مثالهای مشابهی برای animate کردن مستقیم styleها هم میبینید.
Transitionها، Timing و Easing
Animate کردن اغلب به تنظیم timing، delay و easing behaviorها نیاز دارد. این کار را میتوان با چند CSS property یا shorthand property انجام داد.
برای یک keyframe animation در CSS، animation-duration، animation-delay و animation-timing-function را مشخص کنید، یا به جای آن از shorthand property مربوط به animation استفاده کنید.
/* #docregion animation-shared */
@keyframes sharedAnimation {
to {
height: 0;
opacity: 1;
background-color: 'red';
}
}
.animated-class {
animation: sharedAnimation 1s;
}
/* #enddocregion animation-shared */
/* #docregion animation-states */
.open {
height: '200px';
opacity: 1;
background-color: 'yellow';
transition: all 1s;
}
.closed {
height: '100px';
opacity: 0.8;
background-color: 'blue';
transition: all 1s;
}
/* #enddocregion animation-states */
/* #docregion animation-timing */
.example-element {
animation-duration: 1s;
animation-delay: 500ms;
animation-timing-function: ease-in-out;
}
.example-shorthand {
animation: exampleAnimation 1s ease-in-out 500ms;
}
/* #enddocregion animation-timing */
/* #docregion transition-timing */
.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;
}
/* #enddocregion transition-timing */به طور مشابه، برای animationهایی که از @keyframes استفاده نمیکنند، میتوانید از transition-duration، transition-delay و transition-timing-function و shorthand مربوط به transition استفاده کنید.
/* #docregion animation-shared */
@keyframes sharedAnimation {
to {
height: 0;
opacity: 1;
background-color: 'red';
}
}
.animated-class {
animation: sharedAnimation 1s;
}
/* #enddocregion animation-shared */
/* #docregion animation-states */
.open {
height: '200px';
opacity: 1;
background-color: 'yellow';
transition: all 1s;
}
.closed {
height: '100px';
opacity: 0.8;
background-color: 'blue';
transition: all 1s;
}
/* #enddocregion animation-states */
/* #docregion animation-timing */
.example-element {
animation-duration: 1s;
animation-delay: 500ms;
animation-timing-function: ease-in-out;
}
.example-shorthand {
animation: exampleAnimation 1s ease-in-out 500ms;
}
/* #enddocregion animation-timing */
/* #docregion transition-timing */
.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;
}
/* #enddocregion transition-timing */Trigger کردن یک Animation
animationها میتوانند با toggle کردن CSS styleها یا classها trigger شوند. وقتی یک class روی element حاضر باشد، animation رخ میدهد. حذف class باعث میشود element به هر CSSای که برای آن تعریف شده برگردد. این یک مثال است:
// #docplaster
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);
}
}<!-- #docplaster -->
<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;
}Transition و Triggerها
متحرکسازی Auto Height
میتوانید از CSS Grid برای animate کردن به auto height استفاده کنید.
// #docplaster
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);
}
}<!-- #docplaster -->
<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;
}اگر لازم نیست نگران پشتیبانی همه browserها باشید، میتوانید calc-size() را هم بررسی کنید که راهحل واقعی برای animate کردن auto height است. برای اطلاعات بیشتر، MDN's docs و این tutorial را ببینید.
Animate کردن ورود و خروج از view
میتوانید برای زمانی که یک item وارد view میشود یا از view خارج میشود animation بسازید. ابتدا ببینیم چطور یک element را هنگام ورود به view animate کنیم. این کار را با animate.enter انجام میدهیم که وقتی element وارد view شود، animation classها را اعمال میکند.
// #docplaster
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);
}
}<!-- #docplaster -->
<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 کردن element هنگام خروج از view مشابه animate کردن هنگام ورود به view است. از animate.leave استفاده کنید تا مشخص کنید هنگام خروج element از view کدام CSS classها اعمال شوند.
// #docplaster
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);
}
}<!-- #docplaster -->
<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;
}برای اطلاعات بیشتر درباره animate.enter و animate.leave، راهنمای Enter and Leave animations را ببینید.
Animate کردن increment و decrement
Animate کردن هنگام increment و decrement یک pattern رایج در applicationهاست. این مثالی از انجام آن behavior است.
// #docplaster
// #docregion
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 یا همه animationها
اگر میخواهید animationهایی را که مشخص کردهاید غیرفعال کنید، چند option دارید.
- یک custom class بسازید که animation و transition را به
noneاجبار کند.
.no-animation {
animation: none !important;
transition: none !important;
}اعمال این class روی یک element مانع اجرای هر animation روی همان element میشود. همچنین میتوانید این behavior را به کل DOM یا بخشی از DOM خود scope کنید. با این حال، این کار جلوی fire شدن animation eventها را میگیرد. اگر برای حذف element منتظر animation eventها هستید، این راهحل کار نمیکند. یک workaround این است که durationها را به 1 millisecond تنظیم کنید.
- از media query مربوط به
prefers-reduced-motionاستفاده کنید تا برای کاربرانی که animation کمتر را ترجیح میدهند، هیچ animationای play نشود.
- از افزودن animation classها به صورت programatic جلوگیری کنید.
Animation Callbackها
اگر actionهایی دارید که میخواهید در نقطههای مشخصی طی animation اجرا شوند، eventهای مختلفی برای listen کردن وجود دارد. چند نمونه:
OnAnimationStart OnAnimationEnd OnAnimationIteration OnAnimationCancel
OnTransitionStart OnTransitionRun OnTransitionEnd OnTransitionCancel
Web Animations API قابلیتهای اضافی زیادی دارد. برای دیدن همه animation APIهای موجود، documentation را ببینید.
Sequenceهای پیچیده
animationها اغلب پیچیدهتر از یک fade in یا fade out ساده هستند. ممکن است sequenceهای پیچیده زیادی از animationها داشته باشید که بخواهید اجرا شوند. بیایید چند سناریوی ممکن را ببینیم.
Stagger کردن animationها در یک list
یک effect رایج این است که animation هر item در یک list را stagger کنید تا cascade effect ایجاد شود. این کار را میتوان با استفاده از animation-delay یا transition-delay انجام داد. این نمونهای از CSS چنین کاری است.
// #docplaster
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);
}
}<!-- #docplaster -->
<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های parallel
میتوانید با استفاده از shorthand property مربوط به animation، چند animation را همزمان روی یک element اعمال کنید. هرکدام میتوانند duration و delay خودشان را داشته باشند. این به شما اجازه میدهد animationها را با هم compose کنید و effectهای پیچیده بسازید.
.target-element {
animation:
rotate 3s,
fade-in 2s;
}در این مثال، animationهای rotate و fade-in همزمان fire میشوند، اما durationهای متفاوتی دارند.
Animate کردن itemهای یک list در حال reorder
itemهای داخل یک loop مربوط به @for حذف و دوباره اضافه میشوند، که animationها را با استفاده از @starting-styles برای entry animationها fire میکند. به جای آن، میتوانید برای همین behavior از animate.enter استفاده کنید. برای animate کردن elementها هنگام حذف، همانطور که در مثال زیر دیده میشود، از animate.leave استفاده کنید.
// #docplaster
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;
}
}<!-- #docplaster -->
<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;
}کنترل programmatic animationها
میتوانید animationها را مستقیماً از روی یک element با Element.getAnimations() بگیرید. این method آرایهای از هر Animation روی آن element برمیگرداند. با Animation API میتوانید بسیار بیشتر از چیزی که AnimationPlayer از package animations ارائه میکرد انجام دهید. از اینجا میتوانید cancel()، play()، pause()، reverse() و کارهای بسیار بیشتری انجام دهید. این API native باید هر چیزی را که برای کنترل animationها لازم دارید فراهم کند.
بیشتر درباره animationهای Angular
ممکن است به موارد زیر هم علاقهمند باشید: