UIDex

النافذة الحوارية

Modal Dialog

role="dialog" aria-modal="true"

اسمه كمانمودال، مربّع حوار، صندوق حوار، نافذة منبثقة حاجبة، نافذة تأكيد، ديالوج، modal، dialog box، modal window، overlay dialog، confirmation dialog، lightbox

النافذة الحوارية طبقة تُفتح فوق الصفحة وتحتجز التفاعل داخلها: كل ما تحتها يخرج من الخدمة حتى تُغلق. صفتها الفاصلة هي الحَجْب نفسه: aria-modal="true" مع حبس حقيقي للتركيز وطبقة تعتيم تبتلع النقرات. أمّا الشكل والظهور المفاجئ فلا يصنعان نافذة حاجبة. ولهذا فهي ليست popover غير حاجب، فذاك يُغلق بمجرّد النقر خارجه، ويبقى ما وراءه قابلاً للاستخدام وللوصول بلوحة المفاتيح. والدرج (drawer) والورقة السفلية (sheet) حالة أخرى تُعرَّف بمكانها: ينزلق كلّ منهما من حافّة محدّدة ويظلّ ملتصقاً بها، وقد يكون حاجباً أو غير حاجب، بينما النافذة الحوارية تتوسّط المنطقة المرئية بلا انتماء لحافّة. وهي كذلك ليست role="alertdialog"، فذاك نوع ضيّق لرسالة عاجلة تتطلّب ردّاً فورياً، وقارئ الشاشة يقاطع بها ما يقرأه، فلا تستعمله لنموذج تحرير عادي. الاختبار العملي: لو كان بإمكان المستخدم أن يتجاهل ما بداخلها ويمضي في عمله، فما كان يصحّ أن تكون حاجبة من الأساس.

لو قلت عليه…

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

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

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

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

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

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

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

ابنِ نافذة حوارية حاجبة (modal dialog). ارسمها في portal على مستوى <body> حتى لا يقصّها أب فيه overflow: hidden أو transform، واضبط dir على <html> لا على غلاف داخلي حتى ترث النافذة اتجاه الصفحة رغم خروجها من الشجرة. الحاوية تحمل role="dialog" و aria-modal="true" و aria-labelledby يشير إلى معرّف العنوان المرئي و aria-describedby يشير إلى نصّ الجسم. عند الفتح: انقل التركيز إلى أقلّ الأزرار خطراً أو إلى الحاوية نفسها بـ tabIndex={-1}، واحبس Tab داخل النافذة يدوياً لأن aria-modal لا يحبس التركيز، وضع inert على بقية الصفحة، واقفل تمرير الجسم. عند الإغلاق: أعِد التركيز إلى العنصر الذي فتحها. اجعل Escape وزرّ × والنقر على الطبقة الحاجبة تؤدّي جميعها نفس الإغلاق، إلا إذا كان في النموذج تعديل غير محفوظ فاطلب تأكيداً. استخدم الخصائص المنطقية حصراً: inset-inline-end لزرّ الإغلاق، و padding-inline و margin-inline للتباعد، و text-align: start للنصّ، و justify-content: flex-end في التذييل مع ترتيب DOM ثابت (ثانوي ثم أساسي) بلا row-reverse، و padding-inline-end أو scrollbar-gutter: stable لتعويض شريط التمرير. اجعل حركة الدخول translateY مع scale بلا أي translateX ثابت، واحترم prefers-reduced-motion. مرِّر جسم المحتوى وحده مع overscroll-behavior: contain، وأبقِ الرأس والتذييل ثابتين.

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

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

ترتيب أزرار التذييل

ينعكس

التذييل صفّ flex: اترك ترتيب الـ DOM ثابتاً (الثانوي أولاً ثم الأساسي) واضبط justify-content: flex-end. الـ flex-end يتبع direction من تلقاء نفسه، فيستقرّ الزرّ الأساسي عند النهاية المنطقية للسطر (أقصى اليمين في الإنجليزية وأقصى اليسار في العربية)، وهذا هو الترتيب الصحيح لأن الأساسي يقف دائماً في نهاية اتجاه القراءة. الخطأ الشائع هو «إصلاح» الانعكاس بـ flex-direction: row-reverse داخل [dir="rtl"]: القلب يقع مرّتين فيرجع الأساسي إلى البداية وينفصل الترتيب البصري عن ترتيب Tab. وبالمثل استبدل margin-left: auto بـ margin-inline-start: auto.

ركن زرّ الإغلاق

ينعكس

