كن مطوّر واجهات أمامية محترفا
7 دقيقة قراءة

تموضع CSS والتكديس: static وrelative وabsolute وfixed وsticky وz-index

دليل عملي كامل لتموضع CSS — قيم position وكيف يحرّك كلٌّ منها العنصر، وخصائص الإزاحة، والكتل الحاوية، والتموضع اللاصق، وz-index وسياقات التكديس (ولماذا لا يفعل z-index شيئًا أحيانًا)، مع الأخطاء الشائعة وتمارين عملية مع حلولها.

التموضع هو حيث تنكسر بهدوء ثقة كثيرة في CSS. تضبط position: absolute فيطير العنصر إلى مكان غير متوقّع؛ وتضبط z-index: 9999 فيبقى مع ذلك خلف قائمة. لكلٍّ تفسير نظيف حالما تفهم فكرتين: الكتلة الحاوية (containing block) التي يتموضع العنصر بالنسبة إليها، وسياق التكديس (stacking context) الذي يقرّر ما يُرسَم في الأعلى. هذا المقال يبني كليهما. (التموضع يقف بجوار التدفّق العادي الذي تعرفه من نموذج الصندوق وأنماط التخطيط في Flexbox وGrid — إنه الأداة لإخراج عنصر من ذلك التدفّق.)

قاعدتان تفتحان كل التموضع. الأولى: العنصر المتموضع تموضعًا مطلقًا يُزاح عن أقرب سلف متموضع له، لا عن الصفحة. الثانية: z-index يقارن العناصر فقط داخل سياق التكديس نفسه — ولهذا قد تخسر قيمة ضخمة.

التدفّق العادي (ما تنسحب منه)

افتراضيًّا كل عنصر position: static — يجلس في التدفّق العادي، يتكدّس من الأعلى للأسفل (كتلي) أو من اليمين لليسار (سطري)، كلٌّ يدفع التالي. خصائص top/right/bottom/left وz-index لا تفعل شيئًا على عنصر static. التموضع هو فعل سحب عنصر من هذا التدفّق كي تستطيع وضعه عمدًا.

.el { position: static; }  /* الافتراضي — الإزاحات تُتجاهَل */

relative: ادفعه دون مغادرة التدفّق

position: relative يبقي العنصر في التدفّق (مساحته الأصلية محفوظة) لكنه يتيح دفعه عن حيث كان سيكون، باستخدام خصائص الإزاحة:

.badge {
  position: relative;
  top: -4px;     /* تحرّك للأعلى 4px عن موضعه العادي */
  left: 8px;     /* تحرّك لليمين 8px */
}

شيئان يجب معرفتهما: الفجوة التي تركها تبقى محجوزة (الأشقّاء لا يصعدون لملئها)، والأهم بكثير — relative يجعل العنصر سياق تموضع لأي أسلاف متموضعين تموضعًا مطلقًا. تلك المهمّة الثانية هي ما يُستخدم لأجله 90% من الوقت.

absolute: مُزال من التدفّق، متموضع بالنسبة لسلف

position: absolute يزيل العنصر من التدفّق العادي كليًّا (مساحته تنهار، والأشقّاء يقتربون) ويتموضع بالنسبة لـ أقرب سلف متموضع — أقرب سلف قيمة position له أي شيء غير static. إن لم يوجد، يرتدّ إلى منفذ العرض.

.card { position: relative; }     /* يؤسّس الكتلة الحاوية */
.card .tag {
  position: absolute;
  top: 8px;
  right: 8px;                       /* مثبّت في الركن العلوي الأيمن للبطاقة */
}

اقتران الأب relative + الابن absolute هو أكثر أنماط التموضع شيوعًا — الشارات، وأزرار الإغلاق، والقوائم المنسدلة، والتلميحات. والخطأ الكلاسيكي نسيان position: relative على الأب، فيتموضع الابن بالنسبة لـ الصفحة بدل البطاقة وينتهي في الركن الخطأ.

بضبط left وright معًا (أو top وbottom)، يتمدّد العنصر بينهما — طريقة مفيدة للتحجيم بالتثبيت:

.overlay {
  position: absolute;
  inset: 0;        /* اختصار لـ top/right/bottom/left: 0 — املأ الأب */
}

fixed: مثبّت بمنفذ العرض

position: fixed يغادر التدفّق أيضًا، لكنه يتموضع بالنسبة لـ منفذ العرض (viewport) ويبقى ثابتًا مع تمرير الصفحة — مثالي للترويسات اللاصقة، وأزرار العودة للأعلى، والنوافذ المنبثقة:

