UIDex

الأقسام المطوية

Accordion

aria-expanded + aria-controls

اسمه كمانأكورديون، قوائم قابلة للطيّ، أقسام قابلة للتوسيع، لوحات مطوية، كولابس، collapsible، collapse، expansion panel، disclosure group، expander

الأقسام المطوية مجموعة عناوين متراصّة رأسياً، كل عنوان زرّ يفتح لوحة محتوى تحته مباشرة أو يطويها. الوحدة فيها هي القسم الواحد، أي رأسٌ ولوحته الخاصة به، وتحكم المجموعة كلها سياسة واحدة: قسم واحد مفتوح في كل لحظة، أو عدّة أقسام معاً. وهي ليست تبويبات؛ فالتبويبات أشقّاء يتشاركون إطاراً واحداً ثابت الارتفاع تُعرَض فيه لوحة واحدة ولا تُرى لوحتان معاً أبداً، بينما القسم المطويّ يدفع ما تحته ويغيّر طول الصفحة نفسها. والقائمة المنسدلة تختلف عنها كذلك: هي تطفو في طبقة فوق الصفحة ثم تعيدها كما كانت عند الإغلاق، أمّا لوحة القسم فتعيش داخل تدفّق المحتوى وتزيح ما حولها. والإفصاح المفرد (disclosure) غير ذلك كلّه: زرّ واحد قائم بذاته يكشف كتلة واحدة، بلا أشقّاء وبلا سياسة تنسّق بينهم. أمّا العنصران details و summary فهما التنفيذ الأصلي في المتصفّح: يمنحانك السلوك ودلالات الوصولية بلا سطر JavaScript واحد، لكن تحريك ارتفاعهما شاقّ لأن المتصفّح يبدّل الحالة في إطار واحد. والقاعدة العملية أن تتجنّب طيّ محتوى لا بدّ للمستخدم أن يقرأه؛ فالطيّ للمحتوى الثانوي أو المتكرّر كالأسئلة الشائعة وتفاصيل الطلب.

لو قلت عليه…

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

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

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

التبويبات إطار ثابت الارتفاع؛ القسم يدفع ما تحته.

يعطيك السلوك والوصولية بلا JavaScript، لكن تحريكه متعب.

الأول فقط، أو لا شيء إذا كفت العناوين وحدها.

RTLالمؤشّر يدور رأسياً فلا ينعكس، الذي ينعكس هو موضعه

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

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

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

ابنِ أكورديون (أقسام مطوية). كل قسم رأس داخل عنوان h3 يحتوي <button> يحمل aria-expanded و aria-controls بقيمة تساوي id لوحة موجودة فعلاً، واللوحة تحمل aria-labelledby يشير إلى الزرّ؛ ولّد المعرّفات بـ useId حتى لا تتكرّر لو ظهر المكوّن مرّتين في نفس الصفحة. اجعل الزرّ ممتدّاً بعرض الرأس كاملاً، صفّ flex مع justify-content: space-between، والمؤشّر آخر عنصر في ترتيب الـ DOM وعليه aria-hidden. حرّك الارتفاع بـ grid-template-rows من 0fr إلى 1fr مع غلاف داخلي عليه overflow: hidden، أو بـ max-height مقيسة من scrollHeight. ولا تحاول تحريك height: auto. أبقِ زرّ الرأس خارج منطقة القصّ حتى لا تُقصّ حلقة التركيز. اجعل السياسة خياراً: قسم واحد مفتوح أو عدّة أقسام، مع السماح بإغلاق الجميع. عند الطيّ أخرج محتوى اللوحة من ترتيب Tab بـ inert أو hidden أو hidden="until-found". اربط ArrowDown و ArrowUp و Home و End بالتنقّل بين الرؤوس ولا تعكسها في RTL. والتزم بالخصائص المنطقية في كل شيء: padding-inline-start للإزاحة المتداخلة، padding-inline لحشو اللوحة، border-inline-start لخطّ التداخل، inset-inline-end لأي موضع مطلق؛ اعكس المؤشّر الجانبي بـ scaleX(-1) داخل [dir="rtl"]، ولا تعكس المؤشّر الذي يدور رأسياً، واستعمل الخاصية المستقلّة rotate بدل transform حتى لا تمحو إحدى القاعدتين الأخرى. وألغِ الحركة تحت prefers-reduced-motion.

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

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

موضع المؤشّر داخل الرأس

ينعكس

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

المؤشّر الذي يدور رأسياً

لا ينعكس

