شعار أوسكار أوسكار كل المقالات

التصميم المتجاوب: موقع واحد لكل الشاشات

أكثر من نصف زيارات الويب اليوم تأتي من الهواتف، وبقيّتها موزّعة بين أجهزة لوحية وحواسيب بأحجام لا تُحصى. لم يَعُد مقبولاً أن تبني «نسخة للجوال» منفصلة؛ الحلّ هو التصميم المتجاوب: موقع واحد بكودٍ واحد يتكيّف بأناقة مع أي شاشة. في هذا الدليل نمرّ على ركائزه العملية خطوة بخطوة، ثم نفكّك ملف أنماط إنتاجياً حقيقياً سطراً بسطر لنرى كيف تُتَّخذ هذه القرارات فعلاً.

1. ابدأ بوسم viewport الصحيح

كل شيء يبدأ بسطر واحد في <head>:

<meta name="viewport" content="width=device-width, initial-scale=1.0">

بدونه يعرض الجوال صفحتك مصغّرةً كأنها شاشة حاسوب، فيضطرّ المستخدم للتكبير. هذا الوسم يخبر المتصفّح أن يستعمل عرض الجهاز الفعلي كنقطة بداية — وهو أساس كل ما يليه.

2. فكّر بمنهج «الجوال أولاً» (Mobile-First)

صمّم الحالة الأساسية للشاشة الصغيرة، ثم أضِف تحسينات للشاشات الأكبر عبر min-width. لماذا؟ لأن الحالة الأبسط (عمود واحد) هي الأنسب أساساً للجوال، وتوسيعها للأعلى أسهل من تقليص تصميم معقّد:

.layout { display: grid; gap: 1rem; }
@media (min-width: 768px) { .layout { grid-template-columns: 2fr 1fr; } }

النتيجة كود أبسط، وأداء أفضل على الأجهزة الأضعف التي تُحمّل القواعد الأساسية فقط.

3. الشبكات والوحدات المرنة

القياسات الثابتة بالبكسل عدوّ التجاوب. اعتمد المرونة:

  • % وfr للعرض والأعمدة بدل قيم ثابتة.
  • rem للنصوص والمسافات لتحترم إعدادات المستخدم.
  • clamp() لقيمة مرنة بحدّين، مثل: font-size: clamp(1rem, 2.5vw, 1.5rem);
  • minmax() وauto-fit لشبكة تتكيّف تلقائياً: grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));

هذه الأدوات وحدها قد تغنيك عن نصف نقاط التوقّف لأنها تتكيّف انسيابياً.

قاعدة عملية: لا تختر نقاط التوقّف حسب أجهزة بعينها («آيفون»، «آيباد»)، بل حسب اللحظة التي «ينكسر» فيها تصميمك. كبِّر نافذة المتصفّح تدريجياً، وحيثما بدا التخطيط سيّئاً ضع نقطة توقّف.

4. نقاط التوقّف (Breakpoints) بحكمة

نقطة التوقّف هي العرض الذي يتغيّر عنده التخطيط عبر @media. ثلاث نقاط شائعة تكفي معظم المواقع: نحو 600px للجوال الكبير، 768px للّوحي، 1024px للحاسوب. لا تُكثر منها؛ كثرة النقاط تعني كوداً أصعب صيانةً. الهدف انتقالات سلسة لا «قفزات» مفاجئة.

5. الصور والوسائط المتجاوبة

الصورة يجب ألّا تتجاوز حاويتها أبداً:

img { max-width: 100%; height: auto; }

وللتحكّم الأدقّ استعمل سمة srcset ليختار المتصفّح المقاس المناسب لكل شاشة، فلا يُحمّل الجوال صورة بعرض 2000 بكسل بلا داعٍ. وللصور الفنّية المختلفة بين الجوال والحاسوب استعمل وسم <picture>.

وهناك نصف القاعدة الذي يُنسى: height: auto وحدها تجعل ارتفاع الصورة مجهولاً حتى تنتهي من التحميل، فيقفز النصّ أسفلها لحظة وصولها. الحلّ أن تحجز المكان مسبقاً، إمّا بسمتَي width وheight على الوسم نفسه ليحسب المتصفّح النسبة، وإمّا بارتفاع صريح على خلية الشبكة. في المشروع الذي نفكّكه بعد قليل، كل صورة داخل شبكة لها ارتفاع مقطوع: .pp-card img{width:100%;height:96px;object-fit:contain} و.bump-cell img{width:100%;height:120px;object-fit:cover} — والفرق بين contain وcover مقصود: صور المنتجات لا يجوز قصّها، وصور الألبوم يجوز.

