← HTML & CSS/Container Queries: компонентно-адаптивный дизайн#32 из 383← ПредыдущийСледующий →+20 XP
Полезно по теме:Гайд: старт в frontendПрактика: DOM и событияТермин: DOMМаршрут: старт с нуля

Container Queries: компонентно-адаптивный дизайн

Media queries отвечают на вопрос «какой размер у окна?». Container queries отвечают на вопрос «какой размер у моего контейнера?». Это принципиальное отличие: компонент теперь адаптируется к своему окружению, а не к размеру экрана.

Проблема, которую решают Container Queries

Представь карточку товара. В sidebar она занимает 200px, на главной странице — 400px. С media queries ты не можешь сделать разный стиль для одного и того же компонента в разных местах страницы — breakpoint один на весь экран.

Container queries решают это элегантно.

Базовый синтаксис

/* 1. Объявить контейнер */
.card-wrapper {
  container-type: inline-size;  /* Отслеживать ширину */
  container-name: card;         /* Необязательное имя */
}

/* Сокращение */
.card-wrapper {
  container: card / inline-size;
}

/* 2. Применить стили по размеру контейнера */
@container (min-width: 400px) {
  .card {
    flex-direction: row;    /* Горизонтальная карточка в широком контейнере */
  }
}

/* Именованный контейнер */
@container card (min-width: 400px) {
  .card__image {
    width: 40%;
  }
}

container-type: значения

/* inline-size — отслеживать только ширину (чаще всего это нужно) */
.wrapper { container-type: inline-size; }

/* size — отслеживать и ширину, и высоту */
.widget  { container-type: size; }

/* normal — не создавать контейнер (по умолчанию) */
.div     { container-type: normal; }

Единицы cqi и cqb

Аналог vw/vh, но относительно контейнера:

@container (min-width: 300px) {
  .card__title {
    font-size: 5cqi;    /* 5% от inline-size контейнера */
  }
}

/* cqi — container query inline (ширина в ltr) */
/* cqb — container query block (высота в ltr) */
/* cqw, cqh — явно ширина/высота */
/* cqmin, cqmax — меньшее/большее из cqw и cqh */

Media queries vs Container queries

/* Media query — реагирует на экран */
@media (min-width: 768px) {
  .card { flex-direction: row; }
  /* Но что если карточка в sidebar шириной 300px на экране 1200px? */
}

/* Container query — реагирует на контейнер */
.card-wrapper { container-type: inline-size; }

@container (min-width: 400px) {
  .card { flex-direction: row; }
  /* Работает правильно в любом месте страницы */
}

Реальный пример: адаптивная карточка

.card-container {
  container: product-card / inline-size;
}

