الحركة · فصل ٣

الـ Animation بتاعتك — مش بتاعة المكتبة

data-starting-style · data-ending-style · data-state · data-entering · keepMounted · forceMount · --transform-origin · prefers-reduced-motion
🎯 الهدف: تتحكم في حركة الـ modal والـ drawer والـ select والـ toast بـ CSS بتاعك، وتخلي الـ lib مرنة للي بيستخدمها.

الـ headless مابيجيبش animation أصلًا

Base UI و Radix و React Aria مابيجيبوش أي animation خالص. كل اللي بيعملوه إنهم يقولولك الـ component في أنهي حالة: بيفتح، مفتوح، بيقفل. وانت تحرّك زي ما انت عايز، بـ CSS أو Motion أو أي حاجة.

المشكلة الحقيقية: الـ exit animation

في React، لو الـ component اتشال من الـ DOM فورًا، مفيش وقت يعمل fade out.

كل مكتبة بتعرّفك الحالة إزاي

المكتبةالـ attributesللـ JS animation (Motion)
Base UIdata-open / data-closed، و data-starting-style / data-ending-stylekeepMounted و render prop
Radixdata-state="open" / "closed"forceMount
React Ariadata-entering / data-exitingrender props isEntering / isExiting
.dialog-popup {  transition:    opacity var(--motion-duration) var(--motion-ease),    transform var(--motion-duration) var(--motion-ease);}/* أول frame وهو داخل + وهو خارج = نفس الشكل المخفي */.dialog-popup[data-starting-style],.dialog-popup[data-ending-style] {  opacity: 0;  transform: scale(0.96);}

Drawer مع RTL

نفس الفكرة، بس بدل scale بنحرّك من الجنب، ونعكس الاتجاه في العربي:

.drawer-popup[data-starting-style],.drawer-popup[data-ending-style] {  transform: translateX(100%);}[dir="rtl"] .drawer-popup[data-starting-style],[dir="rtl"] .drawer-popup[data-ending-style] {  transform: translateX(-100%);}

الـ Select والـ dropdowns

الـ popup بياخد data-side (top أو bottom)، و Base UI بتديك --transform-origin، فالـ scale بيطلع من ناحية الـ trigger بالظبط:

.select-popup {  transform-origin: var(--transform-origin);  transition: opacity 150ms, transform 150ms;}.select-popup[data-starting-style],.select-popup[data-ending-style] {  opacity: 0;  transform: scale(0.95);}
جرّب: select بيطلع من ناحية الـ triggerاختار صيغة
  • PNG✓
  • JPG✓
  • WEBP✓
  • PDF✓
  • AVIF✓
  • GIF✓

الـ Toast

Base UI فيها Toast headless كمان، وبتديك CSS variables للـ swipe والـ stacking، وانت تكتب الحركة.

جرّب: toast بحركة بتاعتكدوس كذا مرة ورا بعض

خلّي الـ lib مرنة للي بيستخدمها

١. Motion tokens كـ CSS variables

:root {  --motion-duration-fast: 120ms;  --motion-duration: 200ms;  --motion-ease-out: cubic-bezier(0.22, 1, 0.36, 1);}

أي مشروع يغيّر السرعة من غير ما يلمس الكود.

٢. سيب الـ data attributes ظاهرة

ماتخبيهاش ورا classes بتاعتك، كده المستخدم يعمل override للحركة بـ CSS عادي.

٣. ادعم reduced motion من جوه الـ lib

@media (prefers-reduced-motion: reduce) {  :root {    --motion-duration: 0ms;    --motion-duration-fast: 0ms;  }}

٤. الـ default يبقى CSS transitions

أخف ومن غير dependency. ولو حد عايز spring أو gestures (زي سحب الـ drawer)، يستخدم Motion عن طريق render prop. ماتحطش Motion جوه الـ lib نفسها، عشان ماتجبرش كل اللي بيستخدمها ياخدها.

// المستخدم اللي عايز Motion يجيبها بنفسه<Dialog.Popup  keepMounted  render={(props, state) => (    <motion.div {...props} animate={{ opacity: state.open ? 1 : 0 }} />  )}/>

خلّصت الفصل لما

🧠 اختبر نفسك
١) ليه الـ fade out صعب في React من غير مساعدة؟
لأن الـ component بيتشال من الـ DOM فورًا لما الحالة تتغير، فمفيش عنصر يتحرك. المكتبة بتأخّر الـ unmount لحد ما الـ transition يخلص.
٢) ليه منحطش Motion جوه الـ lib؟
عشان ماتجبرش كل مستخدم ياخد dependency تقيلة. الـ default يبقى CSS، واللي عايز Motion يستخدمها بـ render prop.
٣) إزاي المستخدم يبطّأ كل الحركات في مشروعه؟
يغيّر --motion-duration في الـ :root بتاعه، من غير ما يلمس كود الـ lib.