6. اللمس مقابل المؤشّر

على الجوال لا يوجد «تحويم» (hover)، والأصابع أكبر من مؤشّر الفأرة. لذا:

  • اجعل مساحات اللمس لا تقلّ عن 44×44 بكسل.
  • لا تُخفِ وظائف أساسية خلف hover فقط.
  • باعِد بين العناصر القابلة للنقر لتجنّب الضغط الخاطئ.

وفي التطبيق العملي لا تُحقَّق قاعدة الـ44 دائماً بحرفيّتها. في الملف الذي نفكّكه، الأزرار المستقلّة تحترمها بوضوح: أزرار الدخول البديلة 46×46، وزرّ التنبيهات وزرّ المحفوظات 46×46، وأزرار معرض الصور 48×48. أمّا الأزرار الصغيرة داخل الشبكات الكثيفة — .hc-btn و.cal-arrow بمقاس 32×32، وخلية منتقي التاريخ بارتفاع 34px — فهي تحت الحدّ، ويُعوَّض ذلك بحشوة سخيّة حول الخلية ومسافة بينها وبين جيرانها. المقايضة واعية: سبعة أعمدة في أسبوع تقويمي لا تسمح بخلية 44px على هاتف بعرض 360px. ولاحظ أيضاً أن أزرار المعرض تتقلّص إلى 40×40 تحت 480px — لأن ثلاثة أزرار دائرية بمقاس 48 مع الصورة لم تتّسع.

7. اختبر على أجهزة حقيقية

محاكي المتصفّح ممتاز للبداية، لكنه لا يلتقط كل شيء: سرعة الجهاز الفعلية، سلوك لوحة المفاتيح، أو «النوتش». جرّب موقعك على هاتف حقيقي وبسرعات شبكة مختلفة. أبسط اختبار: افتح الموقع على جوالك وحاول إنجاز المهمّة الأساسية بإبهام واحد.

ما فعلناه في مشروع حقيقي: موقع واحد بلغتين وبلا نسخة للجوال

ما سبق نظرية معروفة، والأهمّ منها كيف تتصرّف حين يكون الموقع قائماً ويستخدمه ناس. الملف الذي نستشهد به هنا هو ملف الأنماط الرئيسي لموقع متابعة حمل بمنتدى داخلي: يعمل بالعربية والإنجليزية بالتبديل الفوري، ويُستخدم على الهاتف والحاسوب، وتُغلَّف نسخته لأندرويد داخل حاوية ويب — أي أن كل خطأ تخطيطي يظهر مرّتين، في المتصفّح وداخل التطبيق. حجم الملف 1099 سطراً يغطّي شاشة البداية وشاشة الدخول ولوحة البيانات والتقويم والمنتدى ومحرّر الرسائل ومعرض الصور. من فكرة «تغليف الموقع في تطبيق» تحدّثنا بالتفصيل في من موقع إلى تطبيق أندرويد بـ Capacitor.

الرقم الذي يفاجئ أول مرّة: من هذه الأسطر، 31 سطراً فقط يقع داخل استعلامات وسائط تخصّ التخطيط — أقل من ثلاثة في المئة. عدد الاستعلامات كلّها ثلاثة عشر، أحدها للطباعة لا للشاشة. ولا يعني هذا أن الموقع «قليل التجاوب»؛ بل يعني أن الوحدات المرنة والشبكات وخصائص التخطيط المنطقية استوعبت التغيّر قبل أن يحتاج أيٌّ منها إلى نقطة توقّف. كل نقطة توقّف لم تُكتب هي قاعدتان لن تصونهما، وحالة اختبار لن تنساها.