.product-card {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

/* Узкая карточка (менее 350px) */
.product-card__image { width: 100%; }

/* Широкая карточка — горизонтальная раскладка */
@container product-card (min-width: 350px) {
  .product-card {
    flex-direction: row;
    align-items: center;
  }

  .product-card__image {
    width: 40%;
    flex-shrink: 0;
  }
}

/* Очень широкая — ещё больше деталей */
@container product-card (min-width: 600px) {
  .product-card__description { display: block; }
  .product-card__rating      { display: flex; }
}

Поддержка браузеров

Container queries поддерживаются во всех современных браузерах с 2023 года. Для старых браузеров используй прогрессивное улучшение: стили без @container работают как базовые, стили внутри @container — как улучшение.

Примеры

Симуляция container query matching: сравниваем ширину элемента с breakpoints и вызываем коллбэки

// Симуляция Container Queries через ResizeObserver (как работает браузер внутри)
const container = document.createElement('div')
container.style.cssText = `
  width: 300px;
  border: 2px solid #7b2ff7;
  padding: 16px;
  font-family: sans-serif;
  border-radius: 8px;
  resize: horizontal;
  overflow: auto;
`
document.body.appendChild(container)

const card = document.createElement('div')
card.id = 'adaptive-card'
card.style.cssText = 'background: #f0f4ff; padding: 12px; border-radius: 6px;'
card.textContent = 'Адаптивная карточка'
container.appendChild(card)

const info = document.createElement('pre')
info.style.cssText = 'margin-top: 8px; font-size: 12px; background: #1a202c; color: #a0aec0; padding: 8px; border-radius: 4px;'
document.body.appendChild(info)

// Набор правил, аналогичных @container
const containerRules = [
  {
    minWidth: 0,
    apply() {
      card.style.flexDirection = 'column'
      card.style.display = 'block'
      card.style.background = '#fef3c7'
      return 'xs: вертикальная раскладка (0px+)'
    }
  },
  {
    minWidth: 300,
    apply() {
      card.style.display = 'flex'
      card.style.flexDirection = 'row'
      card.style.gap = '12px'
      card.style.background = '#dbeafe'
      return 'sm: горизонтальная раскладка (300px+)'
    }
  },
  {
    minWidth: 500,
    apply() {
      card.style.background = '#d1fae5'
      return 'lg: широкая карточка (500px+)'
    }
  },
]

function applyContainerRules(element) {
  const width = element.offsetWidth
  let activeRule = null

  for (const rule of containerRules) {
    if (width >= rule.minWidth) {
      activeRule = rule
    }
  }

  if (activeRule) {
    const desc = activeRule.apply()
    info.textContent = `Ширина контейнера: ${width}px\nАктивное правило: ${desc}`
  }
}

// Применяем при изменении размера
const ro = new ResizeObserver(() => applyContainerRules(container))
ro.observe(container)
applyContainerRules(container)

console.log('Попробуй растянуть контейнер — он имеет resize: horizontal')
console.log('Container queries работают именно так: ResizeObserver + условные стили')

Container Queries: компонентно-адаптивный дизайн

Media queries отвечают на вопрос «какой размер у окна?». Container queries отвечают на вопрос «какой размер у моего контейнера?». Это принципиальное отличие: компонент теперь адаптируется к своему окружению, а не к размеру экрана.

Проблема, которую решают Container Queries

Представь карточку товара. В sidebar она занимает 200px, на главной странице — 400px. С media queries ты не можешь сделать разный стиль для одного и того же компонента в разных местах страницы — breakpoint один на весь экран.

Container queries решают это элегантно.

Базовый синтаксис

/* 1. Объявить контейнер */
.card-wrapper {
  container-type: inline-size;  /* Отслеживать ширину */
  container-name: card;         /* Необязательное имя */
}

/* Сокращение */
.card-wrapper {
  container: card / inline-size;
}

/* 2. Применить стили по размеру контейнера */
@container (min-width: 400px) {
  .card {
    flex-direction: row;    /* Горизонтальная карточка в широком контейнере */
  }
}

/* Именованный контейнер */
@container card (min-width: 400px) {
  .card__image {
    width: 40%;
  }
}

container-type: значения

/* inline-size — отслеживать только ширину (чаще всего это нужно) */
.wrapper { container-type: inline-size; }

/* size — отслеживать и ширину, и высоту */
.widget  { container-type: size; }

/* normal — не создавать контейнер (по умолчанию) */
.div     { container-type: normal; }

Единицы cqi и cqb

Аналог vw/vh, но относительно контейнера:

@container (min-width: 300px) {
  .card__title {
    font-size: 5cqi;    /* 5% от inline-size контейнера */
  }
}

/* cqi — container query inline (ширина в ltr) */
/* cqb — container query block (высота в ltr) */
/* cqw, cqh — явно ширина/высота */
/* cqmin, cqmax — меньшее/большее из cqw и cqh */

Media queries vs Container queries

/* Media query — реагирует на экран */
@media (min-width: 768px) {
  .card { flex-direction: row; }
  /* Но что если карточка в sidebar шириной 300px на экране 1200px? */
}

/* Container query — реагирует на контейнер */
.card-wrapper { container-type: inline-size; }

@container (min-width: 400px) {
  .card { flex-direction: row; }
  /* Работает правильно в любом месте страницы */
}

Реальный пример: адаптивная карточка

.card-container {
  container: product-card / inline-size;
}

.product-card {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

/* Узкая карточка (менее 350px) */
.product-card__image { width: 100%; }

/* Широкая карточка — горизонтальная раскладка */
@container product-card (min-width: 350px) {
  .product-card {
    flex-direction: row;
    align-items: center;
  }

  .product-card__image {
    width: 40%;
    flex-shrink: 0;
  }
}

/* Очень широкая — ещё больше деталей */
@container product-card (min-width: 600px) {
  .product-card__description { display: block; }
  .product-card__rating      { display: flex; }
}

Поддержка браузеров

Container queries поддерживаются во всех современных браузерах с 2023 года. Для старых браузеров используй прогрессивное улучшение: стили без @container работают как базовые, стили внутри @container — как улучшение.

Примеры

Симуляция container query matching: сравниваем ширину элемента с breakpoints и вызываем коллбэки

// Симуляция Container Queries через ResizeObserver (как работает браузер внутри)
const container = document.createElement('div')
container.style.cssText = `
  width: 300px;
  border: 2px solid #7b2ff7;
  padding: 16px;
  font-family: sans-serif;
  border-radius: 8px;
  resize: horizontal;
  overflow: auto;
`
document.body.appendChild(container)

const card = document.createElement('div')
card.id = 'adaptive-card'
card.style.cssText = 'background: #f0f4ff; padding: 12px; border-radius: 6px;'
card.textContent = 'Адаптивная карточка'
container.appendChild(card)

const info = document.createElement('pre')
info.style.cssText = 'margin-top: 8px; font-size: 12px; background: #1a202c; color: #a0aec0; padding: 8px; border-radius: 4px;'
document.body.appendChild(info)

// Набор правил, аналогичных @container
const containerRules = [
  {
    minWidth: 0,
    apply() {
      card.style.flexDirection = 'column'
      card.style.display = 'block'
      card.style.background = '#fef3c7'
      return 'xs: вертикальная раскладка (0px+)'
    }
  },
  {
    minWidth: 300,
    apply() {
      card.style.display = 'flex'
      card.style.flexDirection = 'row'
      card.style.gap = '12px'
      card.style.background = '#dbeafe'
      return 'sm: горизонтальная раскладка (300px+)'
    }
  },
  {
    minWidth: 500,
    apply() {
      card.style.background = '#d1fae5'
      return 'lg: широкая карточка (500px+)'
    }
  },
]

function applyContainerRules(element) {
  const width = element.offsetWidth
  let activeRule = null

  for (const rule of containerRules) {
    if (width >= rule.minWidth) {
      activeRule = rule
    }
  }

  if (activeRule) {
    const desc = activeRule.apply()
    info.textContent = `Ширина контейнера: ${width}px\nАктивное правило: ${desc}`
  }
}

// Применяем при изменении размера
const ro = new ResizeObserver(() => applyContainerRules(container))
ro.observe(container)
applyContainerRules(container)

console.log('Попробуй растянуть контейнер — он имеет resize: horizontal')
console.log('Container queries работают именно так: ResizeObserver + условные стили')

Задание

Создай адаптивную карточку с помощью Container Queries. Оберни карточку в контейнер с `container-type: inline-size`. При ширине контейнера меньше 350px карточка должна быть вертикальной (flex-direction: column), при 350px и больше — горизонтальной (flex-direction: row). Попробуй разместить одну и ту же карточку в узком и широком контейнерах.

Подсказка

`container-type: inline-size` — отслеживать ширину контейнера. `container-name: card` — имя для обращения в `@container`. В `@container card (min-width: 350px)` задай `flex-direction: row` для горизонтальной раскладки.

Загружаем среду выполнения...
Загружаем AI-помощника...