زرّ × يقف في ركن الرأس عند النهاية المنطقية: أعلى اليمين في الإنجليزية وأعلى اليسار في العربية. لو ثبّتّه بـ position: absolute; right: 12px بقي في الجهة الخطأ بعد الانعكاس. استعمل inset-inline-end: 12px، أو الأفضل أن تجعله آخر ابن في رأس flex بـ justify-content: space-between فتسقط الحاجة للتموضع المطلق كلياً. أمّا الرمز × فمتماثل حول محوره الرأسي، فلا تطبّق عليه scaleX(-1).

الطبقة الحاجبة لا تنعكس

لا ينعكس

الطبقة الحاجبة تغطّي المنطقة المرئية كاملة، فليس فيها شيء ينعكس أصلاً: inset: 0 و ::backdrop محايدان تماماً تجاه الاتجاه ولا يحتاجان أي قاعدة خاصة داخل [dir="rtl"]. الخطأ الوحيد المتاح هنا أن تُلوّنها بتدرّج اتجاهي مثل linear-gradient(to right, …) أو أن تعطيها ظلاً بإزاحة أفقية، فتحوّل طبقة محايدة إلى اتجاهية بلا سبب. وانتبه أن دوالّ التدرّج لا تقبل الكلمات المنطقية أصلاً: لا وجود لـ to inline-end في CSS، والمتاح جهات فيزيائية أو زاوية. والحلّ إمّا لون مصمت بشفافية، وإمّا متغيّر ‎--scrim-angle قيمته 90deg تقلبه إلى 270deg داخل [dir="rtl"] وتمرّره إلى linear-gradient(var(--scrim-angle), …).

البوابة تفلت من اتجاه الصفحة

ينعكس

النافذة تُرسم في portal معلّق على <body>، فتخرج من شجرة الـ DOM التي وضعت عليها dir="rtl". ولو كان الاتجاه مضبوطاً على غلاف داخلي، أي ‎<div dir="rtl">‎ حول التطبيق، وُلدت النافذة خارجه وورثت ltr من الجذر: النصّ يصطفّ إلى اليسار، وزرّ الإغلاق يقفز إلى الركن المقابل، وينقلب ترتيب أزرار التذييل، بينما كل خصائصك المنطقية سليمة تماماً لأن العطل غياب سياق الاتجاه لا الـ CSS. اضبط dir على <html> نفسه (‎<html lang="ar" dir="rtl">‎) ليرثه أي شيء يُحقن في body، أو مرّر dir صراحةً إلى حاوية الـ portal أو إلى جذر النافذة. وللتحقّق، افتح النافذة واقرأ getComputedStyle(el).direction، فإن جاءت ltr فالعطل هنا ولا فائدة من تعديل أي قاعدة أخرى.

قفل التمرير وتعويض الشريط

ينعكس

قفل تمرير الصفحة خلف النافذة يُخفي شريط التمرير الرأسي، فيقفز المحتوى جانباً بمقدار عرضه. التعويض المتداول padding-right: 15px صحيح في الإنجليزية فقط: الشريط الرأسي يقف عند الحافّة المنطقية النهائية، أي على اليسار في صفحة dir="rtl"، فالتعويض السليم padding-inline-end أو ببساطة scrollbar-gutter: stable على العنصر الجذر. وانتبه أن العطل لا يظهر على macOS بشرائط التمرير العائمة، فاختبره على ويندوز قبل أن تحكم بأنه سليم.

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

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

ARIArole="dialog" aria-modal="true"يخفي بقية الصفحة عن قارئ الشاشة، ولا يحبس التركيز نيابةً عنك.
HTMLdialog.showModal()العنصر الأصلي: طبقة عليا و ::backdrop و Escape و تعطيل الخلفية بلا كود.
HTMLinertالسمة التي تُخرج بقية الصفحة من التركيز ومن شجرة الوصول معاً.
Radix UI<Dialog.Portal> + <Dialog.Overlay>أجزاء مركّبة: Dialog.Content و Dialog.Title و Dialog.Description و Dialog.Close، وينبّهك لو غاب العنوان.
shadcn/ui<DialogContent> + <DialogFooter>مبنيّ فوق Radix؛ DialogFooter هو صفّ الأزرار الذي ينعكس ترتيبه بصرياً في RTL.
Material UI<Dialog open onClose> + <DialogActions>ماتيريال تسمّي التذييل DialogActions، وتفرّق بين Dialog و Drawer بالمكان لا بالحَجْب.
CSSscrollbar-gutter: stableيحجز مكان شريط التمرير مسبقاً فلا تقفز الصفحة عند القفل، وهو يعمل في الاتجاهين.

شوف كمان

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