ومنهج «الجوال أولاً» هنا ليس شعاراً بل شكل القواعد نفسها. شريط المؤشّرات السريعة يبدأ عمودين: .strip-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:12px}، ثم يتوسّع للأكبر فقط: @media(min-width:560px){.strip-grid{grid-template-columns:repeat(4,1fr)}}. وشبكة الأدوات الصحّية مثلها: عمودان أساساً وأربعة عند 680px. وشريط أدوات المنتدى يبدأ flex-direction:column ثم يتّسع. النتيجة أن مسار الهاتف هو المسار الافتراضي، لا الاستثناء الذي يصحّح استثناءً.

الجزء الآخر من «الجوال أولاً» هو المساحات الآمنة، وقد حُلَّ بلا أي نقطة توقّف. في :root أربعة متغيّرات: --safe-top:env(safe-area-inset-top,0px) ومثلها للأسفل واليمين واليسار، والقيمة الاحتياطية صفر. ثم تُستعمل عشر مرّات في الملف، أبرزها main{padding:22px 16px calc(90px + var(--safe-bottom))} و.tab-bar{padding-bottom:var(--safe-bottom)}. على هاتف بشاشة منحوتة تُحجَز المسافة تلقائياً، وعلى الحاسوب تساوي صفراً فلا أثر لها. متغيّر واحد بديلاً عن استعلام لكل حالة.

ملاحظة عملية قبل أن تكمل: إن عدّلت نقطة توقّف ثم لم تظهر على الهاتف، فالمشكلة غالباً في الكاش لا في CSS. الملف يُخدَم بإصدار في الرابط، وأيّ تعديل بلا ترقية للإصدار يبقى غير مرئي لأسبوع أو لسنة حسب سياسة التخزين — التفاصيل في إدارة إصدارات Service Worker.

تفكيك clamp(): من أين تأتي الحدود الثلاثة

في 1099 سطراً هناك استدعاء واحد لـclamp()، وموضعه مقصود: الرقم الكبير الذي يعرض حالة المستخدمة الحاليّة في أعلى لوحة البيانات، وهو أبرز عنصر نصّي في الصفحة:

.pnow-val{font-family:'Amiri',serif;font-weight:800;font-size:clamp(22px,6.4vw,28px);line-height:1.3}

الحدود الثلاثة ليست أرقاماً ذوقية:

  • الحدّ الأدنى 22px: أضيق شاشة مدعومة 320px، والنصّ عربي بخطّ Amiri ويحتوي رقم الأسبوع وعدد الأيام، فأيّ حجم أصغر يجعله يلتفّ إلى سطر ثانٍ ويكسر ارتفاع البطاقة.
  • الحدّ الأعلى 28px: أكبر عنوان في الواجهة كلّها هو .setup-card h2 بحجم 28px. الحدّ الأعلى مساوٍ له بالضبط، لأن قيمة أكبر من أكبر عنوان تقلب سلّم الأولويات البصرية.
  • القيمة الوسطى 6.4vw: هي التي تقرّر أين يحدث النمو، لا مقداره فقط.

وهنا الحساب الذي يجعل الاختيار قابلاً للتكرار: الحدّ الأدنى يسري في كل عرض أقلّ من 22 ÷ 0.064 = 343.75px، والحدّ الأعلى يسري من 28 ÷ 0.064 = 437.5px وما فوق. أي أن النمو كلّه يحدث داخل شريط ضيّق من 344 إلى 437 بكسل — وهو تماماً نطاق عروض الهواتف الشائعة. على هاتف بعرض 360px تساوي القيمة 23.04px، وعلى هاتف 430px تصبح 27.52px، وعلى اللوحي والحاسوب تثبت عند 28px ولا تتضخّم. سطر واحد أدّى عمل أربعة استعلامات لو أردنا التدرّج يدوياً.

ومن هذا تُستخرج القاعدة: القيمة الوسطى = الحدّ الأدنى ÷ العرض الذي تريد أن يبدأ عنده النمو. اختر أوّلاً أضيق شاشة وأكبر حجم مقبول، ثم حدّد عند أي عرض يجب أن يتحرّك الرقم، والباقي قسمة.

الأخت الصغرى لـclamp() هي min()، وهي ببساطة قيمة بحدّ أعلى وحدّ متناسب بلا حدّ أدنى، وتظهر في الملف خمس مرّات بأربع قيم مختلفة، كلٌّ منها يلغي نقطة توقّف كانت ستُكتب:

.splash-logo{height:min(200px,50vw)}
.settings-panel{width:min(300px,90vw)}
.compose-win{width:min(680px,96vw)}
.partner-block:has(iframe){min-width:min(320px,100%)}

