UIDex

جدول البيانات

Data Table

<table> + <th scope>

اسمه كمانالجدول، تيبل، داتا تيبل، جريد، شبكة بيانات، جدول قابل للفرز، table، HTML table، sortable table، data grid، grid view، tabular list

جدول البيانات عرض ثنائي المحور: الصفّ سجلّ واحد، والعمود صفة واحدة تتكرّر في كل السجلّات، ومعنى الخلية يأتي من تقاطع الاثنين. وهو أداة مقارنة قبل أن يكون أداة عرض، فالقيمة التي يحتاج القارئ إلى وزنها بغيرها تُوضع في عمود، وما لا يُقارَن يبقى في بطاقة أو قائمة. وهو ليس شبكة من عناصر <div> منسّقة بـ CSS Grid، فتلك ترسم الخطوط وتُسقِط العلاقة، ولا يعرف قارئ الشاشة أن «12,400» هي مبيعات صفّ «قميص قطن» ما لم يحمل كل مستوى دوره: role="table" ثم rowgroup ثم row ثم columnheader و rowheader و cell. وهو ليس data grid كذلك، لأن role="grid" يُعلن عنصر تحكّم فيه تركيز على مستوى الخلية يتنقّل بمفاتيح الأسهم، وينقل معظم قارئات الشاشة من وضع التصفّح إلى نقطة تركيز واحدة متجوّلة. وهو ليس جدول تخطيط، أي استعمال <table> لترتيب عناصر الصفحة، وذاك يحتاج role="presentation" حتى تخرج دلالات الصفوف والخلايا من شجرة الوصول. والسؤال الذي يحسم الاختيار قبل كتابة سطر واحد: هل ينتقل المستخدم بين الخلايا بالأسهم ويحرّر داخلها؟ إن كان الجواب لا، فترميز <table> العادي هو الجواب كلّه، و role="grid" فوقه ينزع سلوك لوحة المفاتيح من المتصفّح دون أن يضع بديلاً.

لو قلت عليه…

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

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

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

مبيعات الأسبوع حسب المنتج
المنتجالمنطقةآخر طلب
حقيبة ظهردبي21,900+11.8%2026-08-15
قميص قطنالرياض12,400+4.2%2026-08-12
حذاء رياضيالقاهرة8,150‎-3.1%2026-08-09
الجدول مفروز حسب المبيعات تنازلياً
RTLأول عمود عند اليمين، ويتجمّد هناك بـ inset-inline-start

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

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

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

ابنِ جدول بيانات من عناصر الجدول الحقيقية: <table> و <caption> و <thead> و <tbody>، ولا تبنِه من <div> إلا إذا حملت كل مستوياته طقم الأدوار كاملاً role="table" و rowgroup و row و columnheader و rowheader و cell. اجعل <caption> اسم الجدول، وسمِّ كل عمود بـ <th scope="col"> وكل سجلّ بـ <th scope="row">. اجعل الفرز زرّاً داخل خلية الرأس، وضع aria-sort على <th> نفسه، وأعد بقيّة الأعمدة إلى none في التحديث ذاته، وأعلن النتيجة في منطقة aria-live="polite". لُفّ الجدول في حاوية overflow-x تحمل tabindex="0" و role="region" واسماً مأخوذاً من <caption> بـ aria-labelledby. جمّد العمود الأول بـ position: sticky مع inset-inline-start: 0 وخلفية معتمة و z-index، وثبّت الرأس بـ inset-block-start: 0، واضبط border-collapse: separate مع border-spacing: 0 حتى تبقى الحدود ملتصقة بخلاياها. حاذِ الأعمدة الرقمية بـ text-align: end مع font-variant-numeric: tabular-nums، ولُفّ كل قيمة تحمل إشارة أو نسبة في <bdi> أو dir="ltr". استعمل الخصائص المنطقية في كل موضع: padding-inline و margin-inline و border-inline-end و inset-inline-start و text-align: end، ولا تكتب left ولا right ولا padding-left ولا text-align: right. واعرض الحالة الفارغة داخل الجدول في صفّ واحد بخلية ممتدّة على كل الأعمدة مع بقاء الرأس، لا في صندوق بجانب الجدول ولا بحذف الجدول من الصفحة.

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

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

ترتيب الأعمدة

ينعكس

ترتيب الأعمدة يأتي من direction المحسوبة على صندوق الجدول نفسه: في RTL يُرسم أول عمود في الـ DOM عند اليمين، ويسير ما بعده يساراً. لا تعكس ترتيب <td> في الترميز لتصل إلى هذا الشكل، فالعكس اليدوي يفصل الخلية عن رأسها ويكسر scope وهو يبدو سليماً بالعين. وضبط dir على <tbody> أو على خلية واحدة لا يعيد ترتيب شيء، لأن الترتيب يُقرأ من الجدول وحده، فضع الاتجاه على <table> أو على ما يحيط به.

العمود المجمّد

ينعكس