.navbar {
  position: fixed;
  top: 0;
  inset-inline: 0;   /* left:0; right:0 — عرض كامل، منطقي لـ RTL */
}

تحذير واحد: إن كان لأي سلف transform أو filter أو will-change، فإن ذلك السلف يصبح الكتلة الحاوية بدلًا من منفذ العرض — طريقة مفاجئة قد "يتعطّل" بها fixed. (المزيد تحت سياقات التكديس.)

sticky: هجين من relative وfixed

position: sticky يتصرّف كـ relative حتى يصل العنصر إلى عتبة تمرير تضبطها، ثم "يلتصق" كـ fixed ما دامت حاويته في العرض:

.section-heading {
  position: sticky;
  top: 0;          /* يلتصق حالما يصل إلى أعلى منطقة التمرير */
}

اللاصق رائع لترويسات الأقسام وعناوين الجداول، لكن له فخّان يوقعان الجميع:

  • يحتاج عتبة (top، bottom، إلخ) — بدونها لا يلتصق أبدًا.
  • يلتصق داخل صندوق أبيه. إن كان لسلف overflow: hidden (أو auto/scroll)، فاللاصق غالبًا لا يفعل شيئًا بصمت لأنه مقصوص إلى حاوية التمرير الخطأ.

خصائص الإزاحة وinset

top/right/bottom/left تفعل شيئًا فقط على عنصر متموضع (أي شيء غير static). الاختصارات الحديثة توفّر التكرار:

inset: 0;                  /* الجوانب الأربعة = 0 */
inset: 10px 20px;          /* كتلي (top/bottom) 10، سطري (left/right) 20 */
inset-block: 0;            /* top + bottom */
inset-inline-start: 1rem;  /* "left" منطقي في LTR، ينعكس في RTL */

فضّل الصيغ المنطقية (inset-inline-*) في المواقع ثنائية الاتجاه، تمامًا كما مع الهوامش والحشو المنطقية.

z-index وسياقات التكديس

هنا الجزء الذي يربك الجميع. حين تتداخل العناصر، يتحكّم z-index في مَن في الأعلى — لكن فقط بين الأشقّاء داخل سياق التكديس نفسه. سياق التكديس طبقة قائمة بذاتها؛ تُقارَن قيم z-index داخل السياق، لا عبره أبدًا.

يُنشأ سياق تكديس جديد بأمور منها:

  • عنصر الجذر <html>،
  • عنصر متموضع (relative/absolute/fixed/sticky) بـ z-index غير auto،
  • أي عنصر بـ opacity أقلّ من 1، أو transform، أو filter، أو will-change، أو mix-blend-mode، أو isolation: isolate،
  • أبناء flex/grid بـ z-index مضبوط.

النتيجة التي تؤلم: إن عاش العنصر A في سياق تكديس يجلس أبوه تحت سياق العنصر B، فإن لا z-index على A — ولا حتى 999999 — يرفعه فوق B. إنه محبوس داخل طبقة أبيه.

/* .modal لن تظهر فوق .header، رغم z-index الضخم... */
.header { position: relative; z-index: 10; }   /* سياق تكديس عند المستوى 10 */
.content { position: relative; z-index: 1;
           transform: translateZ(0); }          /* ← ينشئ سياقًا عند المستوى 1 */
.modal  { position: fixed; z-index: 99999; }    /* محبوس داخل طبقة .content المستوى 1 */

الحلّ نادرًا ما يكون رقمًا أكبر — بل إخراج العنصر من السياق الحابس (مثلًا رسم النافذة كابن مباشر لـ <body>، وهذا بالضبط سبب استخدام مكتبات الواجهة لـ "البوّابات/portals")، أو إزالة الخاصية التي أنشأت السياق غير المرغوب.

النموذج الذهني: تخيّل سياقات التكديس صناديق متداخلة. تستطيع إعادة ترتيب الأشياء داخل صندوق بحرّية بـ z-index، لكنك لا تستطيع أبدًا جعل شيء في صندوق يقفز فوق الصندوق المجاور له — الصناديق نفسها مرتّبة بسياقها الخاص.