اقرأها هكذا: شعار شاشة البداية 200px على الحاسوب، ولا يتجاوز نصف العرض أبداً على شاشة ضيّقة. لوحة الإعدادات 300px، وعلى هاتف 320px تصبح 288px بهامش مريح. نافذة التحرير 680px على الحاسوب، وتلتصق بحدود الشاشة ناقص 4% على الجوال. أربع حالات كانت ستحتاج أربعة استعلامات، وصارت أربعة تصريحات.

قاعدة الحدود الثلاثة: ابدأ من أضيق شاشة تدعمها لتحديد الحدّ الأدنى (شرطه: لا التفاف نصّ ولا قصّ)، ومن أكبر عنوان في تصميمك لتحديد الحدّ الأعلى (شرطه: لا تنقلب الأولويات البصرية)، ثم احسب القيمة الوسطى قسمةً. وإذا لم تحتج حدّاً أدنى فاستعمل min() — أقصر وأصدق في التعبير عن نيّتك.

الاتجاهان معاً: تسعة أسطر منطقية مقابل خمسة وعشرين سطر تصحيح

الموقع يتحوّل بين العربية والإنجليزية بضغطة زرّ، وقيمة dir على وسم html تتغيّر معه. عملياً هذا يعني أن كل حافة كتبتها right أو left صراحةً صارت خطأً في أحد الاتجاهين. الخصائص المنطقية تحلّ المشكلة من جذرها لأنها تتكلّم بلغة «بداية السطر» و«نهايته» لا «يمين» و«يسار». في الملف تسعة تصريحات من هذا النوع، وكلّها في مواضع تُلاحظ فوراً لو انقلبت:

.due-info h3::before{inset-inline-start:0;width:3px}
.ad-int-close{inset-inline-end:-16px}
.wsn-lang-fab{position:fixed;inset-inline-start:10px}
.img-counter{margin-inline-start:6px}
.due-info h3{text-align:start}
.pdata-val{text-align:end}

الأول شريط لون بسماكة ثلاثة بكسلات على حافة بداية عنوان البطاقة، والثاني زرّ إغلاق في الزاوية الخارجية للنافذة البينية، والثالث زرّ اللغة الطافي. كلّها تنقلب مع اللغة بلا سطر إضافي واحد.

والثمن مرئي في الملف نفسه على الجهة المقابلة: خمسة وعشرون سطراً يبدأ بـhtml[dir="ltr"] مهمّتها الوحيدة إلغاء محاذاة يمنى كُتبت صراحةً في مكان آخر. أوضح مثال قاعدة واحدة تجمع ثلاثة عشر مُحدِّداً — نموذج الدخول، حقول الإعداد، عناصر قائمة الحساب، خطوات التثبيت، تسميات الحقول، عناوين الأقسام، القوائم المعلوماتية — لتقول جميعاً text-align:left. ولو كُتبت في الأصل text-align:start لما وُجدت القاعدة أصلاً.

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

لكن — وهذه النقطة التي يخطئ فيها كثيرون — ليست كل حافة يجب أن تصير منطقية. الأرقام تبقى من اليسار إلى اليمين مهما كانت لغة الواجهة، ولذلك بقيت هذه صريحةً بقصد:

.phone-num{direction:ltr;text-align:left;letter-spacing:.5px}
.otp-input{direction:ltr;text-align:center;letter-spacing:10px}
input[type="date"].field-input{direction:ltr;text-align:left;unicode-bidi:embed}

ومعها إصلاح خاصّ بمتصفّح iOS: input[type="date"]::-webkit-date-and-time-value{direction:ltr;text-align:left}، لأن الحقل داخل نموذج عربي كان يعرض قيمة التاريخ بترتيب معكوس بصرياً بينما القيمة المحفوظة صحيحة — عطل يظهر على جهاز واحد ولا يظهر في أي محاكي سطح مكتب. أمّا حقول التاريخ نفسها فتعرض التقويمين معاً، وذلك بحث منفصل شرحناه في التقويم الهجري: أم القرى.

