UIDex

شريط التنقّل السفلي

Tab Bar / Bottom Navigation

TabView / 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) حالة ثالثة: يختار قيمة تصفية لعرض واحد («الكل / غير المقروء») ويعيش داخل المحتوى، ولا يغيّر وجهتك ولا يرافقك من شاشة إلى شاشة. وله قاعدة سلوكية تغفل عنها أغلب النسخ المكتوبة يدوياً: الضغط على الوجهة المحدّدة مرّة ثانية يفرّغ مكدّسها ويعيدك إلى جذرها، وإن كنت في الجذر أصلاً فهو يمرّر المحتوى إلى الأعلى.

لو قلت عليه…

«الشريط اللي تحت في التطبيق وفيه أربع أيقونات»«الأزرار اللي في آخر الشاشة وكل واحدة تفتح قسم»«الحتّة اللي فيها الهوم والسلة والحساب»«الأيقونة اللي بتتملي لون لما تختارها»«الشريط اللي عليه دايرة حمرا صغيّرة فوق أيقونة الرسايل»

العيّنة الحيّة

تفاعل مع الديمو. كل الأجزاء حقيقية ومرقّمة.

٩:٤١
الرئيسية

أنت عند جذر هذا القسم. كل وجهة تحتفظ بمكدّس تنقّلها وحدها.

RTLالوجهة الأولى عند اليمين، والمؤشّر ينزلق على المحور المنطقي

تشريح العنصر: كل جزء واسمه

مرّر على أي سطر ليتحدّد مكانه في الديمو فوق.

برومبت البناء

ابنِ شريط تنقّل سفلياً لتطبيق موبايل بأربع وجهات عليا. اجعل كل عنصر وجهة لا فعلاً، ولا تتجاوز خمس وجهات إطلاقاً، ولا تضع فيه زرّ إنشاء يفتح ورقة سفلية. احتفظ لكل وجهة بمكدّس تنقّل مستقلّ يُستأنف كما تركه المستخدم عند العودة إليها، واجعل الضغط على الوجهة المحدّدة مرّة ثانية يفرّغ مكدّسها إلى الجذر ثم يمرّر إلى الأعلى إن كان في الجذر بالفعل. لكل عنصر: أيقونة مفرّغة في الحالة العادية وممتلئة عند التحديد، وتسمية كلمة واحدة تظهر دائماً بلا قصّ، وشارة اختيارية سقفها تسعة وتسعون فما فوق، وهدف لمس لا يقلّ عن ٤٤pt / ٤٨dp يغطّي الخليّة كلها. لا تعتمد على اللون وحده في التحديد: غيّر شكل الأيقونة وأضف حبّة تحديد خلفها. ثبّت الشريط بـ inset-block-end: 0 و inset-inline: 0، ومدّ الخلفية إلى حافّة الجهاز مع padding-block-end: env(safe-area-inset-bottom) و viewport-fit=cover، ولا تعبث بـ env(safe-area-inset-left/right) عند تبديل الاتجاه. استخدم الخصائص المنطقية في كل مكان (padding-inline و inset-inline-start و margin-inline)، ولا تكتب left أو right. حرّك مؤشّر التحديد بتغيير inset-inline-start لا بـ translateX حتى ينزلق في الاتجاه الصحيح عند الانعكاس، وضع الشارة بـ inset-inline-end، ولا تعكس أيقونات الأشياء (البيت، الشخص، السلة) في RTL بل اقصر القلب على الأيقونات الاتجاهية وحدها. اربط الحالة بمسار التنقّل الحالي لا بمتغيّر منفصل، وأعلن العنصر المحدّد لقارئ الشاشة بـ aria-current="page" في المكافئ الويبي أو selected في المكوّن الأصلي.

سلوكه في الاتجاه من اليمين لليسار

الجزء ده حصري عندنا.

ترتيب الوجهات ينعكس

ينعكس

الوجهة الأولى تجلس في أقصى اليمين والأخيرة في أقصى اليسار، وهذا يأتي مجّاناً ما دام الصفّ موزّعاً على المحور المضمّن: 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، وخلط الاثنين هو أقصر طريق إلى شريط محشوّ من الجهة الفارغة.

أسماؤه في الكود

كل سطر هو كلمة مكتبة واحدة عن نفس الشيء. خد السطر اللي بيكلّم مشروعك.

SwiftUITabView { … .tabItem { Label(…) } }الشريط كله من حاوية واحدة؛ كل شاشة تحمل ‎.tabItem، والتحديد يُربط بـ selection، والعدد بـ ‎.badge(3).
UIKitUITabBarControllerكل viewController فيه UINavigationController مستقلّ؛ وما زاد عن خمسة يُطوى في moreNavigationController.
Jetpack ComposeNavigationBar { NavigationBarItem(…) }اسم Material 3 للعنصر (خلفاً لـ BottomNavigation في M2)، وحبّة التحديد مضبوطة عبر indicatorColor.
Navigation ComposepopUpTo(startDestination) { saveState = true }مع launchSingleTop و restoreState هي الوصفة الرسمية لمكدّس مستقلّ لكل وجهة.
React NavigationcreateBottomTabNavigator()خياراته tabBarIcon و tabBarBadge، وحدث tabPress هو المكان الذي يُنفَّذ فيه الرجوع إلى الجذر.
ARIA<nav aria-label> + aria-current="page"روابط تنقّل داخل معلَم nav هي المكافئ الويبي الصحيح، لا role="tablist".
CSSpadding-block-end: env(safe-area-inset-bottom)يتطلّب viewport-fit=cover في وسم viewport، وإلا عادت القيمة صفراً بصمت.

شوف كمان

آخر تحديث · 2026-08-19