المؤشّر الذي يشير للأسفل وهو مطويّ ثم يدور ١٨٠ درجة عند الفتح محايد الاتجاه تماماً؛ لا تعكسه أبداً. وانتبه لأيّ زاوية غير ١٨٠. لو كانت الحالة المطوية rotate(-90deg) مثلاً فإن scaleX(-1) فوقها تقلب إشارة الزاوية، فتدور الأيقونة إلى الجهة المعاكسة وتستقرّ مقلوبة. والفخّ الثاني أن transform خاصية واحدة لا تتراكم: قاعدة transform: rotate(180deg) للحالة المفتوحة تمحو قاعدة transform: scaleX(-1) بالكامل، فاجمعهما في تصريح واحد أو استعمل الخاصيتين المستقلّتين rotate و scale لأنهما تُطبَّقان معاً.

المؤشّر الذي يشير جانباً

ينعكس

إذا كان المؤشّر يشير جانباً وهو مطويّ فهو أيقونة اتجاهية خالصة يجب أن تشير إلى اليسار في العربية، لكن طريقة عكسها تختلف باختلاف طريقة رسمها. مسار SVG هندسة لا نصّ، فخاصية direction لا تمسّه إطلاقاً وعليك عكسه صراحةً بـ transform: scaleX(-1) داخل [dir="rtl"]. أمّا المحرف النصّي ‎›‎ فيحمل خاصية Bidi_Mirrored في يونيكود، فالمتصفّح يرسمه معكوساً من تلقاء نفسه داخل فقرة اتجاهها rtl وإضافة scaleX(-1) فوقه تعيده إلى الجهة الخطأ. وأيقونة الخطّ (Font Awesome وأمثالها) تسكن منطقة الاستخدام الخاصّ في يونيكود، وحروفها لا تحمل تلك الخاصية فلا تنعكس أبداً مهما كان اتجاه الفقرة.

الإزاحة والحشو في التداخل

ينعكس

الأكورديون المتداخل يزيح رؤوس الأبناء عن رأس الأب بمقدار ثابت. استعمل padding-inline-start على الرأس بدل padding-left، وإلا هبطت الإزاحة على الطرف الخطأ فتبدو الأقسام الفرعية محاذية لأبيها تماماً ويظهر الفراغ عند نهاية السطر. والقاعدة نفسها تسري على حشو اللوحة (padding-inline لا padding-left/right) وعلى الخطّ الرأسي الذي يدلّ على التداخل: border-inline-start لا border-left، حتى يقف الخطّ إلى جانب المحتوى لا خلفه.

مفاتيح التنقّل

لا ينعكس

محور الأكورديون رأسي، فتنقّل لوحة المفاتيح فيه لا ينعكس: ArrowDown و ArrowUp يبقيان للانتقال بين الرؤوس، و Home و End لأوّلها وآخرها، مهما كان اتجاه الصفحة. الذي ينعكس هو ArrowLeft و ArrowRight إن دعمتهما: في التوجيه الأفقي (orientation="horizontal") أو في نمط الشجرة حيث يفتح أحد السهمين القسم ويغلقه الآخر. ولأن event.key يبلّغ عن المفتاح الفيزيائي ولا يعرف شيئاً عن الاتجاه، اقرأ الاتجاه بنفسك بـ getComputedStyle(el).direction أو el.matches(":dir(rtl)") وبدّل معنى المفتاحين، أو مرّر dir="rtl" إلى جذر المكوّن في Radix ليتكفّل بالتبديل.

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

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

ARIAaria-expanded + aria-controlsالزوج الذي يعرّف النمط: الحالة على الزرّ، والربط باللوحة عبر id مطابق.
HTML<details name="faq"><summary>التنفيذ الأصلي؛ توحيد name بين عدّة details يجعلها أكورديون بقسم واحد مفتوح بلا JavaScript.
HTMLhidden="until-found"يُبقي اللوحة المطوية قابلة للعثور عليها ببحث المتصفّح، ويفتح القسم تلقائياً عند المطابقة.
CSSgrid-template-rows: 0fr → 1frالطريقة العملية لتحريك ارتفاع مجهول، مع غلاف داخلي عليه overflow: hidden.
CSSpadding-inline-startالخاصية المنطقية التي تنقل إزاحة الأقسام المتداخلة إلى الطرف الصحيح عند الانعكاس.
Radix UI<Accordion.Root type="single" collapsible>type يضبط السياسة، ويعرض المكوّن ارتفاع اللوحة في المتغيّر ‎--radix-accordion-content-height.
Ant Design<Collapse accordion items={items} />المكتبة تسمّيه Collapse لا Accordion، والخاصية accordion فيه هي سياسة القسم الواحد.

شوف كمان

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