وهناك بُعد ثالث للتجاوب لا علاقة له بعرض الشاشة إطلاقاً: طول الترجمة. النصّ الإنجليزي في أزرار الإجراءات السريعة أطول من العربي فكان يخرج من حدود الزرّ، فكُتب له استثناء واحد: html[dir="ltr"] .quick-actions .qa-lbl{font-size:11px;white-space:normal;line-height:1.22} — تصغير الخطّ والسماح بالالتفاف. لو اختبرت موقعك بلغة واحدة على عشرين مقاساً لن ترى هذا العطل مرّة واحدة.

من أين جاء كل رقم: نقاط التوقّف والشبكات والصفوف الأفقية

في الملف ست قيم متمايزة لنقاط التوقّف: 380، 480، 520، 560، 600، 680. ولا واحدة منها مقاس هاتف أو لوحي بعينه؛ كلّها عروض ظهر عندها انكسار فعلي. وهذا سردها مع سببها:

  • 600px — النقطة الرئيسية: تحمل ثلاثة استعلامات وحدها. عندها تنزل حشوة المحتوى من 22px 16px إلى 16px 12px (أي ثمانية بكسلات إضافية للنصّ على شاشة ضيّقة)، وتتحوّل نافذة التحرير إلى ملء الشاشة بلا زوايا مستديرة، وتفقد حاضنة المحتوى الإعلاني هامشها الجانبي. هذه هي «لحظة الانكسار» الحقيقية في التصميم: تحت 600px يصبح الهامش الجانبي ترفاً.
  • 560px: شبكتان بعمودين — حقول الإعداد ونموذج إضافة الموعد — تصبحان عموداً واحداً، لأن الحقل ينزل تحت 265px تقريباً وفيه تاريخ ووقت بأرقام لاتينية فيضيق فعلاً. وعند العرض نفسه تتقلّص بطاقة المنتج في الصفّ الأفقي من 122px إلى 108px وصورتها من 96px إلى 84px.
  • 520px: شريط تنبيه الزائرة يصبح flex-wrap:wrap وزرّه بعرض كامل، لأن نصّاً وزرّاً في صفّ واحد لم يتّسعا.
  • 480px: حشوة شريط التنقّل الجانبية تنزل إلى 12px والفراغ إلى 8px، وأزرار معرض الصور من 48×48 إلى 40×40.
  • 380px: شريط التبويبات السفلي يحمل ستة أزرار؛ تحت هذا العرض تلاصقت تسمياتها بحجم 9.5px، فصارت 9px والحشوة 8px 2px 10px. هذه نقطة توقّف وُلدت من عدد الأزرار لا من جهاز.
  • 680px: شبكة الأدوات الصحّية من عمودين إلى أربعة. و680 ليس مقاس جهاز، بل العرض الذي تتّسع فيه أربع بطاقات بمساحة مريحة للأرقام الكبيرة داخلها.

وملاحظة صريحة على الملف نفسه لا تجميلاً له: هو يخلط min-width وmax-width. الشبكتان الرئيسيتان مكتوبتان بمنهج الجوال أولاً (min-width)، أمّا حشوات المحتوى وشريط التبويبات فبمنهج معاكس (max-width). العملي أن تختار اتجاهاً واحداً وتلتزمه، لأن خلط الاتجاهين يجعل تعقّب مصدر قيمة واحدة أثناء التصحيح أطول ممّا يجب.

الشبكات في الملف أربعة عشر تصريح grid-template-columns، تنتظم في ثلاث عائلات لكلٍّ منها منطق مختلف:

الأولى: أعمدة متساوية ثابتة العدد للبطاقات الصغيرة. repeat(2,1fr) على الجوال وrepeat(4,1fr) على الأوسع. مناسبة حين تكون البطاقة رقماً وتسمية قصيرة، فالمساواة بينها مطلوبة بصرياً ولا معنى لتفاوت عروضها.

الثانية: أعمدة لا تتغيّر مهما ضاقت الشاشة. .cal-grid{grid-template-columns:repeat(7,1fr);gap:4px} بخلية ارتفاعها الأدنى 58px، ومثلها منتقي التاريخ .wdp-grid{repeat(7,1fr)} بخلايا 34px داخل نافذة 282px. أسبوع التقويم سبعة أيام ولا يُعاد ترتيبه لأي شاشة؛ الثبات هنا صواب لا تقصير. حين يكون عدد الأعمدة معنىً في المحتوى نفسه، لا تجعله متجاوباً.

