CSS daje ci dwa narzędzia do ruchu: przejścia i animacje @keyframes. Większość deweloperów wie, że oba istnieją, ale sięga po to, którego nauczyli się jako pierwszego — zwykle przejść — a potem zastanawia się, dlaczego spinner ładowania nie zapętla się lub dlaczego powiadomienie powraca na oryginalne miejsce. Decyzja nie jest skomplikowana, gdy rozumiesz, do czego służy każde z nich. Przejścia to reakcja: coś zmienia stan, przeglądarka wygładza zmianę. Animacje @keyframes to samodzielne skrypty: działają samodzielnie, zapętlają się jeśli chcesz i nie potrzebują wyzwalacza stanu. Ten przewodnik obejmuje oba, właściwości, które mają największe znaczenie, i szczegóły wydajności oddzielające płynne animacje 60fps od tych szarpanych. Jeśli piszesz dużo CSS, CSS Formatter warto dodać do zakładek — szybko oczyści każdy zagmatwany skrót.
Przejścia CSS — prosty przypadek
Przejście mówi przeglądarce: „gdy ta właściwość się zmieni, animuj zmianę w czasie zamiast skakać". To wszystko. Najczęstszym zastosowaniem są efekty najechania kursorem — zmiany koloru, zanikanie przezroczystości, subtelne transformacje skali. Przewodnik MDN po przejściach omawia pełną specyfikację, ale cztery właściwości, o które chodzi to transition-property, transition-duration, transition-timing-function i transition-delay.
/* The shorthand: property duration easing delay */
.btn-primary {
background-color: #4f46e5;
color: #fff;
padding: 0.625rem 1.25rem;
border-radius: 6px;
border: none;
cursor: pointer;
transition: background-color 200ms ease, transform 150ms ease;
}
.btn-primary:hover {
background-color: #4338ca;
transform: translateY(-1px);
}
.btn-primary:active {
transform: translateY(0);
}
/* Opacity fade — a card coming into view */
.notification-card {
opacity: 0;
transition: opacity 300ms ease-in-out;
}
.notification-card.is-visible {
opacity: 1;
}Możesz wylistować wiele przejść oddzielonych przecinkami, każde z własnym czasem trwania i łagodzeniem — właśnie to robi przykład przycisku powyżej. Każda właściwość animuje się niezależnie.
transition: all. Kuszące jako ogólne rozwiązanie, ale oznacza to, że każda zmiana właściwości na tym elemencie zostanie animowana — w tym te niezamierzone. Powoduje też więcej pracy przeglądarki przy każdej klatce. Wymień konkretne właściwości, które chcesz przejść. Dokumentacja MDN transition-property wyjaśnia, które właściwości można animować.transition-timing-function — wyjaśnienie łagodzenia
Funkcja czasowania kontroluje krzywą przyspieszenia animacji — czy startuje szybko i zwalnia, porusza się ze stałą prędkością, czy odbija na niestandardowej krzywej. Wbudowane słowa kluczowe pokrywają większość sytuacji.
ease— startuje powoli, przyspiesza, a następnie zwalnia pod koniec. Domyślny. Dobry dla większości ruchu interfejsu.linear— stała prędkość przez cały czas. Dobry dla spinnerów i pasków postępu, zły dla większości innych — wygląda mechanicznie.ease-in— startuje powoli, kończy szybko. Sprawia wrażenie, jakby coś nabierało prędkości. Dobry dla elementów opuszczających ekran.ease-out— startuje szybko, kończy powoli. Wygląda naturalnie dla elementów wchodzących na ekran (jak powiadomienie wsuwające się).ease-in-out— powolny na obu końcach. Wygląda dopracowanie dla elementów, które startują i zatrzymują się w polu widzenia.cubic-bezier(x1, y1, x2, y2)— zdefiniuj własną krzywą. Narzędzia takie jak cubic-bezier.com pozwalają tworzyć i podglądać niestandardowe krzywe wizualnie.
/* Custom spring-like easing with cubic-bezier */
.drawer {
transform: translateX(-100%);
transition: transform 350ms cubic-bezier(0.34, 1.56, 0.64, 1);
}
.drawer.is-open {
transform: translateX(0);
}
/* The cubic-bezier values above overshoot slightly (y > 1)
which gives a satisfying spring effect on open */@keyframes — gdy przejścia nie wystarczą
Przejścia mają twardy limit: potrzebują dwóch stanów i wyzwalacza. Jeśli chcesz, żeby coś zapętlało się w nieskończoność, działało podczas ładowania strony bez żadnej interakcji lub animowało przez więcej niż dwa kroki, potrzebujesz @keyframes. Dokumentacja @keyframes MDN omawia pełną składnię. Definiujesz klatki kluczowe osobno, a następnie dołączasz je do elementu za pomocą właściwości animation.
/* Define the animation */
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* Attach it to an element */
.spinner {
width: 24px;
height: 24px;
border: 3px solid #e5e7eb;
border-top-color: #4f46e5;
border-radius: 50%;
animation: spin 700ms linear infinite;
}
/* Multi-step — more than two stops */
@keyframes pulse-ring {
0% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.15); opacity: 0.7; }
100% { transform: scale(1); opacity: 1; }
}
.badge-live {
animation: pulse-ring 1.5s ease-in-out infinite;
}Skrót animation — wszystkie osiem właściwości
Skrót animation pakuje osiem właściwości w jedną deklarację. Nie musisz ich wszystkich używać za każdym razem, ale znajomość tego, co każda kontroluje, oszczędza ci kopania w dokumentach, gdy coś nie działa poprawnie.
/*
animation: name | duration | timing-function | delay | iteration-count | direction | fill-mode | play-state
*/
.toast {
animation: slide-in-right 350ms ease-out 0s 1 normal forwards running;
}
/* More commonly written as just the values you need: */
.toast {
animation: slide-in-right 350ms ease-out forwards;
}
/* Breaking it out to individual properties for clarity: */
.confetti-piece {
animation-name: float-down;
animation-duration: 2s;
animation-timing-function: ease-in;
animation-delay: calc(var(--i) * 150ms); /* staggered via CSS custom property */
animation-iteration-count: 1;
animation-direction: normal;
animation-fill-mode: forwards;
animation-play-state: running;
}
/* Pausing an animation via JavaScript: toggle a class */
.spinner.is-paused {
animation-play-state: paused;
}animation-fill-mode — naprawianie problemu z powrotem na miejsce
To właściwość, na której wszyscy się potykają w pewnym momencie. Domyślnie, gdy animacja się kończy, element wraca na swoje style sprzed animacji — jakby animacja nigdy nie miała miejsca. Jeśli animujesz powiadomienie toast wsuwające się z prawej strony, wróci na ekran poza widokiem w momencie zakończenia animacji. animation-fill-mode: forwards naprawia to, utrzymując element w jego finalnym stanie klatki kluczowej po zakończeniu animacji.
@keyframes slide-in-right {
from {
transform: translateX(110%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
/* Without forwards: toast slides in, then SNAPS back off-screen */
.toast-bad {
animation: slide-in-right 350ms ease-out;
}
/* With forwards: toast slides in and STAYS in position */
.toast {
animation: slide-in-right 350ms ease-out forwards;
}
/*
fill-mode values:
- none (default) — no styles applied before or after
- forwards — hold the final keyframe after the animation ends
- backwards — apply the first keyframe during the delay period
- both — forwards + backwards combined
*/Praktyczne przykłady — szkielet ładowania, spinner, toast, pulsujące oznaczenie
Oto cztery wzorce, których będziesz używać stale. Każdy demonstruje konkretną kombinację @keyframes i właściwości animacji.
/* --- 1. Skeleton loading shimmer --- */
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
.skeleton {
background: linear-gradient(
90deg,
#f0f0f0 25%,
#e0e0e0 50%,
#f0f0f0 75%
);
background-size: 200% 100%;
animation: shimmer 1.5s infinite linear;
border-radius: 4px;
}
.skeleton-title { height: 20px; width: 60%; margin-bottom: 12px; }
.skeleton-text { height: 14px; width: 100%; margin-bottom: 8px; }
.skeleton-text:last-child { width: 80%; }/* --- 2. Spinning loader --- */
@keyframes spin {
to { transform: rotate(360deg); }
}
.loader-spinner {
display: inline-block;
width: 32px;
height: 32px;
border: 3px solid rgba(79, 70, 229, 0.2);
border-top-color: #4f46e5;
border-radius: 50%;
animation: spin 600ms linear infinite;
}/* --- 3. Slide-in notification toast --- */
@keyframes toast-enter {
from {
transform: translateX(calc(100% + 1.5rem));
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes toast-exit {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(calc(100% + 1.5rem));
opacity: 0;
}
}
.toast {
position: fixed;
bottom: 1.5rem;
right: 1.5rem;
padding: 0.875rem 1.25rem;
background: #1f2937;
color: #fff;
border-radius: 8px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
animation: toast-enter 350ms ease-out forwards;
}
.toast.is-dismissing {
animation: toast-exit 300ms ease-in forwards;
}/* --- 4. Pulsing notification badge --- */
@keyframes badge-pulse {
0%, 100% {
box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.5);
}
50% {
box-shadow: 0 0 0 6px rgba(239, 68, 68, 0);
}
}
.notification-badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 20px;
height: 20px;
padding: 0 5px;
background: #ef4444;
color: #fff;
font-size: 11px;
font-weight: 700;
border-radius: 999px;
animation: badge-pulse 2s ease-in-out infinite;
}Wydajność — właściwości kompozytowane przez GPU vs. wyzwalające układ
Nie wszystkie właściwości CSS są równe pod względem wydajności animacji. Potok renderowania przeglądarki ma trzy etapy, które tu mają znaczenie: układ (obliczanie rozmiarów i pozycji elementów), malowanie (wypełnianie pikseli) i kompozytowanie (łączenie warstw na GPU). Animowanie właściwości, która wyzwala układ, oznacza, że przeglądarka ponownie oblicza całą geometrię dokumentu przy każdej klatce — to kosztowne i powoduje szarpanie. Przewodnik po wydajności animacji web.dev omawia to dogłębnie.
- Bezpieczne do animowania (kompozytowane przez GPU):
transform(translate, scale, rotate) iopacity. Działają całkowicie na wątku kompozytora GPU — nigdy nie wyzwalają układu ani malowania. - Powoduje malowanie (unikaj jeśli możliwe):
color,background-color,border-color,box-shadow. Pomijają układ, ale wyzwalają ponowne malowanie przy każdej klatce. Krótkie przejścia (poniżej 300ms) są zwykle w porządku. - Powoduje układ (nigdy nie animuj):
width,height,top,left,margin,padding. Każda klatka wyzwala pełne ponowne obliczenie układu — gwarantowane szarpanie na złożonych stronach.
transform i opacity. Jeśli chcesz coś przesunąć, użyj translate, nie left/top. Jeśli chcesz coś zmienić rozmiar, użyj scale, nie width/height. CSS Triggers to referencja, która wymienia dokładnie, które etapy renderowania osiąga każda właściwość./* Bad — animating width triggers layout on every frame */
.progress-bar-bad {
transition: width 300ms ease;
}
/* Good — use scaleX transform instead */
.progress-bar {
transform-origin: left center;
transition: transform 300ms ease;
}
/* In JS, set: progressBar.style.transform = 'scaleX(0.75)' for 75% */
/* Bad — animating top/left (positions element with layout) */
.tooltip-bad {
position: absolute;
top: 0;
transition: top 200ms ease;
}
/* Good — use translateY instead */
.tooltip {
position: absolute;
top: 0;
transform: translateY(0);
transition: transform 200ms ease;
}will-change — używaj oszczędnie
will-change to wskazówka dla przeglądarki, że konkretna właściwość zaraz zostanie animowana, więc powinna awansować element do własnej warstwy kompozytora z wyprzedzeniem. Może to wyeliminować krótki błysk szarpania, który czasem widać na początku animacji na słabszym sprzęcie. Ale ma realny koszt: każda awansowana warstwa zużywa pamięć GPU. Jeśli dodasz will-change: transform do każdego animowanego elementu w swojej aplikacji, pogorszysz wydajność na większości urządzeń — odwrotnie do zamierzonego.
/* Right way — add it just before animation starts, remove after */
.modal-overlay {
/* Don't set will-change here by default */
}
/* Add via JavaScript only when user triggers modal open */
/* overlay.style.willChange = 'opacity'; */
/* overlay.addEventListener('transitionend', () => { */
/* overlay.style.willChange = 'auto'; */
/* }); */
/* Acceptable in CSS for elements that animate frequently
(e.g., a persistent floating action button on scroll) */
.fab {
will-change: transform; /* OK — this element genuinely animates on scroll */
transition: transform 200ms ease;
}
/* Don't do this — wastes GPU memory with zero benefit */
.card {
will-change: transform; /* BAD — card only animates on hover, not constantly */
}prefers-reduced-motion — dostępność, której nie możesz pominąć
Znaczna część użytkowników ma zaburzenia przedsionkowe lub inne schorzenia, w których ruch powoduje dyskomfort lub nudności. Zapytanie medialne prefers-reduced-motion pozwala ci respektować ustawienie systemowe „ogranicz ruch". Wytyczna WCAG 2.1 2.3.3 omawia ten wymóg, a dokumentacja prefers-reduced-motion MDN pokazuje obsługę przeglądarek (jest teraz powszechna). Wzorzec jest prosty: owijaj swoje animacje w standardowe zapytanie medialne i zapewnij alternatywę bez ruchu wewnątrz tego ze zredukowanym ruchem.
/* Define animations normally for users who are fine with motion */
@keyframes slide-in-right {
from { transform: translateX(110%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
.toast {
animation: slide-in-right 350ms ease-out forwards;
}
.spinner {
animation: spin 600ms linear infinite;
}
/* Override for users who prefer reduced motion */
@media (prefers-reduced-motion: reduce) {
.toast {
/* No sliding — just appear */
animation: none;
opacity: 1;
transform: none;
}
.spinner {
/* Slow it way down or stop it entirely */
animation-duration: 4s;
}
/* Nuclear option — disable ALL animations site-wide */
/* Use this only if you haven't audited each animation individually */
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}„Opcja nuklearna" na końcu tego fragmentu to powszechny wzorzec dla istniejących baz kodu, które nie przeprowadziły audytu każdej animacji. To lepsze niż nic, ale audytowanie każdej animacji indywidualnie daje ci większą kontrolę — niektóre animacje przekazują stan (pasek postępu, spinner ładowania) i powinny być zachowane, tylko spowolnione.
Przejścia vs @keyframes — przewodnik po decyzji
Jeśli kiedykolwiek nie wiesz, po które sięgnąć, zadaj sobie dwa pytania dotyczące animacji, którą budujesz:
- Czy jest wyzwalana przez zmianę stanu? (najechanie, fokus, przełączenie klasy, checkbox) → Użyj przejścia. Dokładnie do tego są zaprojektowane przejścia.
- Czy zapętla się w nieskończoność? → Użyj @keyframes z
animation-iteration-count: infinite. - Czy ma więcej niż dwa kroki? (nie tylko start → koniec, ale start → środek → koniec lub więcej) → Użyj @keyframes.
- Czy musi działać podczas ładowania strony, bez żadnej interakcji użytkownika? → Użyj @keyframes.
- Czy element musi pozostać w swoim końcowym stanie po zakończeniu? → Użyj @keyframes z
animation-fill-mode: forwards.
Podsumowanie
Przejścia obsługują ruch sterowany stanem. @keyframes obsługuje wszystko inne. Zawsze animuj transform i opacity dla wydajności — zostaw width, height, top i left poza tym. Używaj animation-fill-mode: forwards zawsze, gdy potrzebujesz, żeby element utrzymał swój końcowy stan. Dodaj prefers-reduced-motion przed wdrożeniem czegokolwiek — to dziiesięcioliniowy dodatek, który ma duże znaczenie dla prawdziwych użytkowników. Aby uzyskać więcej informacji na temat modelu kompozytowania i tego, co wyzwala układ, dokumentacja wydajności renderowania web.dev jest najbardziej praktycznym dostępnym zasobem. Gdy już napiszesz CSS, uruchom go przez CSS Formatter, żeby skróty były czytelne, lub przez CSS Minifier, żeby usunąć białe znaki przed wdrożeniem do produkcji.