شريط التنقّل السفلي
Tab Bar / Bottom NavigationTabView / NavigationBar
اسمه كمانشريط التبويب السفلي، تاب بار، بوتوم نافيجيشن، التبويبات السفلية، شريط الأقسام، قائمة التنقّل السفلية، tab bar، bottom navigation bar، bottom nav، bottom tabs، bottom tab navigation، BottomNavigationView
شريط التنقّل السفلي شريط ثابت أسفل شاشة التطبيق يحمل من ثلاث إلى خمس وجهات عليا، وواحدة منها محدّدة في كل لحظة. يميّزه أنه ينقلك بين أقسام كاملة لا بين ألواح محتوى، وأن كل وجهة تحتفظ بمكدّس تنقّل (navigation stack) خاصّ بها: لو دخلت على تفاصيل منتج، ثم قفزت إلى وجهة أخرى، ثم عدت، وجدت نفسك في نفس الشاشة التي تركتها لا في جذر القسم. والتبويبات على الويب (role="tablist") شيء آخر: تلك تبدّل لوحاً واحداً داخل صفحة واحدة، ولا تملك مكدّس رجوع ولا مساراً (URL) خاصّاً بكل لوح، أمّا هذا فيبدّل جذور التطبيق نفسها. ولهذا فمكافئه على الويب <nav> فيه روابط تحمل aria-current="page"، ووضع role="tab" عليه خطأ دلالي صريح. وشريط الأدوات (toolbar) يختلف عنه كذلك: هو يحمل أفعالاً تخصّ الشاشة المعروضة الآن (حذف، مشاركة، فرز)، وأزراره تُطلق فعلها ثم تعود كما كانت ولا تبقى «محدّدة»، بينما كل عنصر هنا مكان تذهب إليه لا فعل تنفّذه. والمفتاح المقسّم (segmented control) حالة ثالثة: يختار قيمة تصفية لعرض واحد («الكل / غير المقروء») ويعيش داخل المحتوى، ولا يغيّر وجهتك ولا يرافقك من شاشة إلى شاشة. وله قاعدة سلوكية تغفل عنها أغلب النسخ المكتوبة يدوياً: الضغط على الوجهة المحدّدة مرّة ثانية يفرّغ مكدّسها ويعيدك إلى جذرها، وإن كنت في الجذر أصلاً فهو يمرّر المحتوى إلى الأعلى.
لو قلت عليه…
العيّنة الحيّة
تفاعل مع الديمو. كل الأجزاء حقيقية ومرقّمة.
أنت عند جذر هذا القسم. كل وجهة تحتفظ بمكدّس تنقّلها وحدها.
تشريح العنصر: كل جزء واسمه
مرّر على أي سطر ليتحدّد مكانه في الديمو فوق.
برومبت البناء
سلوكه في الاتجاه من اليمين لليسار
الجزء ده حصري عندنا.
ترتيب الوجهات ينعكس
ينعكسالوجهة الأولى تجلس في أقصى اليمين والأخيرة في أقصى اليسار، وهذا يأتي مجّاناً ما دام الصفّ موزّعاً على المحور المضمّن: Row في Compose و HStack في SwiftUI وأي flex بلا row-reverse. وثلاثة أشياء تفسده. الأول أن يعكس المطوّر مصفوفة الوجهات بيده أو يضيف flex-direction: row-reverse داخل [dir="rtl"]، فيقع القلب مرّتين ويعود التبويب الأول إلى اليسار، ويبقى ترتيب لوحة المفاتيح و TalkBack على حاله فينفصل الترتيب البصري عن ترتيب القراءة. والثاني أن أندرويد لا يعكس شيئاً من الأساس ما لم تكن android:supportsRtl="true" على وسم <application> في الـ manifest، فيخرج شريط سليم في المعاينة ومقلوب في الجهاز. والثالث أن I18nManager.forceRTL(true) في React Native لا يسري إلا بعد إعادة تحميل الحزمة، فيظنّ المطوّر أن الانعكاس معطّل وهو لم يبدأ بعد. أمّا الفهرس فلا ينعكس أبداً: selectedIndex = 0 يظلّ الوجهة الأولى منطقياً مهما تغيّر موضعها على الشاشة.
مؤشّر التحديد يسير على المحور المنطقي
ينعكسحبّة التحديد، أو الخطّ الذي ينزلق تحت العنصر المختار، يجب أن تسير من اليمين إلى اليسار في العربية. وأي حساب من نوع translateX(index * itemWidth) حسابٌ فيزيائي يدفع الحبّة بعيداً عن الوجهة التي ضغطها المستخدم، لأن transform ليس له صيغة منطقية في CSS أصلاً. على الويب حرّك inset-inline-start بنسبة مئوية؛ وإن احتجت إلى الـ compositor فأبقِ الـ transform لكن أعطه إشارة اتجاه: translateX(calc(var(--dir) * var(--x))) مع --dir: 1 على :root و--dir: -1 داخل [dir="rtl"]. والفخّ الأهدأ هو القياس نفسه: offsetLeft و getBoundingClientRect().left قيمتان فيزيائيتان، فالحبّة الموضوعة من قياس يساري لا تصحّ إلا إذا وضعتها بـ left فيزيائية كذلك، أمّا خلط قياس فيزيائي بخاصّية منطقية فيضعها في الجهة المقلوبة تماماً. وفي Compose استخدم Modifier.offset لأنه يعكس محور x حين تكون LayoutDirection.Rtl، ولا تستخدم Modifier.absoluteOffset فهو يتجاهل الاتجاه عمداً.
الشارة: الركن المنطقي والرقم داخلها
ينعكسالشارة تجلس عند النهاية المنطقية للأيقونة: أعلى اليمين في الإنجليزية، أعلى اليسار في العربية. المكوّنات الجاهزة تتكفّل بالموضع وحدها (.badge في SwiftUI و BadgedBox في Compose)، أمّا التركيب اليدوي فلا: .offset(x: 10) إزاحة فيزيائية لا تقرأ layoutDirection أصلاً، والصحيح ZStack بمحاذاة .topTrailing، أو قراءة \.layoutDirection من البيئة وقلب إشارة الإزاحة؛ وعلى الويب استبدل right: -6px بـ inset-inline-end: -6px. ثم يأتي الرقم نفسه، وهنا خطأ يمرّ على الجميع: الأرقام لا تنعكس، لكن علامة التجاوز تنعكس. في عدّاد مقصور على تسعة وتسعين فما فوق، رقمان تتبعهما علامة زائد، تقع علامة الزائد آخر النصّ بلا رقم بعدها يشدّها، فيعدّها خوارزم ثنائي الاتجاه محرفاً محايداً ويسلّمها لاتجاه الفقرة، فتقفز إلى يمين الرقمين بدل يسارهما ويقرؤها المستخدم «زائد تسعة وتسعين». اعزل العدّاد بـ <bdi> أو unicode-bidi: isolate أو direction: ltr على عنصر الشارة، واختر للشريط كلّه نظام أرقام واحداً، Intl.NumberFormat("ar-EG") للهندية أو "ar-EG-u-nu-latn" للاتينية، ولا تخلط النظامين في شريط واحد.
أيقونات الأشياء لا تنعكس
لا ينعكسالبيت والشخص وعربة التسوّق وجرس التنبيه أشياء لها شكل ثابت في الواقع، وقلبها يجعلها تبدو معطوبة لا مترجمة. الذي ينعكس هو الأيقونة الاتجاهية وحدها: سهم الرجوع، سهم الإرسال، علامة «التالي». لذلك لا تكتب قاعدة شاملة مثل [dir="rtl"] svg { transform: scaleX(-1) }، بل اقصر القلب على صنف مخصّص. والمنصّات تعطيك المفتاح جاهزاً: في Compose استخدم Icons.AutoMirrored.Filled.ArrowBack بدل النسخة القديمة، وفي أندرويد اضبط android:autoMirrored="true" على الـ vector drawable (وهي لا تعمل على <shape>)، وفي SF Symbols اختر chevron.backward التي تنعكس تلقائياً بدل chevron.left التي لا تنعكس أبداً. أمّا أيقوناتك المرسومة يدوياً فلا يعرف النظام اتجاهيّتها، فصرّح بها بنفسك: .flipsForRightToLeftLayoutDirection(true) في SwiftUI، أو خيار الاتجاه في asset catalog.
المنطقة الآمنة تستثنى من قاعدة المنطقي
لا ينعكسenv(safe-area-inset-left) و env(safe-area-inset-right) هما الموضع الوحيد في هذا العنصر الذي تُبقي فيه على left و right رغم كل ما سبق. ولا مكافئ منطقيّ لهما، وقيمتاهما تتبعان دوران الجهاز لا لغة الواجهة: في الوضع الأفقي يقع النتوء على الحافّة التي أُدير الهاتف نحوها، وهذا لا علاقة له بالحافّة التي يبدأ منها النصّ. وتبديلهما داخل [dir="rtl"]، وهو ما يفعله من يطبّق «حوّل كل left إلى right» على الملفّ كلّه، يحشو الحافّة الفارغة ويترك أهداف اللمس تحت النتوء فعلاً. وما عدا ذلك في تثبيت الشريط محايد تجاه الاتجاه لأنه يعيش على المحور الكتلي، فلا inset-block-end: 0 ولا padding-block-end: env(safe-area-inset-bottom) يحتاج قاعدة داخل [dir="rtl"]. والتمييز نفسه قائم على iOS: UIView.safeAreaInsets في UIKit من نوع UIEdgeInsets بحافّتَي left و right فيزيائيّتين، بينما EdgeInsets في SwiftUI يعطيك leading و trailing تُحلّان حسب layoutDirection، وخلط الاثنين هو أقصر طريق إلى شريط محشوّ من الجهة الفارغة.
أسماؤه في الكود
كل سطر هو كلمة مكتبة واحدة عن نفس الشيء. خد السطر اللي بيكلّم مشروعك.
شوف كمان
آخر تحديث · 2026-08-19