الثالثة: شبكة يحدّد المتصفّح عدد أعمدتها. .weeks-grid{grid-template-columns:repeat(auto-fill,minmax(88px,1fr));gap:8px} لخلايا أسابيع الحمل. الحساب مفيد هنا: على هاتف بعرض 360px، بعد حشوة 12px من كل جانب، يبقى 336px — فتظهر ثلاثة أعمدة يتمدّد كلٌّ منها إلى نحو 106px. وعلى الحاسوب حيث أقصى عرض للمحتوى 948px تظهر تسعة أعمدة. من ثلاثة إلى تسعة بسطر واحد وبلا نقطة توقّف واحدة.

ولماذا auto-fill لا auto-fit؟ الفرق يظهر عند قلّة العناصر: auto-fit يطوي المسارات الفارغة فتتمدّد الخلايا الموجودة لتملأ الصفّ كلّه، فتصير خليّتان بعرض نصف الشاشة لكلٍّ منهما. auto-fill يُبقي المسارات فتحافظ الخلية على عرضها المتوقّع. في شبكة أسابيع كل خليّة فيها رقم صغير، ثبات المقاس مطلوب — وهذا قرار يُتّخذ حسب المحتوى لا حسب القاعدة العامّة.

أمّا حين لا يتّسع العرض للبطاقات، فالحلّ ليس ضغطها بل تمريرها. في الملف صفّ أفقي واحد فقط، ووحيدٌ أيضاً في استعمال overflow-x:

.pp-row{display:flex;gap:10px;overflow-x:auto;overflow-y:hidden;scroll-snap-type:x proximity;-webkit-overflow-scrolling:touch;scrollbar-width:thin}
.pp-card{flex:0 0 122px;scroll-snap-align:start}

المفتاح flex:0 0 122px: لا تنمو ولا تنكمش. البطاقة تحتفظ بعرض يجعل عنوانها مقروءاً (بسطرين كحدّ أقصى عبر -webkit-line-clamp:2) ويجعل صورتها واضحة، والصفّ يمرّر أفقياً بلقطات ناعمة عبر scroll-snap. أربع بطاقات مضغوطة إلى 70px لا يقرؤها أحد؛ بطاقتان ونصف مع تمرير أفضل بكثير. والقاعدة العامّة: الضغط مسموح إلى الحدّ الذي يبقى المحتوى فيه مفهوماً، وبعده انتقل إلى التمرير.

الخطأ الذي كلّفنا شريط تمرير أفقي — وثلاث مصائد اختبار

المصيدة الأولى: عنصر مرن يرفض الانكماش. اسم الموقع وسطره التعريفي يقعان داخل صفّ flex مع أزرار الحساب. العنوان العربي الطويل كان يدفع الشريط إلى عرض أكبر من الشاشة، فيظهر شريط تمرير أفقي في كل صفحة — والصفحة تبدو «مكسورة» بلا سبب ظاهر. السبب أن العنصر المرن قيمته الافتراضية min-width:auto، ومعناها «لا تنكمش تحت عرض محتواك»، فحتى overflow:hidden لا ينفع قبل معالجتها. الحلّ ثلاثة أسطر:

.nav-logo-text{min-width:0}
.nav-logo-text h1,.nav-logo-text p{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}

وأثر الدرس منتشر في الملف: min-width:0 مكتوبة ثماني مرّات على كل عنصر مرن يحمل نصّاً متغيّر الطول (جسم البطاقة، معلومات الموعد، نصّ التنبيه، صفّ القراءات)، وبجوارها 49 تصريح flex-shrink:0 للعناصر التي يجب ألّا تنكمش أبداً: الأيقونات، الأزرار الدائرية، وأعمدة النِّسب. القاعدة: في كل صفّ مرن حدّد صراحةً من ينكمش ومن لا ينكمش؛ ما تتركه للافتراضي سيقرّره طول نصّ لم تتوقّعه.

