Mundarija (34)
- Bu darsda
- 1. Nega bu kerak?
- 2. Holat o'zgarishi: className va data-*
- 2.1 Transition — CSS'dagidek
- 2.2 onTransitionEnd va onAnimationEnd
- 3. Kirish: element paydo bo'lganda
- 3.1 Muammo: transition'ga "oldingi holat" kerak
- 3.2 @keyframes — paydo bo'lganda o'zi ishlaydi
- 3.3 @starting-style — transition uchun "oldingi holat"
- 3.4 Animatsiyani qayta boshlash: key
- 4. Chiqish: React o'chirishdan oldin
- 4.1 Avval animatsiya, keyin o'chirish
- 4.2 <dialog> va popover: allow-discrete
- 5. Radix va Tailwind: data-state bilan animatsiya
- 5.1 Radix chiqishni kutadi
- 5.2 Kanon topgan tuzoq: kirish paytida Esc
- 5.3 Tailwind'da: data-variant va tw-animate-css
- 6. CSS yoki JavaScript?
- 6.1 Qaror
- 6.2 Kanonning tanlovi
- 7. Vazifalar qadami: dialog, menyu va tooltip animatsiyasi (holat 22)
- 7.1 Tasdiq oynasi: kirish VA chiqish
- 7.2 Topilma: ::backdrop va reduced-motion
- 7.3 Menyu va tooltip: faqat kirish
- 7.4 Tekshiruvlar va commit
- 8. Ko'p uchraydigan xatolar
- 9. Mashqlar
- 1-mashq (oson): qaysi vosita?
- 2-mashq (o'rta): xato xabari "titrasin"
- 3-mashq (qiyin): qayta ishlatiladigan useExitAnimation
- 4-mashq: Vazifalar qadami
- 10. Real ishda
- Xulosa
- Manbalar
CSS animatsiyalari React'da: transition, keyframes, data-state va @starting-style bilan kirish-chiqish
Qisqacha: React'da ko'p animatsiyaga CSS yetadi. Holat o'zgarsa —
classNameyokidata-*atributi almashadi,transitionqolganini qiladi. Element paydo bo'lganda transition ishlamaydi — buning uchun@keyframesyoki@starting-style. Element yo'qolganda esa React uni darhol o'chiradi: chiqishni kutish uchunanimationend(Radix'ningdata-state="closed"i shunday ishlaydi).<dialog>va popover uchun esatransition-behavior: allow-discreteyetadi —displayham animatsiya oxirigacha kechikadi. Qolgan murakkab holatlar — Motion.
Bu darsda
classNamevadata-*bilan transition qilasiz va nega yangi element "animatsiyasiz" paydo bo'lishini tushuntirasiz.- Kirish animatsiyasini
@keyframesva@starting-stylebilan, qayta ishga tushirishnikeybilan qilasiz. - Chiqish animatsiyasini React'da to'g'ri qurasiz: avval animatsiya, keyin o'chirish.
- Radix
data-stateva Tailwind (data-[state=open]:, tw-animate-css) bilan menyuni jonlantirasiz. vazifalardagi tasdiq oynasi, menyu va tooltip'ga kirish-chiqish animatsiyasini qo'shib, commit qilasiz.
Oldin bilishingiz kerak: UI animatsiya tamoyillari va performance, Radix Primitives chuqur, @keyframes va animation xususiyatlari, Kirish va chiqish animatsiyalari: @starting-style.
1. Nega bu kerak?
Oldingi darsda qoidalarni o'rgandik: 150–300 ms, ease-out, faqat transform va opacity. Endi ularni React komponentlarida qo'llaymiz.
Sardor CSS qismidan transition'ni yaxshi biladi. React'da esa ikki joyda qoqildi. Birinchisi: filtr paneli {open && <Panel />} bilan chiqadi va transition yozilgan bo'lsa ham "sakrab" paydo bo'ladi. Ikkinchisi: toast yopilganda chiqish animatsiyasi umuman ko'rinmaydi — toast shunchaki yo'qoladi.
Ikkala muammo bitta sababdan: React elementni DOM'ga qo'yadi va olib tashlaydi. CSS transition esa "bir holatdan ikkinchisiga" ishlaydi. Element hali yo'q bo'lsa, "oldingi holat" ham yo'q. Element yo'q bo'lsa, "keyingi holat" ni chizadigan narsa ham yo'q.
Hayotiy o'xshatish: teatr pardasi. Aktyor sahnaga chiqishi uchun parda ochiladi. Lekin aktyor sahnaning orqasida turmasa — u to'satdan "paydo bo'ladi". Ketishda ham: sahnadan chiqib ketishiga vaqt berish kerak, uni bir zumda yo'q qilib bo'lmaydi.
2. Holat o'zgarishi: className va data-*
2.1 Transition — CSS'dagidek
Element joyida turadi, faqat holati o'zgaradi. Bu CSS transition'ning eng qulay ishi. React holatni sinf yoki atributga aylantiradi, qolganini CSS qiladi:
import { useState } from "react";
const css = `
.star {
border: 1px solid #bbb;
border-radius: 8px;
background: #fff;
padding: 8px 12px;
transition: scale 150ms ease-out, background-color 150ms;
}
.star[aria-pressed="true"] {
scale: 1.05;
background-color: #fde68a;
}
`;
export default function App() {
const [fav, setFav] = useState(false);
return (
<div>
<style>{css}</style>
<button
className="star"
aria-pressed={fav}
onClick={() => setFav(!fav)}
>
Sevimli
</button>
<p>Osh {fav ? "sevimlilarda" : "sevimlilarda emas"}</p>
</div>
);
}Ekranda:
Sevimli
Osh sevimlilarda«Ishga tushir» dan keyin tugmani bosing — u bir oz kattalashib sariq bo'ladi, qayta bosing — joyiga qaytadi. Uslub uchun alohida sinf emas, aria-pressed atributini ishlatdik. Bu atribut Malika uchun "bosilgan" holatini ham aytadi. Bitta haqiqat — ham a11y, ham ko'rinish (React'da accessibility darsidagi g'oya).
scale — transform: scale() ning alohida xususiyati: kompozitorda bajariladi. background-color — bo'yash bosqichi, lekin kichik tugmada arzon.
2.2 onTransitionEnd va onAnimationEnd
React DOM'ning animatsiya hodisalarini ham beradi: onTransitionEnd, onAnimationStart, onAnimationEnd, onAnimationIteration. Ular "animatsiya tugadi" deb xabar beradi — chiqish animatsiyasida aynan shu kerak bo'ladi.
Ikki tuzoq bor:
- Hodisa ko'tariladi (bubbling). Ichki elementning animatsiyasi tugasa ham, ota-elementning
onAnimationEndi chaqiriladi. Kerak bo'lsae.target === e.currentTargetni tekshiring. - Har xususiyat uchun alohida.
transition: opacity, scale— ikkitatransitionend.e.propertyNameqaysi biri ekanini aytadi.
3. Kirish: element paydo bo'lganda
3.1 Muammo: transition'ga "oldingi holat" kerak
{open && <div className="panel">…</div>} — open true bo'lganda div DOM'ga birinchi marta qo'yiladi. Brauzer uni darhol yakuniy uslubda chizadi. opacity: 0 dan 1 ga o'tish uchun "oldin 0 edi" degan holat yo'q. Shuning uchun transition ishlamaydi.
Uchta yechim bor.
3.2 @keyframes — paydo bo'lganda o'zi ishlaydi
animation elementga qo'yilgan zahoti boshlanadi. Element DOM'ga kirdimi — animatsiya bo'ladi:
@keyframes kirish {
from { opacity: 0; translate: 0 8px; }
}
.panel {
animation: kirish 200ms ease-out;
}to yozilmagan — yakuniy qiymat elementning oddiy uslubi. Kanon ham shu qisqa shaklni ishlatadi.
3.3 @starting-style — transition uchun "oldingi holat"
Kirish va chiqish animatsiyalari darsida o'rgangan @starting-style aynan shu muammo uchun yaratilgan: "element birinchi marta chizilganda shu qiymatlardan boshla". React qo'ygan element uchun ham ishlaydi:
import { useState } from "react";
const css = `
.panel {
padding: 8px;
border: 1px solid #bbb;
border-radius: 8px;
transition: opacity 200ms ease-out, translate 200ms ease-out;
}
@starting-style {
.panel { opacity: 0; translate: 0 8px; }
}
`;
export default function App() {
const [open, setOpen] = useState(false);
return (
<div>
<style>{css}</style>
<button onClick={() => setOpen(!open)}>Filtr</button>
{open && <div className="panel">Faqat bajarilmaganlar</div>}
</div>
);
}Ekranda:
Filtr
Faqat bajarilmaganlarNatija oynasida «Filtr» ni bosing — panel pastdan ko'tarilib, so'nib chiqadi. JavaScript'da hech qanday animatsiya kodi yo'q.
@keyframes va @starting-style farqi nimada? Transition to'xtatilishi mumkin: panel chiqayotganda yana bossangiz, joriy qiymatdan orqaga qaytadi. Keyframe animatsiyasi esa o'z yo'lini oxirigacha bosadi. Tez-tez ochib-yopiladigan narsaga transition yaxshiroq.
Uchinchi, eski usul: element avval "yopiq" sinf bilan qo'yiladi, keyingi kadrda (requestAnimationFrame yoki effekt) "ochiq" sinf beriladi. Eski kodda uchratasiz. Bugun @starting-style ning o'zi yetadi. Lekin u yangi: Baseline'ga 2024-yil avgustda kirgan, eski brauzerlarda yo'q (caniuse da tekshiring). U yerda element shunchaki animatsiyasiz paydo bo'ladi — hech narsa buzilmaydi.
3.4 Animatsiyani qayta boshlash: key
Savatga taom qo'shilganda raqam belgisi bir marta "lip" etsin. Keyframe animatsiyasi esa faqat element yangi paydo bo'lganda ishlaydi. Raqam 2 dan 3 ga o'zgarsa, element o'sha-o'sha — animatsiya qaytmaydi.
State'ning saqlanishi va key bilan reset darsidan bilasiz: key o'zgarsa, React eski elementni olib tashlab, yangisini yasaydi. Buni ataylab ishlatamiz:
import { useEffect, useState } from "react";
const css = `
@keyframes lip { 50% { scale: 1.3; } }
.badge { display: inline-block; animation: lip 200ms ease-out; }
`;
function Badge({ count }) {
useEffect(() => {
console.log("badge chizildi:", count);
}, []);
return <span className="badge">{count}</span>;
}
export default function App() {
const [count, setCount] = useState(0);
return (
<div>
<style>{css}</style>
<button onClick={() => setCount((c) => c + 1)}>
Qo'shish
</button>{" "}
Savat: <Badge key={count} count={count} />
</div>
);
}Konsolda:
badge chizildi: 0
badge chizildi: 1
badge chizildi: 2
badge chizildi: 3Ekranda:
Qo'shish Savat: 3Effekt bo'sh bog'liqlik bilan faqat yangi komponent paydo bo'lganda ishlaydi. Konsolda to'rtta yozuv — har bosishda belgi yangidan yasaldi va animatsiya qaytadan boshlandi. display: inline-block nega? scale oddiy <span> ga (inline element) ta'sir qilmaydi.
Narxi: key almashganda butun komponent va uning ichidagi state yo'qoladi. Kichik belgi uchun muammo emas. Katta komponentni shunday "qayta tug'dirmang".
Tekshirib ko'ring: Sardor menyu ro'yxatiga
transition: opacity 0.2svaopacity: 1yozdi. Ro'yxat{open && …}bilan chiqadi. Animatsiya ko'rinmadi. Eng kam o'zgarish bilan qanday tuzatasiz?
Javob
@starting-style { .menu { opacity: 0; } } qo'shish yetadi. Element birinchi chizilganda brauzer opacity: 0 dan boshlaydi va transition 1 gacha olib boradi. Yoki transition o'rniga animation: kirish 0.2s + @keyframes kirish { from { opacity: 0 } }. Ikkalasi ham JavaScript'siz.
4. Chiqish: React o'chirishdan oldin
4.1 Avval animatsiya, keyin o'chirish
setOpen(false) — React elementni darhol DOM'dan olib tashlaydi. Chiqish animatsiyasi uchun tartibni o'zgartirish kerak:
- Holatni "chiqmoqda" ga o'tkazamiz — sinf almashadi va CSS chiqish animatsiyasini boshlaydi.
- Animatsiya tugaganda (
onAnimationEnd) — haqiqiy o'chirish. - Zaxira taymer: hodisa kelmasa ham (element yashirin, animatsiya bekor qilindi) — baribir o'chirish.
import { useRef, useState } from "react";
const css = `
@keyframes kirish { from { opacity: 0; translate: 0 8px; } }
@keyframes chiqish { to { opacity: 0; translate: 0 8px; } }
.toast { animation: kirish 200ms ease-out; }
.toast.chiqmoqda { animation: chiqish 150ms ease-in forwards; }
`;
export default function App() {
const [open, setOpen] = useState(false);
const [leaving, setLeaving] = useState(false);
const fallback = useRef();
function finish() {
clearTimeout(fallback.current);
setLeaving(false);
setOpen(false);
}
function close() {
setLeaving(true); // avval chiqish animatsiyasi
fallback.current = setTimeout(finish, 300); // zaxira
}
const status = !open ? "yopiq" : leaving ? "chiqmoqda" : "ochiq";
return (
<div>
<style>{css}</style>
<button onClick={() => setOpen(true)}>Saqlash</button>{" "}
<button onClick={close}>Yopish</button>
{open && (
<p
className={leaving ? "toast chiqmoqda" : "toast"}
onAnimationEnd={() => leaving && finish()}
>
Saqlandi
</p>
)}
<p>Holat: {status}</p>
</div>
);
}Ekranda:
Saqlash Yopish
Holat: yopiqekran("Holat: chiqmoqda") — oraliq nuqtani tekshirdi: «Yopish» dan keyin toast hali DOM'da, holat "chiqmoqda". Natija oynasida toast 150 ms da pastga so'nib ketadi va onAnimationEnd uni o'chiradi. Vaqt bo'yicha:
| Qadam | Holat | Ekranda |
|---|---|---|
| «Saqlash» bosildi | open, kirish 200 ms |
"Saqlandi" pastdan chiqadi |
| «Yopish» bosildi | leaving, chiqish 150 ms |
"Saqlandi" so'nmoqda, DOM'da bor |
animationend (yoki 300 ms) |
finish() — yopiq |
toast yo'q, "Holat: yopiq" |
Muhim detallar:
forwards— animatsiya tugagach oxirgi kadrda qoladi. Aks holda React o'chirguncha bir kadr davomida toast to'liq ko'rinib, "miltillaydi".- Zaxira taymer — fon tabda yoki element yashirilgan bo'lsa, brauzer animatsiyani o'tkazib yuborishi va hodisa kechikishi mumkin. Kanon ham o'chirish animatsiyasini ko'pi bilan 300 ms kutadi (Motion I darsida ko'ramiz).
- Reduced-motion — global qoida animatsiyani 0.01 ms qiladi, hodisa darhol keladi. Agar
animation: nonebo'lsa, hodisa umuman kelmaydi va faqat zaxira taymer qutqaradi.
Bu kod — Motion'ning AnimatePresence komponenti ichida qiladigan ishning eng sodda shakli. Motion III darsida uni kutubxona bilan qilamiz va uning ro'yxatdagi tuzog'ini o'lchaymiz.
4.2 <dialog> va popover: allow-discrete
<dialog> va popover da vaziyat boshqacha: element DOM'da qoladi, faqat yopilganda display: none bo'ladi. display esa "diskret" xususiyat — oraliq qiymati yo'q (block va none orasida "yarim" yo'q). Shuning uchun u sukut bo'yicha darhol almashadi va chiqish animatsiyasi ko'rinmay qoladi.
transition-behavior: allow-discrete (qisqa yozuvda display 0.2s allow-discrete) — "diskret xususiyatni ham o'tishga qo'sh". Brauzer display: none ni o'tish oxirigacha kechiktiradi. overlay — top layer'dan chiqish ham kechiksin (Top layer darsida). Kirish uchun esa yana @starting-style.
JavaScript'siz to'liq misol — HTML'ning o'z popovertarget atributi bilan:
<style>
body { font-family: system-ui, sans-serif; }
[popover] {
padding: 0.75rem 1rem;
border: 1px solid #bbb;
border-radius: 8px;
opacity: 0;
translate: 0 0.5rem;
transition:
opacity 0.2s ease-out,
translate 0.2s ease-out,
overlay 0.2s allow-discrete,
display 0.2s allow-discrete;
}
[popover]:popover-open {
opacity: 1;
translate: 0 0;
}
@starting-style {
[popover]:popover-open { opacity: 0; translate: 0 0.5rem; }
}
@media (prefers-reduced-motion: reduce) {
[popover] { transition: none; }
}
</style>
<button popovertarget="menyu">Bugungi menyu</button>
<div id="menyu" popover>
<p>Osh — 35 000 so'm</p>
<p>Manti — 30 000 so'm</p>
<button popovertarget="menyu" popovertargetaction="hide">
Yopish
</button>
</div>Tugmani bosing — menyu pastdan chiqadi. Tashqariga bosing, Esc yoki «Yopish» — u so'nib ketadi va keyin yo'qoladi. Uchta qism birga ishladi:
| Qism | Nima qiladi |
|---|---|
:popover-open { opacity: 1 } |
ochiq holat |
@starting-style |
kirish boshlanadigan qiymat |
display … allow-discrete |
yopilganda display: none o'tish oxirigacha kechikadi |
React'da <dialog> (showModal()) bilan xuddi shu CSS ishlaydi. vazifalar dagi tasdiq oynasi bugun shunday bo'ladi.
Tekshirib ko'ring: Toast misolida
forwardsni olib tashlasak, chiqish oxirida nima ko'rinadi?
Javob
Animatsiya tugashi bilan element @keyframes siz uslubiga — to'liq ko'rinadigan holatga — qaytadi. onAnimationEnd dan keyin React uni o'chirguncha bir kadr o'tadi. Shu bir kadrda toast yana paydo bo'lib, keyin yo'qoladi — ko'zga "miltillash" bo'lib ko'rinadi. forwards oxirgi kadrni (opacity: 0) ushlab turadi.
5. Radix va Tailwind: data-state bilan animatsiya
5.1 Radix chiqishni kutadi
Radix Primitives chuqur darsida data-state="open"/"closed" bilan stil berdik. Animatsiya ham shu atributga bog'lanadi. Radix'da yana bir yordam bor: ichki Presence komponenti. Menyu yopilganda Radix data-state="closed" qo'yadi va qaraydi: elementda animatsiya bormi? Bo'lsa — animationend ni kutadi, keyin olib tashlaydi. Ya'ni «Avval animatsiya, keyin o'chirish» bo'limidagi ish Radix'da tayyor:
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
const css = `
@keyframes kirish { from { opacity: 0; scale: 0.96; } }
@keyframes chiqish { to { opacity: 0; scale: 0.96; } }
.menu {
padding: 4px;
border: 1px solid #bbb;
border-radius: 8px;
background: #fff;
transform-origin:
var(--radix-dropdown-menu-content-transform-origin);
}
.menu[data-state="open"] { animation: kirish 120ms ease-out; }
.menu[data-state="closed"] { animation: chiqish 100ms ease-in; }
`;
export default function App() {
return (
<div>
<style>{css}</style>
<DropdownMenu.Root modal={false}>
<DropdownMenu.Trigger>Hisobot</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content className="menu" align="start">
<DropdownMenu.Item>CSV</DropdownMenu.Item>
<DropdownMenu.Item>JSON</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
</div>
);
}Ekranda:
HisobotMenyu ochildi va Esc bilan yopildi — oxirida ekranda faqat tugma. Natija oynasida o'zingiz bosing: menyu tugmadan "o'sib" chiqadi va yopilganda 100 ms so'nadi. --radix-dropdown-menu-content-transform-origin — Radix bergan "o'sish nuqtasi": menyu qaysi tomonda ochilsa, o'sha tomondagi tugmadan o'sadi.
5.2 Kanon topgan tuzoq: kirish paytida Esc
vazifalar da aynan shunday qilinganda (kirish ham, chiqish ham) e2e tekshiruv yiqildi: menyu ochila boshlagan zahoti Esc bosilsa, menyu yopildi, lekin fokus tugmaga qaytmadi — bandda qoldi. Sabab: kirish animatsiyasi paytida Esc bosilsa, Radix yopishni animatsiya tugashini (animationend) kutib kechiktirardi. Shu orada fokus bandda qolib ketdi:
| Qadam | Fokus / Holat | Ekranda |
|---|---|---|
| menyu klaviaturadan ochildi | fokus bandda, kirish (120 ms) boshlandi | menyu o'sib chiqmoqda |
| darhol Esc | data-state="closed", Radix animationend ni kutadi |
menyu hali ko'rinadi |
| animatsiya tugadi | menyu yo'q, fokus bandda qolgan | Bobur fokusni yo'qotdi |
Kanon yechimi — animatsiya faqat data-state="open" da. Yopilganda animatsiya nomi none bo'ladi, Radix kutmaydi va menyuni darhol olib tashlaydi. Menyu kuniga yuz marta ochiladi va yopilganda unga hech kim qaramaydi. Chiqish animatsiyasidan voz kechish — arzon narx.
Bu e2e tekshiruvlarsiz topilmasdi: ko'z bilan sinab ko'rganda hammasi chiroyli edi.
5.3 Tailwind'da: data-variant va tw-animate-css
Tailwind v4 da (Tailwind CSS 2: variantlar) xuddi shu qoidalar:
<DropdownMenuContent
className="origin-(--radix-dropdown-menu-content-transform-origin)
data-[state=open]:motion-safe:animate-kirish"
/>data-[state=open]:—[data-state="open"]selektori.motion-safe:— faqatprefers-reduced-motion: no-preferenceda. Teskarisi —motion-reduce:.animate-kirish—@themedagi--animate-kirishtokenidan (Tailwind CSS 3 darsida).
shadcn komponentlarida esa boshqa yozuvni ko'rasiz:
"data-[state=open]:animate-in data-[state=closed]:animate-out
data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0
data-[state=open]:zoom-in-95"Bu sinflar tw-animate-css paketidan: animate-in/animate-out — umumiy kirish/chiqish animatsiyasi, fade-in-0, zoom-in-95, slide-in-from-top-2 — uning parametrlari. Tailwind v3 davrida bu ish tailwindcss-animate plagini bilan qilinardi. v4 da shadcn shu paketga o'tdi va CSS'da @import "tw-animate-css" qatori qo'shiladi.
Kanon tw-animate-css'ni olmadi. Bitta 120 ms lik kirish uchun @theme dagi 4 qatorlik token yetdi. Chiqish esa yuqoridagi sabab bilan ataylab yo'q.
6. CSS yoki JavaScript?
6.1 Qaror
| CSS yetadi | JavaScript (Motion) kerak |
|---|---|
| hover, focus, bosish javobi | to'xtatilganda tezlikni saqlash (spring) |
| ochish/yopish: menyu, tooltip | sudrash, surib tashlash (gesture) |
kirish: @keyframes, @starting-style |
ro'yxatda qatorlar joyini almashtirish (layout) |
<dialog>/popover chiqishi: allow-discrete |
ixtiyoriy React elementining chiqishi |
| loader, skeleton | ketma-ketlik va orkestratsiya ma'lumotga bog'liq |
flowchart TD
A{"Sudrash, spring yoki joy almashishi?"} -->|ha| M["Motion"]
A -->|yo'q| B{"Element yo'qoladimi?"}
B -->|"dialog, popover, Radix"| C["CSS: allow-discrete / data-state"]
B -->|"oddiy React elementi"| D["CSS + onAnimationEnd yoki Motion"]
B -->|yo'q| E["CSS: transition, keyframes"]Sxemada nimaga qarang: ko'p yo'l CSS'ga olib boradi. CSS animatsiyasining afzalliklari: 0 KB JavaScript, kompozitorda ishlaydi (asosiy oqim band bo'lsa ham), CSP bilan muammosiz (.css faylda).
6.2 Kanonning tanlovi
vazifalar da shu qoida qo'llandi. Tasdiq oynasi, menyu, tooltip va toast — CSS. Faqat ro'yxatning yangi qatori — Motion (keyingi darslarda). O'chirilayotgan qator chiqishi esa brauzerning Web Animations API'si bilan (Web Animations API).
Tekshirib ko'ring: "Yangi vazifa ro'yxatning tepasiga qo'shilsa, qolgan qatorlar silliq pastga sursin" — CSS bilan bo'ladimi?
Javob
Oddiy CSS bilan — yo'q. Yangi qatorning o'zini @keyframes bilan jonlantirish mumkin. Lekin qolgan qatorlarning joyi layout natijasida o'zgaradi va ularning "oldingi joyi" CSS'ga noma'lum. Buning uchun FLIP kerak: eski joyni o'lchash, yangi joyga qo'yish, farqni transform bilan orqaga qaytarib, nolga animatsiya qilish. Motion'ning layout props'i shuni qiladi (Motion III). View Transitions API ham bunga yaqin imkon beradi (View Transitions darsida).
7. Vazifalar qadami: dialog, menyu va tooltip animatsiyasi (holat 22)
7.1 Tasdiq oynasi: kirish VA chiqish
Oldingi darsda feat/css-animatsiya branch'ini ochib, toast'ni tayyorlagan edik. Endi tasdiq oynasi. 13-qismdan beri u faqat @keyframes bilan kirardi, yopilganda esa darhol yo'qolardi. asos.css (kanon, holat 22):
-dialog[open] { animation: dialog-kirish 0.15s ease-out; }
-@keyframes dialog-kirish {
- from { opacity: 0; transform: translateY(0.5rem); }
+/* 20/#22: kirish VA chiqish — CSS o'tishi (13/#09 dagi @keyframes
+ faqat kirishni ko'rsatardi: yopilganda oyna darhol yo'qolardi).
+ Yopiq <dialog> — display: none; display va overlay (top layer)
+ "allow-discrete" bilan o'tish oxirigacha kechikadi.
+ @starting-style — ochilishdagi boshlang'ich qiymat (display: none
+ dan keyin birinchi kadr). Faqat opacity va transform — kompozitor
+ (layout yo'q) */
+dialog {
+ opacity: 0;
+ transform: translateY(0.5rem);
+ transition:
+ opacity 0.15s ease-out,
+ transform 0.15s ease-out,
+ overlay 0.15s allow-discrete,
+ display 0.15s allow-discrete;
+}
+dialog[open] { opacity: 1; transform: none; }
+@starting-style {
+ dialog[open] { opacity: 0; transform: translateY(0.5rem); }
+}
+dialog::backdrop {
+ transition:
+ background-color 0.15s,
+ overlay 0.15s allow-discrete,
+ display 0.15s allow-discrete;
+}
+@starting-style {
+ dialog[open]::backdrop { background-color: transparent; }
}(Kanon izohi telefonga sig'ishi uchun qatorlarga qayta bo'lingan, mazmuni o'sha.) Popover misolidagi uchlik: yopiq holat, [open] holat, @starting-style. Fon (::backdrop) ham alohida o'tadi: qora parda so'nib paydo bo'ladi.
7.2 Topilma: ::backdrop va reduced-motion
Global reduced-motion qoidasi *, ::before, ::after uchun yozilgan edi. ::backdrop bu ro'yxatga kirmaydi — fon o'tishi harakatni kamaytirgan foydalanuvchida ham 0.15 s qoldi. Birinchi tuzatish — ro'yxatga ::backdrop ni qo'shish — ishlamadi. Build'dagi CSS optimallashtiruvchi uni jimgina tashlab yubordi. e2e tekshiruvi buni ushladi. Yechim — alohida qoida:
+ /* 20/#22: ::backdrop — "*" ga kirmaydi (dialog foni ham o'tadi).
+ ALOHIDA qoida: ro'yxatda (*, ::before, ::after, ::backdrop)
+ bo'lsa, build'dagi CSS optimallashtiruvchi ::backdrop ni
+ jimgina tashlab yubordi (o'lchandi, e2e-20: fon o'tishi
+ reduced-motion'da ham 0.15s qoldi) */
+ ::backdrop {
+ transition-duration: 0.01ms !important;
+ animation-duration: 0.01ms !important;
+ animation-iteration-count: 1 !important;
+ }Saboq: CSS'ni ko'z bilan emas, build natijasida tekshiring. Dev rejimda ishlagan qoida production'da yo'q bo'lishi mumkin.
7.3 Menyu va tooltip: faqat kirish
vazifalar.css dagi @theme ga yangi animatsiya tokeni:
+ /* 20/#22: menyu va tooltip ochilishi — qisqa (120 ms),
+ faqat opacity va scale. Chiqish animatsiyasi yo'q: Radix
+ yopilganda elementni darhol olib tashlaydi (animatsiya
+ bo'lsa, animationend kutadi) */
+ --animate-kirish: kirish 0.12s ease-out;
+ @keyframes kirish {
+ from { opacity: 0; scale: 0.96; }
+ }ui/dropdown-menu.tsx dagi DropdownMenuContent sinflariga ikki narsa qo'shildi — o'sish nuqtasi va kirish (kanon qatori uzun, faqat yangi qismlari):
// 20/#22: origin — trigger tomondan o'sadi; motion-safe —
// harakatni kamaytirish so'ralmagan bo'lsa. Faqat data-state=open
// da: yopilganda animatsiya nomi "none" bo'ladi va Radix kutmasdan
// olib tashlaydi — aks holda kirish animatsiyasi paytidagi Esc
// animationend'ni kutardi (e2e-18 topdi: menyu yopildi, fokus
// bandda qoldi)
"… origin-(--radix-dropdown-menu-content-transform-origin) …
data-[state=open]:motion-safe:animate-kirish"ui/tooltip.tsx da ham xuddi shunday, faqat tooltip'ning ochiq holatlari ikkita: data-[state=delayed-open] (kutib ochildi) va data-[state=instant-open] (darhol — boshqa tooltip'dan o'tilganda).
7.4 Tekshiruvlar va commit
git add -A
git commitCommit (kanon): feat: dialog kirish/chiqish (@starting-style, allow-discrete), menyu animatsiyasi. Diff: 4 fayl, +53 −7.
Tekshiruvlar (haqiqiy, holat 22):
npm test— node:testpass 203, VitestTests 65 passed (65);lint,format:check,tip,knip—exit 0.- Build: birinchi yuklash JS 186.7 KB, CSS 4.6 KB (gzip) — animatsiya CSS'da, JavaScript qo'shilmadi.
- axe — 8 ko'rinishda 0 buzilish. Ko'rinish solishtiruvi (17 → 22): 236 element, farq 4 — yopiq dialog va buyruqlar oynasining
opacitysi1 → 0(yangi yopiq holat). - Brauzer: 159, 72, 100 va 20-qism ssenariysi (
e2e-20) — 68/68. Halol izoh: Ctrl+K ning rus tartibidagi tekshiruvi ssenariyga keyinroq qo'shilgan (Command palette darsida), holat 22 esa u bilan qayta yurgizilmagan. U bilan 69 ta bo'lardi — o'sha tekshiruv holat 11 va 25 da o'tgan.
Quyida ssenariyning animatsiya bo'limi (Chrome 155). Har qatorda tekshiruv nomi va o'lchangan qiymat — ikkinchi va uchinchi qatorga alohida qarang:
✅ dialog ochilishi: opacity va transform o'tishi (150 ms) — @starting-style: ["opacity:150","transform:150"]
✅ Esc: open=false, lekin chiqish o'tishi davomida ko'rinadi (display allow-discrete): [false,true]
✅ chiqishdan keyin: display none, fokus tugmada: ["none","ochir"]
✅ menyu: kirish animatsiyasi «kirish» (120 ms): ["kirish","0.12s"]
✅ toast: o'tish — opacity va translate (kompozitor): "opacity, translate"
✅ animatsiya: CSP buzilishi yo'q: []
✅ reduced-motion: dialog va fon o'tishi ~0 (0.01 ms): ["1e-05s","1e-05s"]
✅ reduced-motion: Esc — darhol yopildi (display none): "none"
✅ reduced-motion: menyu animatsiyasiz (motion-safe): "none"
✅ animatsiya (reduced-motion): CSP buzilishi yo'q: []Nima bo'ldi? Ikkinchi qatorga qarang: open allaqachon false, lekin oyna hali ko'rinadi — allow-discrete ishladi. Uchinchisida — chiqish tugadi, display: none, fokus «O'chirish» tugmasiga qaytdi. 1e-05s — 0.01 ms ning soniyadagi yozuvi (0.00001 s). CSP buzilishi yo'q, chunki hamma animatsiya .css faylda.
8. Ko'p uchraydigan xatolar
| Belgi | Sabab | Davosi |
|---|---|---|
{open && …} bilan chiqqan element animatsiyasiz |
transition'ga oldingi holat yo'q | @starting-style yoki @keyframes |
| chiqish animatsiyasi ko'rinmaydi | React elementni darhol o'chiradi | avval "chiqmoqda" holati, keyin o'chirish |
| chiqish oxirida "miltillash" | forwards yo'q |
animation-fill-mode: forwards |
| dialog yopilganda darhol yo'qoladi | display diskret |
display … allow-discrete, overlay |
onAnimationEnd kutilmaganda chaqiriladi |
ichki elementdan ko'tarildi | e.target === e.currentTarget |
| element o'chmay qoldi | animationend kelmadi (animation: none, fon tab) |
zaxira taymer |
| Radix menyu Esc'dan keyin fokus bandda | kirish animatsiyasi paytida yopish | animatsiya faqat data-state=open da |
scale <span> da ishlamaydi |
inline element | display: inline-block |
9. Mashqlar
Mashqlar kurs/mashqlar/20/22-css-animatsiya/ da. Har yechimni «Ishga tushir» bilan sinang.
1-mashq (oson): qaysi vosita?
Vaziyatga mos CSS vositasini yozing: @starting-style, allow-discrete, key yoki forwards.
<dialog>yopilganda so'nib ketishi uchun →{open && <Panel />}paydo bo'lganda transition ishlashi uchun →- Har yangi xabarda belgi qayta "lip" etishi uchun →
- Chiqish animatsiyasi oxirida element ko'rinmay qolishi uchun →
Yechim
allow-discrete—display: noneo'tish oxirigacha kechikadi.@starting-style— birinchi chizilish uchun boshlang'ich qiymat.key— komponent yangidan yasaladi, keyframe qayta boshlanadi.forwards— oxirgi kadr ushlab turiladi, React o'chirguncha miltillamaydi.
2-mashq (o'rta): xato xabari "titrasin"
Login formasida parol noto'g'ri bo'lsa, xato xabari chapga-o'ngga bir marta titrasin. Har yangi urinishda — qaytadan.
@keyframes titrash—translatebilan: 0 → −6px → 6px → −6px → 0.- Har noto'g'ri urinishda animatsiya qayta boshlansin (
key). - Reduced-motion'da titrash bo'lmasin, faqat matn chiqsin.
- Xabar
role="alert"bilan — Malika eshitsin.
Ishora: «Animatsiyani qayta boshlash: key» bo'limi. Urinishlar sonini state'da sanang va uni key qiling.
Yechim
import { useEffect, useState } from "react";
const css = `
@keyframes titrash {
25% { translate: -6px 0; }
50% { translate: 6px 0; }
75% { translate: -6px 0; }
}
.xato { color: #b91c1c; animation: titrash 300ms ease-in-out; }
@media (prefers-reduced-motion: reduce) {
.xato { animation: none; }
}
`;
function ErrorMessage({ attempt }) {
useEffect(() => {
console.log("xabar chizildi:", attempt);
}, []);
return (
<p className="xato" role="alert">
Parol noto'g'ri ({attempt}-urinish)
</p>
);
}
export default function App() {
const [attempts, setAttempts] = useState(0);
return (
<div>
<style>{css}</style>
<button onClick={() => setAttempts((a) => a + 1)}>
Kirish
</button>
{attempts > 0 && (
<ErrorMessage key={attempts} attempt={attempts} />
)}
</div>
);
}Konsolda:
xabar chizildi: 1
xabar chizildi: 2Ekranda:
Kirish
Parol noto'g'ri (2-urinish)Ikkinchi bosishda xabar yangidan yasaldi (konsolda ikkinchi yozuv) — titrash qayta boshlandi. role="alert" — yangi paydo bo'lgan xabar ekran o'quvchida darhol o'qiladi. Element har safar yangidan qo'yilgani uchun Malika har urinishda eshitadi. 25%, 50%, 75% dagi kadrlar; boshi va oxiri yozilmagan — element o'z joyidan boshlab, o'z joyida tugaydi.
3-mashq (qiyin): qayta ishlatiladigan useExitAnimation
«Avval animatsiya, keyin o'chirish» bo'limidagi mantiqni hook'ga chiqaring: const { mounted, leaving, close, onAnimationEnd } = useExitAnimation(open, setOpen, 300).
Talablar:
close()— chiqishni boshlaydi;onAnimationEndyokimslik zaxira taymer — qaysi biri oldin bo'lsa —setOpen(false)ni chaqiradi.onAnimationEndfaqat elementning o'z animatsiyasida ishlasin (e.target === e.currentTarget).- Komponent o'chirilsa, taymer tozalansin.
- Bitta sahifada ikkita toast — ikkalasi ham hook'dan foydalansin.
Tekshirish: birinchi toastni yoping, ikkinchisi ochiq qolsin.
Ishora: taymerni useRef da saqlang, tozalashni bo'sh bog'liqlikli effektda qiling (useEffect II).
Yechim
import { useEffect, useRef, useState } from "react";
const css = `
@keyframes chiqish { to { opacity: 0; translate: 0 8px; } }
.chiqmoqda { animation: chiqish 150ms ease-in forwards; }
`;
function useExitAnimation(open, setOpen, ms) {
const [leaving, setLeaving] = useState(false);
const timer = useRef();
useEffect(() => () => clearTimeout(timer.current), []);
function finish() {
clearTimeout(timer.current);
setLeaving(false);
setOpen(false);
}
function close() {
setLeaving(true);
timer.current = setTimeout(finish, ms);
}
function onAnimationEnd(e) {
if (leaving && e.target === e.currentTarget) finish();
}
return { mounted: open, leaving, close, onAnimationEnd };
}
function Toast({ text, closeLabel }) {
const [open, setOpen] = useState(true);
const t = useExitAnimation(open, setOpen, 300);
if (!t.mounted) return null;
return (
<p
className={t.leaving ? "chiqmoqda" : undefined}
onAnimationEnd={t.onAnimationEnd}
>
{text} <button onClick={t.close}>{closeLabel}</button>
</p>
);
}
export default function App() {
return (
<div>
<style>{css}</style>
<Toast text="Saqlandi" closeLabel="Birinchini yopish" />
<Toast text="Nusxalandi" closeLabel="Ikkinchini yopish" />
</div>
);
}Ekranda:
Nusxalandi Ikkinchini yopishekran("Saqlandi") — bosishdan keyingi oraliq holat: birinchi toast hali chiqmoqda, DOM'da. Keyin u o'chdi, ikkinchisi joyida. Har toastning o'z useExitAnimation i — o'z taymeri va o'z leaving holati. e.target === e.currentTarget — tugma ichida animatsiya bo'lsa ham (masalan hover effekti), toastni noto'g'ri paytda o'chirmaydi.
4-mashq: Vazifalar qadami
feat/css-animatsiya branch'ida: asos.css — dialog o'tishlari va ::backdrop reduced-motion qoidasi; vazifalar.css — --animate-kirish; ui/dropdown-menu.tsx va ui/tooltip.tsx — origin-(…) va data-[state=open]:motion-safe:animate-kirish. Keyin commit.
Yechim
Tekshiruv ro'yxati:
- O'chirish oynasini oching va Esc bosing — oyna so'nib ketadi, keyin fokus «O'chirish» tugmasida.
- DevTools → Rendering →
prefers-reduced-motion: reduce— oyna va fon darhol yopiladi, menyu animatsiyasiz. - Menyuni oching va darhol Esc bosing — fokus menyu tugmasiga qaytadi (bandda qolmaydi).
npm run buildvanpm run previewda ham tekshiring —::backdropqoidasi production CSS'da bor.npm test— node 203, Vitest 65. Commit:feat: dialog kirish/chiqish (@starting-style, allow-discrete), menyu animatsiyasi.
10. Real ishda
- Dizayn tizimlari (shadcn, Radix Themes, korporativ kitlar) — kirish/chiqish asosan CSS va
data-statebilan. tw-animate-css sinflarini shadcn kodida har kuni ko'rasiz. - Mobil veb — telefonda JavaScript sekinroq. CSS animatsiyasi kompozitorda ishlaydi va sahifa yuklanayotganda ham silliq qoladi.
- Code review — "
{open && …}+ transition" va "forwardsyo'q" — tez-tez topiladigan xatolar. - Intervyu savollari: "React'da chiqish animatsiyasini kutubxonasiz qanday qilasiz?", "
@starting-stylenima uchun kerak?", "allow-discreteqaysi muammoni hal qiladi?", "Radix yopilishni qanday kutadi?".
Xulosa
- Holat o'zgarishi —
className/data-*/aria-*+ CSStransition. Yangi element uchun —@keyframesyoki@starting-style; qayta boshlash —key. - Chiqish: avval "chiqmoqda" holati va animatsiya, keyin o'chirish (
onAnimationEnd+ zaxira taymer,forwards). <dialog>/popover:displayvaoverlay—allow-discrete, kirish —@starting-style. JavaScript'siz.- Radix
data-state="closed"dagi animatsiyani kutadi; kanonda menyu animatsiyasi faqatopenda (kirish paytidagi Esc tuzog'i). - Tailwind:
data-[state=open]:,motion-safe:; shadcn — tw-animate-css (animate-in,fade-in-0,zoom-in-95). - CSS yetadigan joyda — CSS (0 KB, kompozitor, CSP'ga mos); spring, gesture, layout — Motion.
Keyingi dars: Motion I: motion komponentlar va asosiy animatsiya — motion.div, initial/animate/transition, spring va keyframes massivi; vazifalar ga Motion qo'shiladi.
Manbalar
- MDN:
@starting-style,transition-behavior,overlay,animationend— developer.mozilla.org - MDN: Popover API (
popovertarget) — developer.mozilla.org/docs/Web/API/Popover_API - Radix Primitives: Animation guide — radix-ui.com/primitives/docs/guides/animation
- tw-animate-css — npm sahifasi; shadcn/ui: Tailwind v4 — ui.shadcn.com/docs/tailwind-v4
- Tailwind CSS:
motion-safe,animation— tailwindcss.com/docs
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!