استعلامات الحاوية (Container Queries): مكوّن يعرف حاويته، لا نافذة العرض
استعلامات الحاوية (Container Queries): مكوّن يعرف حاويته، لا نافذة العرض
لديك بطاقة منتج. صورة فوق العنوان، سعر، وزر «أضف إلى السلة». في الشبكة الرئيسية للمتجر، وعند عرض شاشة يتجاوز 768 بكسل، تحوّلها استعلامات الوسائط إلى تخطيط أفقي – الصورة إلى اليسار، والمحتوى إلى اليمين، لأن هذا يستغل المساحة المتاحة بشكل أفضل. يعمل الأمر بشكل ممتاز. ثم يرى مدير المنتج البطاقة نفسها ويريدها في الشريط الجانبي الضيق «منتجات موصى بها»، بجانب المحتوى الرئيسي للمقال. تضع المكوّن دون أي تعديل – فينكسر. التخطيط الأفقي، المصمَّم أصلًا لعرض يتجاوز 700 بكسل، يحاول أن يتسع في عمود عرضه 260 بكسلًا فقط. تنضغط الصورة إلى شريط رفيع، وينكسر النص إلى كلمة واحدة في كل سطر.
أنت لم ترتكب خطأً في CSS. استعلام الوسائط لديك يعمل تمامًا كما ينبغي له – لكنه يجيب عن سؤال خاطئ. @media (min-width: 768px) يسأل: «ما عرض نافذة العرض (viewport)؟». أما بطاقة المنتج، لكي تبدو جيدة، فلا ينبغي أبدًا أن يهمها عرض نافذة العرض – بل ينبغي أن يهمها مقدار المساحة التي حصلت عليها من أصلها (parent). هذان سؤالان مختلفان تمامًا، عاملهما CSS كسؤال واحد لعقدين من الزمن، لأنه لم يكن يملك وسيلة لطرح السؤال الثاني.
لماذا لا تكفي استعلامات الوسائط
استعلام الوسائط هو استعلام عام (global). لا يهم إن كان مكوّنك يجلس في عمود عرضه 1200 بكسل أو في بطاقة جانبية ضيقة عرضها 260 بكسلًا – فـ@media (min-width: 768px) سيُعيد القيمة true نفسها في الحالتين، لأنه يسأل عن نافذة المتصفح، لا عن أصل العنصر (parent). المكوّن المبني كليًا على استعلامات الوسائط يكون إذن صحيحًا فقط في السياق الذي صُمِّم من أجله أصلًا – ويتوقف عن كونه صحيحًا حالما ينتقل الكود نفسه إلى أي مكان آخر. هذا يقوّض بشكل جوهري الوعد الذي يُفترض أن تقدّمه المكوّنات: إمكانية إعادة استخدامها بأمان في أي موضع من التخطيط.
قبل عام 2023، كان الحل البديل الوحيد العملي هو ResizeObserver في جافاسكريبت – تراقب العنصر، وتقيس عرضه عند كل تغيير، وتضع النتيجة في الحالة (state)، ثم تضيف صنف CSS بشكل شرطي. يعمل هذا، لكنه يكلّف: حالة إضافية للمكوّن، وإعادة رسم (re-render) عند كل تغيير في الحجم، وكود يجب كتابته يدويًا لكل مكوّن على حدة، ولا يكون موجودًا إلا بعد تنفيذ جافاسكريبت – لذا في العرض من جهة الخادم (SSR)، ستُظهر اللقطة الأولى التخطيط «بشكل أعمى» على أي حال، قبل أن يتمكن 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"><imgsrc={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;}/* عند تجاوز عرض الحاوية (لا نافذة العرض) 360 بكسلًا – بدّل إلى تخطيط أفقي */@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)، ودون صنف معدِّل، ودون سطر واحد من جافاسكريبت.
وحدات الحاوية: مرونة دون نافذة عرض
جلبت استعلامات الحاوية معها جزءًا ثانيًا، أقل شهرة، من المواصفة نفسها: وحدات نسبية إلى الحاوية، لا إلى نافذة العرض – 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 للتصميم المتجاوب، لذا بالغنا بطبيعة الحال في استخدامها أيضًا حيث كان الأمر يتعلق فعليًا بالأصل، لا بنافذة العرض. استعلامات الحاوية ليست نسخة أخرى من الصياغة لنفس الآلية – إنها سؤال مختلف، يُطرح في الموضع الصحيح من شجرة DOM، مدعوم بالاحتواء الذي يضمن ألا تدخل الإجابة عنه في حلقة لا نهائية أبدًا. الكود الوارد في هذا المقال – مكوّن واحد، ملف CSS واحد، بلا جافاسكريبت إطلاقًا – يعمل بشكل صحيح في الشبكة، وفي الشريط الجانبي، وفي أي مكان آخر لا تعلم بعد أنك ستضعه فيه يومًا ما.