الأخطاء الشائعة

  • ضبط top/left على عنصر static وعدم رؤية شيء — الإزاحات تحتاج عنصرًا متموضعًا.
  • نسيان position: relative على الأب، فيثبّت الابن absolute نفسه بالنسبة للصفحة بدلًا منه.
  • رفع z-index إلى 9999 بينما المشكلة الحقيقية سياق تكديس حابس — الرقم لا يهرب منه.
  • transform أو opacity على سلف ينشئ سياق تكديس بصمت (أو يعيد تثبيت ابن fixed) ويكسر الطبقات.
  • position: sticky بلا عتبة (top/bottom) — لا يلتصق أبدًا.
  • اللاصق مقصوص بـ overflow: hidden/auto لسلف فيبدو وكأنه لا يفعل شيئًا.
  • استخدام absolute لتخطيط صفحة كاملة حيث Flexbox أو Grid أبسط وأمتن.
  • افتراض أن fixed دائمًا بالنسبة لمنفذ العرض — transform لسلف يغيّر ذلك.

تمارين

جرّب كلًّا منها قبل فتح الحل.

تمرين 1 — ثبّت شارة

ضع .badge صغيرة في الركن العلوي الأيمن لـ .card، تتراكب مع حافّتها قليلًا.

إظهار الحل
.card  { position: relative; }     /* الكتلة الحاوية */
.badge {
  position: absolute;
  top: -8px;
  right: -8px;                       /* الإزاحات السالبة تدفعها فوق الركن */
}

position: relative للأب هو ما يجعل الشارة تثبت بالنسبة للبطاقة؛ بدونه ستقفز الشارة إلى ركن الصفحة.

تمرين 2 — تراكب يملأ بالكامل

غطِّ .hero بالكامل بـ .scrim شبه شفّاف لتراكب نصّي، باستخدام خاصية واحدة للإزاحات.

إظهار الحل
.hero  { position: relative; }
.scrim {
  position: absolute;
  inset: 0;                          /* top/right/bottom/left كلها 0 — يملأ الأب */
  background: rgb(0 0 0 / 0.4);
}

بالإزاحات الأربع عند 0، يتمدّد الـ scrim المتموضع تموضعًا مطلقًا إلى كل حافّة من أبيه المتموضع.

تمرين 3 — ترويسة قسم لاصقة

اجعل <h2 class="section-title"> يلتصق بأعلى منفذ العرض بينما يمرّ قسمه.

إظهار الحل
.section-title {
  position: sticky;
  top: 0;            /* العتبة المطلوبة */
}

يتصرّف كتدفّق عادي حتى يصل الأعلى، ثم يلتصق هناك حتى يخرج قسمه الأب من العرض. إن لم يلتصق، تأكّد أنه لا يوجد سلف بـ overflow: hidden.

تمرين 4 — لماذا لا يعمل z-index؟

.panel  { position: relative; z-index: 1; opacity: 0.95; }
.tooltip{ position: absolute; z-index: 9999; }   /* داخل .panel */

ما زالت .tooltip تظهر تحت .toolbar شقيقة (z-index: 5) تعيش خارج .panel. لماذا، وكيف تُصلِحها؟

إظهار الحل

opacity: 0.95 على .panel ينشئ سياق تكديس عند z-index: 1. قيمة التلميح 9999 تنافس فقط داخل ذلك السياق، فتجلس طبقة .panel كلها عند المستوى 1 — تحت مستوى .toolbar 5. الحلّ: أزل opacity من .panel (أو ارفع z-index الخاص بـ .panel فوق 5)، أو ارسم .tooltip خارج .panel كي لا تبقى محبوسة.

النموذج الذهني الذي تحتفظ به

التموضع يدور حول الانسحاب من التدفّق العادي لوضع عنصر عمدًا. relative يدفع في المكان و — مهمّته الحقيقية — يضبط سياق تموضع؛ وabsolute يزيل العنصر ويثبّته بـ أقرب سلف متموضع (فاقرنه بأب relative)؛ وfixed يثبّت بمنفذ العرض؛ وsticky هو relative-حتى-عتبة-ثم-fixed (ويحتاج عتبةً وسلفًا غير مقصوص). للطبقات، تذكّر أن z-index يقارن فقط داخل سياق تكديس واحد — تخيّل سياقات التكديس صناديق متداخلة، وحين "لا يفعل" z-index كبير شيئًا، يكون العنصر محبوسًا في صندوق، فأخرِجه بدل اللجوء إلى رقم أكبر. احجز التموضع للتراكبات والواجهة المثبّتة؛ ولبنية الصفحة، Flexbox وGrid هما الأداتان الصحيحتان.