CSS والتخطيطات

Container Queries: مكوّن يعرف حاويته، لا نافذة العرض

Container Queries: مكوّن يعرف حاويته، لا نافذة العرض

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

أنت لم ترتكب خطأً في CSS. استعلام الوسائط لديك يعمل تمامًا كما ينبغي له – لكنه يجيب عن سؤال خاطئ. @media (min-width: 768px) يسأل: «كم يبلغ عرض نافذة العرض؟». لكن بطاقة المنتج، لكي تبدو جيدة، لا ينبغي لها أبدًا أن تهتمّ بعرض نافذة العرض – بل ينبغي أن تهتمّ بمقدار المساحة التي منحها إياها العنصر الأب. هذان سؤالان مختلفان تمامًا، لكن CSS تعامل معهما طوال عقدين كأنهما سؤال واحد، لأنه لم يكن يملك وسيلة لطرح السؤال الآخر.

لماذا لا يكفي استعلام الوسائط

استعلام الوسائط هو استعلام عالمي. لا يهمّ إن كان مكوّنك جالسًا في عمود عرضه 1200 بكسل أو في بطاقة جانبية ضيّقة عرضها 260 – فـ@media (min-width: 768px) سيُعيد في الحالتين القيمة true نفسها، لأنه يسأل عن نافذة المتصفح، لا عن العنصر الأب للعنصر. فالمكوّن المبني اعتمادًا على استعلام الوسائط فقط يكون إذن صحيحًا فقط في السياق الذي صُمِّم فيه أصلًا – ويتوقف عن كونه صحيحًا بمجرد أن يُستخدم الكود نفسه في أي مكان آخر. هذا يهدم جوهريًا الوعد الذي يُفترض أن تقدّمه المكوّنات: إمكانية إعادة استخدامها بأمان في أي موضع من التخطيط.

قبل عام 2023، كان الحلّ البديل الوحيد الواقعي هو ResizeObserver في جافاسكريبت – تراقب العنصر، وتقيس عرضه عند كل تغيير، وتضع النتيجة في الحالة (state)، وتضيف صنفًا (class) في CSS بشكل مشروط. يعمل هذا، لكنه يُكلّف: حالة إضافية للمكوّن، وإعادة عرض (re-render) عند كل تغيير في الحجم، وكود يجب كتابته يدويًا لكل مكوّن على حدة، ولا يوجد أصلًا قبل أن تُنفَّذ جافاسكريبت – فعند العرض من جانب الخادم، سيظهر الإطار الأول التخطيط «بشكل أعمى» على أي حال، قبل أن يتمكّن ResizeObserver من قياس أي شيء.

jsx

// حلّ بديل من قبل Container Queries – يعمل، لكنه مُكلف
import { useEffect, useRef, useState } from "react";
const ProductCard = ({ product }) => {
const cardRef = useRef(null);
const [isWide, setIsWide] = useState(false);
useEffect(() => {
const el = cardRef.current;
const observer = new ResizeObserver((entries) => {
setIsWide(entries[0].contentRect.width > 320);
});
observer.observe(el);
return () => observer.disconnect();
}, []);
return (
<article ref={cardRef} className={`card ${isWide ? "card--wide" : ""}`}>
{/* ... */}
</article>
);
};

تحلّ Container Queries المشكلة نفسها دون سطر واحد من جافاسكريبت، ودون حالة، ودون إعادة عرض – لأن السؤال «كم من المساحة متاح لي» يعود إلى حيث كان CSS يطرحه دائمًا: إلى ملف الأنماط.

كيف يعمل الأمر فعليًا تحت السطح: الاحتواء (containment)، لا مجرّد صياغة جديدة

صياغة @container وحدها تبدو مثل @media باسم مختلف – لكن الفارق يكمن في مكان آخر، في ما يجب أن يحدث قبل أن يوافق المتصفح أصلًا على الإجابة عن استعلام كهذا. لكي يصبح عنصر ما «حاوية استعلام»، عليك أن تُعلن ذلك صراحةً:

css

.card-slot {
container-type: inline-size;
container-name: card;
}
@container card (min-width: 400px) {
.card {
grid-template-columns: 40% 1fr;
}
}