المصيدة الثانية — وهي الأهمّ: عنصر بموضع ثابت يقيس النافذة. معرض الصور مكتوب position:fixed;inset:0 والصورة داخله max-width:96vw;max-height:92vh، وشعار شاشة البداية height:min(200px,50vw). الآن افتح الصفحة داخل إطار أو أداة معاينة عرضها صفر أو ارتفاعها صفر: تصبح 96vw صفراً و50vw صفراً، فتختفي الصورة ويظهر المعرض صندوقاً أسود فارغاً، والشعار يغيب تماماً. الكود سليم مئة في المئة، والقياس هو الكاذب. أهدرنا وقتاً في تصحيح «عطل» لا وجود له، ثم فتحنا الصفحة في نافذة حقيقية فظهر كل شيء صحيحاً من أول مرّة. القاعدة: كل عنصر طافٍ أو مقيس بـvw/vh يُختبر في نافذة بمقاس معلوم؛ لا يُحكم عليه من معاينة مصغّرة أو إطار مطويّ.

المصيدة الثالثة: 100vh على iOS. نافذة التحرير على الشاشات ≤600px تصبح ملء الشاشة، ومكتوب لها height:100dvh;max-height:100dvh لا 100vh. السبب أن 100vh على iOS تساوي الشاشة كأن شريط أدوات المتصفّح غير موجود، فيهبط زرّ الإرسال أسفل الحدّ المرئي ولا يظهر إلا بعد تمرير لا يتوقّعه المستخدم. dvh تتبع الارتفاع المتاح فعلاً وتتغيّر مع ظهور الشريط واختفائه. هذه الوحدة مستعملة مرّة واحدة في الملف كلّه — في الموضع الوحيد الذي يحتاجها، وهو أي عنصر ملء الشاشة فيه زرّ عند حافته السفلى.

المصيدة الرابعة: height:auto على إطار بمقاس صارم. فتحات المحتوى في الموقع تستقبل إمّا صورة وإمّا شيفرة طرف ثالث. القاعدة العامّة img,ins{max-width:100%;height:auto} صحيحة تماماً للصور، لكن تطبيقها على iframe كان كارثة صغيرة: بنر بمقاس ثابت 320×50 يفقد ارتفاعه المعلن فيسقط إلى الارتفاع الافتراضي للإطار — 150 بكسل — فيظهر صندوقاً فارغاً بثلاثة أضعاف ارتفاعه الصحيح. لذلك صارت قاعدة الإطارات max-width:100% فقط، والارتفاع متروك لسمة العنصر. وأُضيف معها سطران يحلّان مشكلة الصندوق العريض حول بنر ضيّق:

.partner-block:has(iframe){width:fit-content;min-width:min(320px,100%)}
.partner-block:empty,.partner-block .partner-block-body:empty{display:none}

الأول يجعل الحاضنة تحتضن البنر بمقاسه، والثاني يمنع رسم فتحة بلا محتوى فعلي — لأن الفتحة الفارغة تحجز مساحة وتترك فراغاً محرجاً وسط النصّ. الخلاصة: حين تُدخل شيفرة طرف ثالث في تخطيطك، افترض أنها تأتي بمقاس صارم لا يقبل التمدّد، واستثنِها من قواعدك العامّة صراحةً. ولمن يدمج محتوى خارجياً، هناك طبقة أخرى من التعقيد في الأمان لا في التخطيط، فصّلناها في سياسة أمان المحتوى مع أطراف ثالثة.

قبل أن تصدّق أن تخطيطك معطوب: افتح الصفحة في نافذة حقيقية بمقاس معلوم لا في إطار معاينة. ثم اسأل ثلاثة أسئلة: هل العنصر مقيس بـvw/vh (فيصير صفراً في نافذة صفرية)؟ هل هو ملء الشاشة بـ100vh على iOS (فيهبط زرّه أسفل الحدّ المرئي)؟ هل داخل صفّ مرن عنصر نصّي بلا min-width:0 (فيدفع الصفحة لأوسع من الشاشة)؟ ثلاثة أسئلة تكشف أغلب «الأعطال» التي ليست أعطالاً.

أسئلة شائعة

هل أحتاج إطار عمل مثل Bootstrap للتجاوب؟ لا. Flexbox وGrid وclamp() تتيح تجاوباً كاملاً بكود أنظف وأخفّ. الأطر تسرّع البداية لكنها تأتي بحجم وأنماط جاهزة قد لا تحتاجها.

