ثابت نسبت به صفحه‌نمایش و چسبنده هنگام اسکرول: fixed و sticky

در دو درس قبل با relative و absolute آشنا شدیم. حالا وقتشه با دو مقدار دیگه از Property به اسم position آشنا بشیم که هر دو، به روش خودشون، به رفتار عنصر هنگام اسکرول کردن صفحه مربوط می‌شن؛ اما با نتایج کاملاً متفاوت.

position: fixed چیست؟

.box {
  position: fixed;
  top: 20px;
  right: 20px;
}

fixed از خیلی جهات شبیه absolute عمل می‌کنه: عنصر کاملاً از Normal Flow خارج می‌شه و هیچ فضایی در چیدمان اشغال نمی‌کنه. اما تفاوت اصلی اینه: Containing Block عنصر fixed، همیشه Viewport (ناحیه قابل‌مشاهده مرورگر که در فصل ۶ باهاش آشنا شدیم) است؛ نه یک عنصر جد relative یا هر عنصر دیگه‌ای.

ویژگی مهم: ثابت ماندن هنگام اسکرول

.back-to-top {
  position: fixed;
  bottom: 20px;
  right: 20px;
}
<button class="back-to-top">بازگشت به بالا</button>

مهم‌ترین ویژگی fixed اینه که، حتی وقتی کاربر صفحه رو اسکرول می‌کنه، این عنصر همیشه در همون موقعیت ثابت نسبت به Viewport باقی می‌مونه؛ انگار به خود پنجره مرورگر «چسبیده»، نه به محتوای صفحه. در این مثال، دکمه «بازگشت به بالا»، همیشه در گوشه پایین-راست صفحه‌نمایش دیده می‌شه، صرف‌نظر از این‌که کاربر چقدر پایین اسکرول کرده باشه.

کاربردهای رایج fixed

  • هدر ثابت (Sticky Header): منویی که همیشه بالای صفحه باقی می‌مونه.
  • دکمه شناور: مثل دکمه «بازگشت به بالا» یا دکمه چت آنلاین.
  • پنجره‌های Modal یا Overlay: که باید همیشه وسط صفحه‌نمایش کاربر باقی بمونن، حتی اگه پشت‌شون محتوای زیادی برای اسکرول وجود داشته باشه.
.header {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  background-color: white;
}

نکته مهم: fixed داخل عناصر با transform یا filter

یک استثنای فنی مهم وجود داره: اگه یکی از عناصر جد یک المان fixed، Property به اسم transform، filter یا چند Property تخصصی دیگه (که در فصل‌های Transform و Animation کامل بررسی‌شون می‌کنیم) داشته باشه، اون عنصر جد، خودش تبدیل به Containing Block می‌شه؛ نه Viewport. این یه رفتار خاص و کمتر شناخته‌شده‌ست که گاهی باعث سردرگمی می‌شه، ولی برای درک اولیه fixed، فعلاً همین‌قدر کافیه بدونیم در حالت عادی (بدون این Propertyهای خاص روی جدها)، Containing Block همیشه Viewport است.

position: sticky چیست؟

.sticky-nav {
  position: sticky;
  top: 0;
}

sticky ترکیبی جالب از رفتار relative و fixed است: عنصر ابتدا دقیقاً مثل relative (یعنی در جایگاه طبیعی خودش در Normal Flow) رفتار می‌کنه؛ اما وقتی کاربر اسکرول کنه و عنصر به یک نقطه آستانه مشخص (که با top، bottom و مشابه‌ها تعریف می‌شه) برسه، عنصر مثل fixed «می‌چسبد» و تا وقتی والدش در دید باشه، ثابت باقی می‌مونه.

یک مثال عملی برای درک بهتر

.category-title {
  position: sticky;
  top: 0;
  background-color: #f5f5f5;
  padding: 8px;
}
<div>
  <h3 class="category-title">میوه‌ها</h3>
  <p>سیب</p>
  <p>موز</p>
  <p>پرتقال</p>
  <h3 class="category-title">سبزیجات</h3>
  <p>هویج</p>
  <p>کاهو</p>
</div>