جمّد العمود بـ inset-inline-start: 0 لا بـ left: 0. مع left: 0 في العربية تلتصق الخلية بالحافّة اليسرى بينما عمودها معروض عند اليمين، فتستقرّ فوق أعمدة أخرى. والفاصل بينه وبين ما يمرّ تحته يكون border-inline-end، أما box-shadow فلا صيغة منطقية له إطلاقاً، فإن استعملته فوقّع إزاحته بمتغيّر اتجاه على هيئة calc(6px * var(--dir)) وأعطِ ‎--dir‎ القيمة ‎-1‎ داخل [dir="rtl"].

سهم الفرز

لا ينعكس

سهم الفرز رأسي: لأعلى للتصاعدي ولأسفل للتنازلي، والمحرفان ↑ U+2191 و ↓ U+2193 قيمة Bidi_Mirrored فيهما No فلا ينقلب أيّهما مع الصفحة، وكذلك لا يتغيّر معنى aria-sort، فـ ascending هو الأصغر أولاً في العربية كما في الإنجليزية. والقاعدة الشاملة [dir="rtl"] svg { transform: scaleX(-1) } تطال هذا السهم مع كل أيقونة أخرى في الرأس، فاقصر القلب على صنف تضعه على الأيقونات الأفقية وحدها. أما موضع السهم داخل خلية الرأس فينعكس لأنه يجلس عند النهاية المنطقية، فادفعه بـ margin-inline-start: auto أو بـ justify-content: flex-end في صفّ flex، ولا تكتب margin-left.

الأعمدة الرقمية

ينعكس

الأرقام لا تنقلب، فمحارف 0-9 صنفها في bidi هو EN وتُرسم من اليسار إلى اليمين داخل السطر العربي؛ الذي ينقلب هو محاذاة العمود، فاكتب text-align: end لا text-align: right التي تلصق الأرقام بالحافّة التي يبدأ منها النصّ العربي وتبعدها عن رأسها. والفخّ في الإشارة لا في الرقم: محرف U+002D صنفه ES، وإن لم يقع بين رقمين صار محايداً فأخذ اتجاه الفقرة واستقرّ على الطرف المقابل للقيمة التي يخصّها، فتقرأ «3.1%» وناقصها بعيد عنها. لُفّ كل قيمة تحمل إشارة أو نسبة في <bdi> أو في عنصر بـ dir="ltr" حتى تلتصق الإشارة برقمها، وأضف font-variant-numeric: tabular-nums حتى تتحاذى الخانات رأسياً بين الصفوف.

التمرير الأفقي

ينعكس

حاوية التمرير تبدأ عند بدايتها المنطقية، أي عند الحافّة اليمنى في العربية، فيظهر أول عمود بلا تدخّل منك. وقراءة هذا الموضع بالكود هي موضع اختلاف المحرّكات: Blink و Gecko تعطيان scrollLeft صفراً عند تلك البداية ثم تنزل القيمة إلى ما دون الصفر، بينما ظلّت WebKit تعطي قيمة موجبة تبدأ من الفرق بين scrollWidth و clientWidth عند الحافّة نفسها، فلا تقارن scrollLeft بصفر لتعرف أين وقف المستخدم، ومرّر إلى scrollBy إزاحة موقّعة بـ getComputedStyle(el).direction === "rtl" ? -1 : 1 لأن الإزاحة فيزيائية لا تعرف اتجاه الصفحة. وأعطِ الحاوية tabindex="0" مع role="region" واسماً مقروءاً عبر aria-labelledby: كروم يجعل حاويات التمرير قابلة للتركيز من تلقائه منذ الإصدار 127، لكن tabindex الصريح هو الجزء الوحيد الذي تضمنه بنفسك، وبدونه لا يملك مستخدم لوحة المفاتيح طريقة لتمرير الجدول.

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

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

HTML<th scope="col"> / <th scope="row">الربط الرخيص بين القيمة ورأسها؛ يكفي حتى تتبع الخلية رأسين على المحور نفسه فينتقل الأمر إلى headers و id.
HTML<caption>اسم الجدول، ولا يصحّ إلا أول عنصر داخل <table>.
ARIAaria-sort="ascending | descending | none"على <th> لا على الزرّ، وعلى عمود واحد فقط في كل لحظة.
ARIArole="grid"يحوّل الجدول إلى عنصر تحكّم بتنقّل بالأسهم؛ لا تضعه على جدول للقراءة فقط.
CSSposition: sticky + inset-inline-start: 0يجمّد العمود الأول على الحافّة الصحيحة في الاتجاهين بلا قاعدة خاصّة بـ RTL.
CSSborder-collapse: separate + border-spacing: 0شرط عملي للخلايا الملتصقة، لأن الحدود المدمجة تخصّ الجدول فتنزلق مع التمرير.
Material UI<TableContainer><Table stickyHeader>الحاوية هي ما يتمرّر، و stickyHeader يعمل لأن الجدول يتحوّل إلى border-collapse: separate.

شوف كمان

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