Motion, biblioteka animacji, która zmieniła nazwę
Framer Motion usamodzielnił się i nazywa się dziś Motion. Paczka na npm to motion, a import w Reakcie prowadzi przez motion/react. Interfejs pozostał ten sam, więc przejście na nową wersję sprowadza się zwykle do podmiany nazwy paczki i ścieżek importu.
Sama biblioteka rozwiązuje problem, który w czystym CSS jest uciążliwy: animowanie elementów pojawiających się i znikających, animowanie zmian układu oraz spinanie tego wszystkiego z gestami. Silnik łączy JavaScript z natywnymi mechanizmami przeglądarki, więc animacje idą przez kartę graficzną tam, gdzie to możliwe.
Podstawy
Animowany element to zwykły znacznik poprzedzony przedrostkiem, a wartości podajesz jako właściwości.
pnpm add motionimport { motion } from 'motion/react'
export function Karta({ dzieci }: { dzieci: React.ReactNode }) {
return (
<motion.div
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.25, ease: 'easeOut' }}
>
{dzieci}
</motion.div>
)
}Trzy właściwości opisują całość: stan początkowy, stan docelowy i sposób przejścia. To wystarcza do większości animacji wejścia, które w praktyce stanowią gros pracy.
Domyślny typ przejścia zależy od animowanej wartości. Przekształcenia, czyli przesunięcie, skala i obrót, dostają sprężynę, a pozostałe wartości, na przykład przezroczystość albo kolor, krzywą czasową o długości 0,3 sekundy. Ten podział jest przemyślany: sprężyna reaguje na przerwanie w połowie i płynnie zmienia kierunek, podczas gdy animacja o zadanym czasie musi dobiec do końca albo szarpnąć, a przy samym zanikaniu ta różnica nie ma znaczenia.
Znikanie elementów
To jest właściwy powód, dla którego sięga się po tę bibliotekę. Element usuwany z drzewa Reacta znika natychmiast i w czystym CSS nie da się tego animować bez sztuczek.
import { AnimatePresence, motion } from 'motion/react'
<AnimatePresence>
{otwarte && (
<motion.div
key="panel"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
Zawartosc panelu
</motion.div>
)}
</AnimatePresence>Opakowanie zatrzymuje element w drzewie do czasu zakończenia animacji wyjścia. Klucz jest tu obowiązkowy, a jego brak jest najczęstszą przyczyną sytuacji, w której animacja wyjścia po prostu nie działa.
Przy listach, gdzie elementy dochodzą i znikają, warto ustawić tryb oczekiwania, żeby wychodzący element skończył, zanim wejdzie nowy. Inaczej przy szybkim przełączaniu dwa elementy nakładają się na siebie.
Ten sam mechanizm obsługuje przejścia między widokami. Zawartość strony opakowana w to rozwiązanie, z kluczem opartym o adres, daje płynne przejście przy nawigacji zamiast natychmiastowej podmiany.
Animacje układu
Mechanizm, który trudno odtworzyć samodzielnie. Element z odpowiednią właściwością animuje przejście między pozycjami, gdy zmienia się układ, nawet jeśli zmiana wynika ze zmiany kolejności w tablicy.
{zadania.map((zadanie) => (
<motion.li key={zadanie.id} layout>
{zadanie.tytul}
</motion.li>
))}Sortowanie listy przestaje być przeskokiem, a staje się przesunięciem. Biblioteka mierzy pozycję przed i po zmianie, po czym odtwarza przejście przekształceniem, więc animacja idzie przez kartę graficzną, a nie przez przeliczanie układu.
Powiązanym mechanizmem jest wspólny identyfikator układu, pozwalający animować przejście między dwoma różnymi elementami tak, jakby były jednym. Miniatura rozwijająca się w pełny widok to typowe zastosowanie, podobnie jak zaznaczenie przesuwające się między zakładkami, gdzie jeden element podąża za aktywną pozycją zamiast pojawiać się skokowo.
Ostrzeżenie praktyczne: animacja układu jest kosztowna przy setkach elementów naraz. Przy długiej liście lepiej animować tylko widoczny fragment albo zrezygnować z tego efektu, bo zysk wizualny nie równoważy zacięć.
Warianty
Przy trzech animowanych elementach właściwości przy każdym z nich stają się nieczytelne. Warianty pozwalają nazwać stany i sterować całym poddrzewem z jednego miejsca.
const lista = {
ukryta: { opacity: 0 },
widoczna: { opacity: 1, transition: { staggerChildren: 0.06 } }
}
const element = {
ukryta: { opacity: 0, y: 8 },
widoczna: { opacity: 1, y: 0 }
}
<motion.ul variants={lista} initial="ukryta" animate="widoczna">
{pozycje.map((p) => (
<motion.li key={p.id} variants={element}>{p.nazwa}</motion.li>
))}
</motion.ul>Nazwa stanu przekazuje się w dół drzewa automatycznie, więc dzieci nie potrzebują własnych właściwości sterujących. Opóźnienie między dziećmi daje efekt kaskady jednym parametrem, zamiast liczeniem opóźnień ręcznie.
To rozwiązanie zyskuje przy rozbudowie. Dodanie stanu „błąd" do całego formularza sprowadza się do dopisania jednego wpisu w obiekcie wariantów, a nie do zmiany dziesięciu komponentów.
Wydajność
Zasada jest jedna i pochodzi wprost z tego, jak działa przeglądarka. Animowanie przezroczystości i przekształceń jest tanie, bo dzieje się poza przeliczaniem układu. Animowanie szerokości, wysokości, marginesów i pozycji jest drogie, bo wymusza przeliczenie układu w każdej klatce.
<motion.div animate={{ scale: 1.05 }} />
<motion.div animate={{ width: 320 }} />Pierwsza wersja pójdzie płynnie przy dziesiątkach elementów, druga zacznie zacinać się przy kilku. Jeśli musisz animować rozmiar, użyj skalowania albo animacji układu zamiast bezpośredniej zmiany wymiaru.
Druga rzecz to rozmiar paczki. Import komponentu animowanego ciągnie za sobą cały silnik, więc w projekcie, gdzie animacja jest na jednej stronie, warto ładować ją dynamicznie. Biblioteka udostępnia też wariant lżejszy, oparty na natywnym interfejsie animacji przeglądarki, wystarczający do prostych przejść.
Trzecia to liczba animowanych elementów naraz. Dwadzieścia elementów z animacją układu przy przewijaniu to zwykle za dużo, a użytkownik i tak zauważy tylko te widoczne, więc ograniczenie animacji do widocznego fragmentu listy nic nie kosztuje wizualnie.
Gesty i przeciąganie
Biblioteka obsługuje gesty jako właściwości elementu, więc najechanie, naciśnięcie i przeciąganie nie wymagają obsługi zdarzeń wskaźnika.
<motion.div
whileHover={{ scale: 1.03 }}
whileTap={{ scale: 0.97 }}
drag="x"
dragConstraints={{ left: -120, right: 0 }}
onDragEnd={(_, info) => {
if (info.offset.x < -80) usun()
}}
/>Ten fragment to gotowy wzorzec przesunięcia w bok, znany z aplikacji mobilnych. Ograniczenia wyznaczają zakres ruchu, a informacja o zakończeniu przeciągania niesie przesunięcie i prędkość, więc decyzję o wykonaniu akcji można oprzeć na jednym z nich.
Warto uwzględnić prędkość, a nie tylko przesunięcie. Szybki, krótki ruch zwykle oznacza zamiar wykonania akcji, więc próg oparty wyłącznie na odległości sprawia, że interfejs wydaje się oporny.
Przy gestach na urządzeniach dotykowych pamiętaj o kolizji z przewijaniem strony. Przeciąganie w pionie na liście, którą użytkownik chce przewinąć, kończy się tym, że żadna z tych rzeczy nie działa dobrze, więc ograniczaj przeciąganie do jednej osi.
Animacje przy przewijaniu
Osobny zestaw mechanizmów obsługuje sytuacje, w których animacja zależy od pozycji przewijania. Najprostszy wariant uruchamia animację, gdy element pojawi się w polu widzenia.
<motion.section
initial={{ opacity: 0, y: 24 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, amount: 0.3 }}
>
{tresc}
</motion.section>Ustawienie jednorazowego uruchomienia jest ważniejsze, niż wygląda. Bez niego sekcja animuje się przy każdym przewinięciu w tę i z powrotem, co przy dłuższej stronie zamienia się w migotanie.
Drugi wariant wiąże wartość animowaną wprost z postępem przewijania, co pozwala budować paski postępu i efekty paralaksy. Tu obowiązuje ta sama zasada wydajności: wiąż postęp z przezroczystością albo przekształceniem, nigdy z wysokością elementu.
Przy stronach z dużą liczbą takich sekcji warto sprawdzić wynik na słabszym urządzeniu. Efekt, który na komputerze wygląda płynnie, na telefonie sprzed trzech lat potrafi zacinać się na tyle, że lepiej z niego zrezygnować.
Dostępność
Część osób ma w systemie włączoną preferencję ograniczenia ruchu i to nie jest kwestia gustu, tylko czasem zdrowia. Biblioteka udostępnia hak odczytujący to ustawienie.
import { useReducedMotion } from 'motion/react'
const ograniczRuch = useReducedMotion()
<motion.div
initial={{ opacity: 0, y: ograniczRuch ? 0 : 12 }}
animate={{ opacity: 1, y: 0 }}
/>Właściwe podejście nie polega na wyłączeniu wszystkich animacji, tylko na zastąpieniu ruchu zmianą przezroczystości. Element nadal pojawia się płynnie, ale nie przesuwa się po ekranie.
Osobno warto pamiętać o tym, żeby animacja nie blokowała interakcji. Przejście trwające sekundę wygląda efektownie przy pierwszym użyciu i irytuje przy pięćdziesiątym, więc przy elementach używanych często trzymaj się zakresu stu pięćdziesięciu do dwustu pięćdziesięciu milisekund.
Migracja ze starej paczki
Przejście z poprzedniej nazwy jest proste i w większości projektów nie wymaga zmian w kodzie animacji.
pnpm remove framer-motion
pnpm add motionZmieniasz importy z poprzedniej nazwy na motion/react i to zwykle wszystko. Dwunasta wersja nie wprowadziła zmian łamiących zgodność w części reactowej, więc kod pisany wcześniej działa dalej.
Warto przy okazji sprawdzić dwie rzeczy. Pierwsza to zgodność z wersją Reacta, bo nowsze wydania biblioteki są dostosowane do renderowania współbieżnego. Druga to komponenty animowane w bibliotekach zewnętrznych, które mogą nadal ciągnąć starą paczkę, przez co w projekcie znajdą się obie naraz.
Sprawdzenie drugiej rzeczy zajmuje jedno polecenie menedżera pakietów wypisujące, co ciągnie starą nazwę. Jeśli jest to zależność, której nie kontrolujesz, zwykle wystarczy poczekać na jej aktualizację, bo dwie wersje działające obok siebie nie powodują błędów, tylko niepotrzebnie powiększają paczkę.
Osobno warto przejrzeć własne komponenty opakowujące animacje. W projektach, które przeszły przez kilka wersji, zwykle znajdzie się kilka miejsc z ustawieniami dobranymi doraźnie, a przy okazji migracji łatwiej ujednolicić czasy trwania i krzywe niż wracać do tego osobno.
Gdzie animacja ma sens, a gdzie przeszkadza
Biblioteka daje możliwości, których łatwo użyć za dużo. Animacja ma dwa zadania: wyjaśnić, co się właśnie stało, i pokazać związek między dwoma stanami. Wszystko poza tym jest ozdobą, która kosztuje czas użytkownika.
Dobre zastosowania są przewidywalne. Element dochodzący do listy powinien się pojawić, a nie mignąć, bo inaczej użytkownik nie wie, czy coś się dodało. Panel wysuwający się z boku pokazuje, skąd przyszedł, więc zamknięcie go jest oczywiste. Sortowanie z przesunięciem pozwala śledzić wzrokiem element, który zmienił pozycję.
Złe zastosowania też są przewidywalne. Animacja na elemencie używanym dziesiątki razy dziennie zamienia się w opóźnienie. Efekt przy przewijaniu na stronie z treścią odciąga uwagę od czytania. Przejście trwające sekundę przy otwieraniu okna modalnego sprawia, że aplikacja wydaje się wolna, nawet jeśli reszta działa błyskawicznie.
Prosty sprawdzian: wyłącz animację i sprawdź, czy interfejs stał się niezrozumiały. Jeśli tak, animacja niesie informację i warto ją zostawić. Jeśli zrobiło się tylko mniej efektownie, można ją usunąć bez straty.
W projekcie zbudowanym na gotowej bibliotece komponentów, na przykład HeroUI albo daisyUI, część animacji już istnieje. Dokładanie drugiej warstwy na tych samych elementach kończy się zwykle podwójnym przejściem, którego nikt nie zaplanował.
Motion kontra alternatywy
| Rozwiązanie | Mocna strona | Słabość | Kiedy wybrać |
|---|---|---|---|
| Motion | Animacje wyjścia i układu, gesty, warianty | Rozmiar paczki, koszt przy wielu elementach | Aplikacja w Reakcie z animacjami interfejsu |
| Przejścia CSS | Zero kosztu, natywne | Brak animacji wyjścia i układu | Proste najechania i zmiany stanu |
| GSAP | Sekwencje i sterowanie osią czasu | Poza modelem Reacta | Złożone animacje narracyjne |
| Tailwind CSS z klasami | Bez dodatkowej biblioteki | Ograniczone możliwości | Drobne przejścia w istniejącym projekcie |
Drugi wiersz warto rozważyć poważnie, zanim sięgniesz po bibliotekę. Najechanie kursorem, zmiana koloru i płynne pojawienie się elementu, który nie znika z drzewa, to zadania dla czystego CSS. Biblioteka zaczyna się opłacać przy animacjach wyjścia, zmianach układu i gestach.
Poza tą tabelą leży przypadek, w którym wynikiem ma być plik, a nie ruch na stronie. Remotion renderuje komponenty Reacta do wideo, więc te same umiejętności dają nagranie, które da się wysłać albo wstawić w serwisie społecznościowym. Ma to dwa koszty: renderowanie trwa i wymaga mocy obliczeniowej, a licencja zależy od wielkości firmy i jest bezpłatna tylko do trzech osób w organizacji.
Typowe błędy
Pierwszy to brak klucza przy elemencie w opakowaniu obsługującym wyjście. Animacja po prostu nie działa, a nic o tym nie informuje.
Drugi to animowanie szerokości i wysokości zamiast skalowania. Efekt wygląda tak samo, a koszt jest wielokrotnie wyższy, bo każda klatka wymusza przeliczenie układu.
Trzeci to animacje trwające zbyt długo. To, co przy pierwszym uruchomieniu robi wrażenie, przy codziennym używaniu spowalnia pracę.
Czwarty to ignorowanie preferencji ograniczenia ruchu. To ustawienie systemowe wybierane świadomie i warto je uszanować, zastępując ruch zmianą przezroczystości.
Piąty to animacja układu na długiej liście. Przy setkach elementów przeglądarka nie nadąża, a użytkownik widzi zacięcia zamiast płynności.
Szósty to trzymanie dwóch wersji biblioteki naraz po niepełnej migracji. Stara paczka ciągnięta przez zależność zewnętrzną podwaja rozmiar i potrafi powodować trudne do wyjaśnienia zachowania.
FAQ
Czy Framer Motion to to samo co Motion?
Tak, to ta sama biblioteka po zmianie nazwy i usamodzielnieniu się od firmy, w której powstała. Paczka nazywa się teraz motion, a import w Reakcie prowadzi przez motion/react. Interfejs pozostał zgodny, więc migracja to zwykle podmiana nazwy i ścieżek importu.
Czy warto używać jej zamiast CSS?
Zależy od zadania. Przejścia przy najechaniu i zmianie stanu obsłuż czystym CSS, bo nic nie kosztuje. Bibliotekę bierz do animacji wyjścia, zmian układu i gestów, czyli rzeczy, których w CSS nie da się zrobić bez sztuczek.
Jak to wpływa na rozmiar paczki?
Import komponentu animowanego dołącza silnik animacji, co przy jednej animowanej stronie warto załadować dynamicznie. Biblioteka udostępnia też lżejszy wariant oparty na natywnym interfejsie przeglądarki, wystarczający do prostych przejść bez gestów i animacji układu.
Czy działa z Next.js?
Tak, przy czym komponenty animowane wymagają dyrektywy klienta, bo używają stanu i efektów. W Next.js z routerem aplikacji oznacza to wydzielenie animowanego fragmentu do osobnego komponentu klienckiego, zamiast oznaczania całej strony.
Jak długo powinna trwać animacja interfejsu?
Zakres od stu pięćdziesięciu do dwustu pięćdziesięciu milisekund sprawdza się przy większości przejść. Krótsza bywa niezauważalna, dłuższa zaczyna spowalniać pracę przy elementach używanych wielokrotnie w ciągu dnia.
Dokumentacja stoi na stronie projektu, a opis zmian znajdziesz w dzienniku zmian.