در این مثال، وقتی کاربر پایین اسکرول می‌کنه و تیتر «میوه‌ها» به لبه بالای Viewport (یعنی top: 0) می‌رسه، این تیتر همون‌جا می‌چسبه و تا وقتی که همه آیتم‌های «میوه‌ها» از دید خارج نشدن، ثابت باقی می‌مونه؛ به‌محض این‌که تیتر بعدی («سبزیجات») از پایین برسه، تیتر اول به‌طور طبیعی جای خودش رو به تیتر جدید می‌ده. این الگو، دقیقاً همون چیزیه که در اپلیکیشن‌های لیست مخاطبین (که حروف الفبا هنگام اسکرول می‌چسبن) یا فهرست دسته‌بندی‌های فروشگاهی زیاد دیده می‌شه.

شرط ضروری sticky: تعریف حداقل یکی از Offsetها

.sticky-without-offset {
  position: sticky;
  /* بدون top، right، bottom یا left، sticky هیچ اثری ندارد */
}

برخلاف relative که می‌تونست بدون هیچ Offsetای هم معنا داشته باشه (فقط برای Containing Block ساختن)، sticky حتماً باید حداقل یکی از top، right، bottom یا left رو داشته باشه؛ چون بدون این مقدار، مرورگر نمی‌دونه عنصر باید در چه نقطه‌ای «بچسبد».

محدودیت مهم: sticky به والد مستقیمش محدود است

یک نکته کلیدی درباره sticky: این چسبندگی، فقط تا زمانی ادامه داره که عنصر والدِ مستقیمش، هنوز در Viewport قابل‌مشاهده باشه. به‌محض این‌که کل والد از دید خارج بشه (چون خودش هم اسکرول شده)، عنصر sticky هم همراهش از دید خارج می‌شه؛ برخلاف fixed که همیشه، بدون توجه به هیچ والدی، در صفحه ثابت باقی می‌مونه.

مقایسه کامل fixed و sticky

ویژگی fixed sticky
رفتار اولیه همیشه خارج از Flow، ثابت نسبت به Viewport ابتدا در Flow طبیعی، مثل relative
وضعیت هنگام اسکرول همیشه ثابت و بدون تغییر موقعیت در یک نقطه مشخص می‌چسبد و بعد ثابت می‌ماند
وابستگی به والد مستقل از هر والدی محدود به محدوده والد مستقیم خودش
نیاز به Offset اختیاری (بدون آن در جای پیش‌فرض خودش می‌ماند) ضروری (بدون آن هیچ چسبندگی رخ نمی‌دهد)

یک مثال کامل: هدر ثابت با fixed

body {
  padding-top: 60px; /* جبران فضای هدر ثابت */
}

.site-header {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 60px;
}

نکته مهم در این مثال: چون fixed عنصر رو کاملاً از Flow خارج می‌کنه، محتوای زیرش ممکنه زیر هدر پنهان بشه. برای همین، اضافه کردن یک padding-top به اندازه ارتفاع هدر روی body، یکی از تکنیک‌های رایج برای جبران این فضای از‌دست‌رفته‌ست.

نکات مهم

  • position: fixed عنصر را نسبت به Viewport ثابت می‌کند و در هنگام اسکرول هرگز حرکت نمی‌کند.
  • position: sticky ابتدا مثل relative رفتار می‌کند، سپس در یک نقطه آستانه مشخص، مثل fixed می‌چسبد.
  • sticky حتماً باید حداقل یک مقدار Offset (مثل top) داشته باشد؛ در غیر این‌صورت هیچ اثری ندارد.

اشتباهات رایج

  • فراموش کردن تعریف یک Offset (مثل top: 0) روی sticky، که باعث می‌شود این Property هیچ اثری نداشته باشد.
  • استفاده از fixed برای یک هدر بدون جبران فضای از‌دست‌رفته با padding یا margin روی محتوای زیرش.
  • تصور اینکه sticky دقیقاً مثل fixed در همه‌جای صفحه ثابت می‌ماند؛ در حالی که محدود به محدوده والد مستقیم خودش است.

جمع‌بندی

position: fixed عنصر را کاملاً از Normal Flow خارج کرده و آن را همیشه نسبت به Viewport ثابت نگه می‌دارد، صرف‌نظر از میزان اسکرول صفحه. position: sticky رفتاری ترکیبی دارد: ابتدا مثل relative در جریان طبیعی صفحه قرار می‌گیرد، و در یک نقطه آستانه مشخص، مثل fixed می‌چسبد؛ اما این چسبندگی، برخلاف fixed، محدود به محدوده والد مستقیم عنصر است.