container-type: inline-size ليس مجرّد مفتاح لتفعيل وضع الاستعلام – إنه إعلان عن الاحتواء في CSS (CSS Containment)، مواصفة منفصلة تخبر المتصفح: «محتوى هذا العنصر لا يؤثر في حجمه على المحور الداخلي (inline)، فيمكنك التعامل معه بأمان كجزيرة مستقلة عند حساب التخطيط». هذا ليس تفصيلًا تقنيًا بلا أهمية – إنه شرط ضروري لكي توجد استعلامات الحاويات أصلًا دون أن تدخل في حلقة مفرغة. فلو سمح المتصفح للعنصر بأن يغيّر حجمه استجابةً لعرضه الخاص وأن يؤثر في هذا العرض عبر محتواه في آنٍ واحد، لنشأت تبعية دائرية: المحتوى يغيّر حجم الحاوية ← تغيّر الحجم يُفعّل @container ← القواعد الجديدة تغيّر المحتوى ← المحتوى يغيّر حجم الحاوية من جديد. الاحتواء يقطع هذه الحلقة من جذورها، بفرضه أن حجم الحاوية على محور معيّن يُحدَّد بمعزل عمّا بداخلها.

لهذا انعكاس عملي ملموس عند اختيار قيمة container-type:

  • inline-size – احتواء على المحور الداخلي فقط (عادة الأفقي). أمّا الارتفاع فيظلّ حرًّا يُستمَدّ من المحتوى. هذه القيمة التي تلجأ إليها في 95% من الحالات – تمامًا كما في مثال بطاقة المنتج.
  • size – احتواء على المحورين معًا. يتوقف العنصر تمامًا عن الاعتماد على محتواه في تحديد حجمه – ما يعني أنه يجب عليك منحه ارتفاعًا صريحًا (عبر height أو aspect-ratio)، وإلا فسيقطعه الاحتواء عن ارتفاعه الطبيعي المستمَدّ من المحتوى، فيهبط الارتفاع الفعلي إلى صفر.
  • normal – القيمة الافتراضية، لا احتواء، والعنصر ليس حاوية استعلام.

container-name اختياري، لكنه عادة تستحق أن تكتسبها – فبدونه، يستعلم @container (min-width: 400px) عن أقرب سلف يمثّل حاوية، أيًّا كان. في مكوّن مسطّح لا ضرر من ذلك، لكن في تخطيط متداخل (بطاقة داخل لوحة، واللوحة داخل عمود – وكلاهما حاوية) فإن الحاوية المسمّاة تحدّد بوضوح أيّ سلف تستهدفه بالضبط، بدلًا من الاعتماد على القرب العشوائي في شجرة DOM.

مثال عملي: بطاقة واحدة، سياقان، ودون جافاسكريبت إطلاقًا

نعود إلى بطاقة المنتج من المقدّمة – هذه المرّة مبنيّة بحيث يُعرَض المكوّن نفسه بشكل صحيح سواء في الشبكة الواسعة أو في الشريط الجانبي الضيّق، دون أي معرفة مسبقة بالمكان الذي وُضع فيه.

jsx

const ProductCard = ({ product }) => (
<div className="product-card-slot">
<article className="product-card">
<img
src={product.image}
alt={product.name}
className="product-card__image"
/>
<div className="product-card__body">
<h3 className="product-card__title">{product.name}</h3>
<p className="product-card__price">{product.price} ر.ق</p>
<button type="button" className="product-card__cta">
أضف إلى السلة
</button>
</div>
</article>
</div>
);
export default ProductCard;

css

/* العنصر الأب للمكوّن يُعلن نفسه حاوية استعلام */
.product-card-slot {
container-type: inline-size;
container-name: product-card;
}
/* التخطيط الافتراضي: الصورة فوق المحتوى – آمن في المساحات الضيقة */
.product-card {
display: grid;
grid-template-columns: 1fr;
gap: 12px;
}
.product-card__image {
width: 100%;
aspect-ratio: 4 / 3;
object-fit: cover;
border-radius: 8px;
}
/* فوق عرض 360px الخاص بالحاوية، لا نافذة العرض – بدّل إلى تخطيط أفقي */
@container product-card (min-width: 360px) {
.product-card {
grid-template-columns: 40% 1fr;
align-items: center;
}
.product-card__image {
aspect-ratio: 1 / 1;
height: 100%;
}
}

القرار الحاسم هنا مخفيّ في البنية، لا في CSS: container-type موضوعة على .product-card-slot، أي العنصر الأب للبطاقة، لا على .product-card نفسها. هذا ليس صدفة ولا أسلوبًا زائدًا عن الحاجة – فالمواصفة تستبعد صراحةً إمكانية أن يستعلم العنصر عن حجمه الخاص عبر @container (المشكلة الدائرية نفسها مجددًا: لا يمكن للعنصر أن يُعرِّف الحاوية وأن يُصمَّم استجابةً لحجمها في آنٍ واحد). لهذا فالنمط المعتاد والمتكرر هو غلاف-حاوية رقيق من الخارج، والمكوّن الفعلي في الداخل، مُصمَّم داخل كتلة @container. ملف CSS نفسه، إن وُضع مرّة في شبكة المنتجات (حيث عرض الفتحة 480 بكسل) ومرّة في الشريط الجانبي (حيث عرض الفتحة 240 بكسل)، سينتج عنه تخطيطان مختلفان وصحيحان – دون خاصية إضافية (prop)، ودون صنف مُعدِّل، ودون سطر واحد من جافاسكريبت.