التصميم المتجاوب أم تطبيق منفصل للجوال؟ لموقع محتوى أو أعمال، التصميم المتجاوب أوفر وأسهل صيانةً وأفضل للسيو (رابط واحد لكل صفحة). التطبيق المنفصل لحالات خاصّة جداً.

لماذا يهتمّ Google بالتجاوب؟ لأنه يعتمد «فهرسة الجوال أولاً»: يقيّم نسخة الجوال من موقعك أساساً للترتيب. موقع غير متجاوب يخسر ترتيبه ومستخدميه معاً.

لماذا نقطة توقّف رئيسية واحدة أفضل من أربع؟ لأن النقطة الواحدة تحرس قراراً واحداً واضحاً: «هنا يفقد التخطيط هامشه الجانبي ويصبح المحتوى بعمود واحد». الأربع المأخوذة من مقاسات هواتف تحرس أجهزةً تتغيّر كل سنة، وتضاعف حالات الاختبار أربع مرّات، وتخلق فجوات لا يمرّ بها أحد. في الملف الذي فكّكناه، نقطة 600px تحمل ثلاثة تغييرات مترابطة، وباقي النقاط تفاصيل صغيرة كلٌّ منها وُلد من عطل مرئي محدّد.

هل clamp() تُغني عن نقاط التوقّف كلّها؟ لا، وهذا سوء فهم شائع. clamp() تضبط قيمة واحدة على محور واحد: حجم خطّ، حشوة، عرض عنصر. لكنها لا تعرف أن تُحوّل شبكة من عمودين إلى عمود، ولا أن تقلب صفّاً إلى عمود، ولا أن تُخفي عنصراً. للتغيير الهيكلي تبقى نقطة التوقّف هي الأداة، ودور الوحدات المرنة أن تقلّل عددها لا أن تُلغيه.

هل أستبدل كل right/left بخصائص منطقية؟ لا. استبدل ما يتبع اتجاه النصّ: المحاذاة، الهوامش الجانبية، مواضع الشارات والأزرار على الحواف. وأبقِ صريحاً ما لا يتبع اللغة: الأرقام وحقول الهاتف ورموز التحقّق وحقول التاريخ والوقت تبقى من اليسار إلى اليمين في كل الأحوال، وقلبها مع الواجهة عطل لا ميزة.

موقعي يبدو مكسوراً في أداة المعاينة وسليماً على الجوال — أيّهما أصدق؟ الجوال. أدوات المعاينة والإطارات قد تعطي عرضاً أو ارتفاعاً صفرياً، وكل قياس بـvw أو vh يصبح صفراً معها، فتغيب العناصر الطافية وتظهر صناديق فارغة. اختبر في نافذة حقيقية أوّلاً، ولا تُصلح شيئاً قبل أن تتأكّد أن العطل ليس في المقياس.

خلاصة

ابدأ بوسم viewport، صمّم من الجوال للأعلى، اعتمد الشبكات والوحدات المرنة، ضع نقاط توقّف عند «انكسار» التصميم لا حسب أجهزة بعينها، اجعل الصور والمساحات اللمسية متكيّفة، واختبر على جهاز حقيقي. التجاوب ليس ميزة إضافية اليوم، بل الحدّ الأدنى لموقع يُحترَم.

والدرس الذي أضافته التجربة الإنتاجية فوق ذلك: أفضل نقطة توقّف هي التي لم تكتبها. ملف بألف سطر ومئة، أقلّ من ثلاثة في المئة منه داخل استعلامات وسائط، ليس ملفاً مهملاً بل ملفاً نقلت فيه المرونة إلى الوحدات نفسها: clamp() بحدود محسوبة لا مقدّرة، وmin() بديلاً عن استعلام لكل نافذة، وشبكة auto-fill تعطي ثلاثة أعمدة على هاتف وتسعة على حاسوب بسطر واحد، وخصائص منطقية تجعل الاتجاهين مجّاناً بدل خمسة وعشرين سطر تصحيح. ثم — وهذا نصف العمل — لا تحكم على تخطيطك من إطار معاينة: العناصر الطافية والمقيسة بأبعاد النافذة تُختبر في نافذة حقيقية، وإلا صحّحت أعطالاً لم تحدث وتركت أعطالاً تحدث.

موقع متجاوب يجب أن يكون سريعاً أيضاً. اقرأ: دليل تحسين سرعة موقعك →