وحدات الحاوية: مرونة دون نافذة العرض

جلبت Container Queries معها جزءًا ثانيًا، أقلّ شهرة، من المواصفة نفسها: وحدات نسبية إلى الحاوية، لا إلى نافذة العرض – cqw (1% من عرض الحاوية)، وcqh (1% من ارتفاعها)، وcqi وcqb (1% على المحور الداخلي/الكتلي، أي صحيحة أيضًا في اللغات ذات الكتابة العمودية)، وcqmin/cqmax، المماثلتان لـvmin/vmax لكن محسوبتان من البُعد الأصغر أو الأكبر للحاوية.

استخدامها الطبيعي هو الطباعة المرنة داخل المكوّن، بمعزل عن عرض شاشة المستخدم في تلك اللحظة:

css

.product-card__title {
/* يتغيّر حجمه مع عرض البطاقة، لا مع عرض الشاشة */
font-size: clamp(1rem, 0.85rem + 2cqi, 1.375rem);
}

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

الفخاخ والممارسات الجيدة

  • container-type: size دون ارتفاع صريح يلتهم المحتوى. الاحتواء على المحور الكتلي يقطع العنصر عن ارتفاعه الطبيعي المستمَدّ من أبنائه – إن لم تحدّد height أو aspect-ratio، ستتقلّص الحاوية فعليًا إلى ارتفاع صفري، وتختفي أبناؤها بصريًا رغم بقائها في DOM. في الغالبية الساحقة من الحالات تكفي inline-size ولا تظهر هذه المشكلة أصلًا.
  • لا يمكن للحاوية أن تستعلم عن نفسها. إذا كنت تواجه مشكلة أن @container «لا يعمل»، فأوّل ما يجب الاشتباه به هو هذا بالضبط: container-type والمحدِّد المُصمَّم عبر @container موجودان على العنصر نفسه. افصل بينهما – container-type على الغلاف، والأنماط المستهدَفة على الابن، تمامًا كما في مثال بطاقة المنتج أعلاه.
  • استعلامات الحاويات لا تحلّ محلّ استعلامات الوسائط – بل تكمّلها. استعلام الوسائط لا يزال الأداة الصحيحة للقرارات على مستوى الصفحة (مثل: هل تظهر الشريط الجانبي أصلًا، أم تُبدّل التنقّل إلى قائمة همبرغر). أمّا استعلام الحاوية فيتولّى القرارات على مستوى المكوّن، أينما حلّ ذلك المكوّن. التخطيط الجيّد في 2026 يستخدم عادةً كليهما معًا، كلٌّ حيث يناسبه السؤال الذي يطرحه فعليًا.
  • لا تجعل من كل عنصر div حاوية «احتياطًا». للاحتواء تكلفة حقيقية على محرّك العرض – إنه إشارة «تعامل معي كوحدة تخطيط معزولة»، مفيدة حيث لديك فعلًا مكوّن يستجيب لحجمه، لا إعداد افتراضي لشجرة DOM بأكملها.
  • دعم المتصفحات لم يعد مشكلة. بلغت container-type و@container مرتبة Baseline (متاحة على نطاق واسع) في مطلع 2023 – وتعمل بشكل أصلي في Chrome وSafari وFirefox دون polyfills ودون @supports. وحدات cqw/cqi وبقية العائلة تتمتع بالتغطية نفسها تمامًا.

الخلاصة

المكوّن القابل لإعادة الاستخدام ليس ذلك الذي يمتلك عددًا كافيًا من الخصائص (props) والأصناف المُعدِّلة ليتعامل يدويًا مع كل مكان قد يُوضع فيه – بل ذلك الذي يعرف بنفسه كم من المساحة لديه، ويستجيب لذلك دون مساعدة من أحد. كان استعلام الوسائط طوال عقدين الأداة الوحيدة في CSS للتصميم المتجاوب، فأسأنا استخدامه بشكل طبيعي حتى في الحالات التي كان الأمر فيها يتعلّق فعليًا بالعنصر الأب، لا بنافذة العرض. Container Queries ليست صياغة بديلة أخرى للآلية نفسها – إنها سؤال مختلف، يُطرح في الموضع الصحيح من شجرة DOM، مدعوم باحتواء يضمن ألّا تدخل الإجابة عنه في حلقة مفرغة أبدًا. الكود في هذا المقال – مكوّن واحد، وملف CSS واحد، ودون جافاسكريبت إطلاقًا – يعمل بشكل صحيح في الشبكة، وفي الشريط الجانبي، وفي أي مكان آخر لا تعرف بعد أنك ستضعه فيه يومًا.