CSS Container Queries 실무 적용 가이드 — 뷰포트 말고 컴포넌트 기준으로 반응형 설계하기
저도 한때 이 상황을 겪었습니다. 미디어 쿼리를 열심히 작성했는데, 같은 카드 컴포넌트를 사이드바에 옮겨 놓으니 레이아웃이 완전히 어그러지는 거예요. "그냥 클래스 하나 더 추가하면 되지"라고 생각했다가, 어느 날 비슷한 미디어 쿼리가 열 개 넘게 쌓인 걸 보고 뭔가 근본적으로 잘못됐다는 걸 깨달았습니다. 이 글에서는 CSS Container Queries의 세 가지 쿼리 유형(Size Queries, Style Queries, Scroll-State Queries)과 @container, container-type 등의 핵심 문법, 실무에서 바로 활용할 수 있는 패턴, 그리고 흔히 빠지기 쉬운 함정까지 한 번에 정리합니다. CSS 미디어 쿼리를 써본 경험이 있다면 바로 이해할 수 있는 내용들입니다.
2025 State of CSS 조사에서 응답자의 41%가 이미 Size Queries를 사용해봤다고 답했고, Style Queries와 Scroll-State Queries는 각각 36%, 43%가 학습 목록에 올려둔 상태입니다. Size Queries 기준 주요 브라우저 지원율은 93%를 넘어 이제는 실무 적용을 주저할 이유가 없습니다. 다만 Style Queries와 Scroll-State Queries는 아직 지원 범위가 제한적이니, 이 부분은 본문에서 자세히 살펴보겠습니다.
핵심 개념
미디어 쿼리와 무엇이 다른가요?
미디어 쿼리는 브라우저 창(뷰포트)의 크기를 기준으로 스타일을 적용합니다. 그런데 현대 레이아웃에서는 같은 컴포넌트가 전체 너비 메인 영역에도, 좁은 사이드바에도 동시에 등장하는 경우가 흔합니다. 뷰포트 너비가 1200px일 때 사이드바 안의 카드도, 메인 영역의 카드도 똑같이 "wide" 스타일을 받게 되는데, 이게 바로 문제예요.
Container Queries는 방향을 바꿉니다. "브라우저 창이 얼마나 큰가?"가 아니라 "이 컴포넌트가 담긴 박스가 얼마나 큰가?" 를 묻습니다. 덕분에 컴포넌트가 어디에 놓이든 스스로 상황에 맞게 적응할 수 있습니다. 이것이 요즘 프론트엔드 설계에서 자주 언급되는 Intrinsic Design(내재적 설계)의 핵심 아이디어입니다.
📖 Intrinsic Design: Jen Simmons가 주도적으로 발전시킨 설계 철학으로, 컴포넌트가 자신이 놓인 컨텍스트(컨테이너 크기, 상태 등)를 스스로 파악하고 레이아웃을 결정하는 방식입니다. Container Queries와 함께 본격적으로 실용화됐습니다.
세 가지 쿼리 유형
Container Queries는 크게 세 가지로 나뉩니다.
| 유형 | 무엇을 묻나 | container-type 값 |
|---|---|---|
| Size Queries | 컨테이너의 너비·높이 | inline-size / size |
| Style Queries | CSS 커스텀 프로퍼티 값 | 별도 선언 불필요 (아래 설명 참고) |
| Scroll-State Queries | sticky 고정 여부, 스냅 상태 등 | scroll-state |
기본 문법: 두 단계면 됩니다
/* 1단계: 부모 요소에 컨테이너 선언 */
.card-wrapper {
container-type: inline-size;
container-name: card; /* 이름은 선택 사항이지만, 여러 컨테이너를 구분할 때 유용합니다 */
}
/* 2단계: 컨테이너 크기 기준으로 자식 스타일 지정 */
@container card (min-width: 400px) {
.card {
display: flex;
flex-direction: row;
}
}솔직히 처음엔 container-type을 어디 선언해야 하나 헷갈렸어요. 핵심은 스타일을 바꾸고 싶은 요소의 부모에 선언한다는 겁니다. @container 내부 스타일은 컨테이너 자신에게는 적용되지 않고 자식 요소에만 적용됩니다(Scroll-State Queries는 예외 — 아래에서 설명합니다).
📖
inline-sizevssize:inline-size는 가로 축(쓰기 방향 기준)만 쿼리 가능하고,size는 가로·세로 모두 가능합니다. 대부분의 경우inline-size로 충분하고,size는 높이 기반 레이아웃이 필요할 때만 씁니다.
Style Queries: CSS 변수로 조건 분기
Style Queries는 CSS 커스텀 프로퍼티(변수) 값을 조건으로 활용하는 방식입니다. 흥미로운 점은, 커스텀 프로퍼티 기반 Style Queries는 container-type을 따로 선언하지 않아도 동작합니다. container-name만 지정하면 충분합니다.
/* container-type 없이 container-name만으로 동작합니다 */
.btn-wrapper {
container-name: btn;
}
:root {
--btn-variant: default;
}
@container btn style(--btn-variant: danger) {
.btn {
background: red;
color: white;
}
}
@container btn style(--btn-variant: success) {
.btn {
background: green;
color: white;
}
}--btn-variant 값은 JavaScript에서 인라인 스타일로 변경하는 방식으로 트리거할 수 있습니다.
// 특정 버튼 래퍼에 danger 변형 적용
document.querySelector('.btn-wrapper').style.setProperty('--btn-variant', 'danger');
// 원래대로 되돌리기
document.querySelector('.btn-wrapper').style.removeProperty('--btn-variant');CSS 커스텀 프로퍼티를 조건으로 쓰는 방식이라 디자인 토큰 시스템이나 테마 전환과 궁합이 좋습니다. 단, 2026년 현재 Firefox에서는 아직 미지원이니 점진적 향상(Progressive Enhancement) 방식으로 적용하는 것을 권장합니다.
Scroll-State Queries: JavaScript 없는 스크롤 반응
.sticky-nav {
container-type: scroll-state;
position: sticky;
top: 0;
}
@container scroll-state(stuck: top) {
.sticky-nav {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
backdrop-filter: blur(8px);
}
}sticky 요소가 실제로 화면 상단에 고정됐을 때만 그림자가 생기는 패턴입니다. 예전엔 IntersectionObserver로 직접 구현해야 했던 걸 CSS 몇 줄로 처리할 수 있습니다.
💡 예외 — 컨테이너 자신에게 스타일 적용 가능: 일반 Size Queries와 달리, Scroll-State Queries에서는
@container scroll-state(...)내부에서 컨테이너 자신에게도 스타일을 적용할 수 있습니다. Scroll-State는 컨테이너 자신의 스크롤 상태를 반영하는 쿼리이기 때문에 이 예외가 적용됩니다.
Chrome 133(2025년 초)부터 지원이 시작됐고, Chrome 144에서는 scrolled 상태(마지막 스크롤 방향)까지 쿼리 가능해졌습니다.
실전 적용
예시 1: 사이드바와 메인 영역에서 동시에 쓰는 카드 컴포넌트
실무에서 가장 자주 맞닥뜨리는 상황입니다. 같은 카드가 좁은 사이드바(세로 레이아웃)와 넓은 메인 영역(가로 레이아웃)에 동시에 존재해야 할 때, Container Queries 없이는 별도 클래스를 만들거나 복잡한 미디어 쿼리를 중복 작성할 수밖에 없습니다. 저는 이 패턴을 도입하면서 카드 관련 미디어 쿼리를 절반 가까이 걷어낼 수 있었어요.
/* 컨테이너에 이름을 붙여두면 중첩 시 혼란을 막을 수 있습니다 */
.card-container {
container-type: inline-size;
container-name: card;
}
/* 기본: 좁은 공간 → 세로 레이아웃 */
.card {
display: block;
}
.card__image {
width: 100%;
aspect-ratio: 16 / 9;
}
/* 500px 이상: 가로 레이아웃으로 전환 */
@container card (min-width: 500px) {
.card {
display: grid;
grid-template-columns: 200px 1fr;
gap: 1rem;
}
.card__image {
aspect-ratio: 1;
}
}| 배치 위치 | 동작 방식 |
|---|---|
| 사이드바 | 컨테이너가 좁으므로 자동으로 세로 레이아웃 유지 |
| 메인 영역 | 컨테이너가 500px 이상이면 가로 그리드로 전환 |
| 분기 기준 | 뷰포트와 무관하게 컨테이너 너비만 봅니다 |
예시 2: Sticky 헤더에 스크롤 반응 그림자
JavaScript IntersectionObserver를 쓰지 않고도 sticky 헤더가 실제로 고정됐을 때만 그림자와 blur 효과를 줄 수 있습니다.
header {
container-type: scroll-state;
position: sticky;
top: 0;
background: white;
transition: box-shadow 0.2s ease, backdrop-filter 0.2s ease;
}
@container scroll-state(stuck: top) {
header {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
backdrop-filter: blur(8px);
background: rgba(255, 255, 255, 0.8);
}
}⚠️ 주의: Scroll-State Queries는 현재 Chrome/Edge/Opera만 지원합니다. Safari와 Firefox에서는 동작하지 않으므로, 그림자가 없어도 기능에 문제없는 방식으로 설계해두는 것이 좋습니다.
예시 3: 대시보드 위젯 적응형 레이아웃
차트 위젯이 배치된 컬럼 너비에 따라 표시 방식을 자동으로 조절하는 패턴입니다. 대시보드처럼 위젯을 드래그로 크기 조절하는 UI에서 특히 유용합니다.
.widget-container {
container-type: inline-size;
container-name: widget;
}
/* 기본: 숫자 요약만 표시 */
.widget__chart {
display: none;
}
.widget__summary {
font-size: 2rem;
font-weight: bold;
}
/* 중간 크기: 미니 차트 표시 */
@container widget (min-width: 300px) {
.widget__chart {
display: block;
height: 80px;
}
}
/* 넓은 크기: 풀 차트 + 범례 */
@container widget (min-width: 600px) {
.widget__chart {
height: 240px;
}
.widget__legend {
display: flex;
gap: 1rem;
}
}예시 4: 반응형 타이포그래피 + 멀티 컬럼
본문이 충분히 넓은 영역에 배치됐을 때 두 칼럼으로 전환하는 패턴입니다. 긴 글을 읽기 편한 너비로 분할해 가독성을 높일 수 있습니다. 단, 좁은 컨테이너에서는 단일 컬럼이 읽기 흐름에 자연스럽고, 무리하게 두 칼럼으로 쪼개면 오히려 불편해질 수 있으니 충분한 너비(700px 이상)에서만 적용하는 것을 권장합니다.
.article-body {
container-type: inline-size;
container-name: article;
}
.article-body p {
font-size: 1rem;
line-height: 1.7;
}
@container article (min-width: 700px) {
.article-body p {
font-size: 1.125rem;
column-count: 2;
column-gap: 2rem;
}
}장단점 분석
Container Queries가 만능 해결책은 아닙니다. 장단점을 파악하고 미디어 쿼리와 역할을 잘 분담하는 게 핵심입니다.
장점
| 항목 | 내용 |
|---|---|
| 컴포넌트 자율성 | 뷰포트와 무관하게 컴포넌트가 스스로 레이아웃 결정 |
| 높은 재사용성 | 동일 컴포넌트를 다양한 컨텍스트에 배치해도 각각 적절히 작동 |
| 유지보수 편의 | 스타일이 컴포넌트 단위로 캡슐화되어 전역 미디어 쿼리 충돌 감소 |
| 미디어 쿼리와 공존 | 전체 페이지 레이아웃은 미디어 쿼리, 컴포넌트 내부는 컨테이너 쿼리로 역할 분담 가능 |
| 컴포넌트 설계 친화적 | Atomic Design 같은 컴포넌트 단위 설계 방법론과 자연스럽게 어울림 |
| Size Queries 지원 93%+ | 모든 주요 브라우저에서 지원, 실무 적용 가능한 수준 |
단점 및 주의사항
아직 주의해야 할 부분도 있습니다. 특히 Style Queries와 Scroll-State Queries는 Firefox와 Safari 지원이 제한적이라 Production에 바로 도입하기엔 신중할 필요가 있습니다.
| 항목 | 내용 |
|---|---|
container-type 선언 필수 |
부모 요소에 반드시 명시해야 하며, 누락 시 쿼리가 동작하지 않음 |
<img> srcset/sizes 비호환 |
CSS 적용 전에 브라우저가 이미지를 선택하므로 반응형 이미지와 함께 쓸 수 없음 |
| Style Queries Firefox 미지원 | 커스텀 프로퍼티 기반 Style Queries는 현재 Firefox에서 동작하지 않음 |
| Scroll-State 제한적 지원 | Chrome/Edge/Opera만 지원, Safari·Firefox 미지원 |
| 컨테이너 자신에게 적용 불가 | @container 내부 스타일은 자식 요소에만 적용 (Scroll-State Queries는 예외) |
| 중첩 시 복잡도 증가 | 여러 겹 중첩 시 어느 컨테이너를 참조하는지 파악이 어려울 수 있음 |
대응 방안 요약:
container-type누락 방지 → 컴포넌트 래퍼 스타일에 기본값으로 포함해두는 패턴을 권장합니다.- 반응형 이미지 → 이미지 크기 결정은 기존
sizes속성 방식을 유지하는 것이 좋습니다. - Style Queries Firefox 미지원 → Progressive Enhancement 방식으로 적용하고, 핵심 기능은 CSS 변수 없이도 작동하게 설계하는 것을 권장합니다.
- Scroll-State 제한적 지원 → 기능 향상(enhancement)으로만 활용하고, 필수 스타일에는 사용을 지양합니다.
- 중첩 복잡도 →
container-name으로 명시적으로 이름을 지정하면 명확하게 관리할 수 있습니다.
📖 Progressive Enhancement(점진적 향상): 기본 기능은 모든 브라우저에서 동작하도록 설계하고, 최신 기능은 지원하는 브라우저에서만 추가 경험을 제공하는 방식입니다. Style Queries나 Scroll-State Queries처럼 지원 범위가 제한적인 기능에 특히 적합한 전략입니다.
실무에서 가장 흔한 실수
container-type을 자기 자신에게 선언하는 경우 — 스타일을 바꾸고 싶은 요소에 직접 선언하면, 자기 자신에게는@container스타일이 적용되지 않습니다. 항상 부모 래퍼에 선언하는 것이 맞습니다.- 미디어 쿼리를 전부 Container Queries로 교체하려는 시도 — 전체 페이지 레이아웃 전환(예: 데스크톱 ↔ 모바일 내비게이션)은 미디어 쿼리가 여전히 적합합니다. Container Queries는 컴포넌트 내부 적응에 집중하는 것이 좋습니다.
container-name없이 중첩 컨테이너 사용 — 이름 없이 중첩하면 가장 가까운 조상 컨테이너를 참조하는데, 의도치 않은 컨테이너를 참조할 수 있습니다. 복잡한 레이아웃에서는container-name을 명시적으로 지정하는 습관이 도움이 됩니다.
마치며
CSS Container Queries는 미디어 쿼리를 대체하는 게 아니라, 오랫동안 CSS가 답하지 못했던 "컴포넌트 수준의 반응형"이라는 질문에 드디어 답을 내놓은 기능입니다. Size Queries는 이미 93% 이상의 브라우저에서 지원하고 있어 지금 당장 실무에 도입할 수 있고, Style Queries와 Scroll-State Queries는 지원 범위가 계속 넓어지고 있어 지금 익혀두면 나중에 훨씬 자연스럽게 쓸 수 있습니다.
지금 바로 시작해볼 수 있는 3단계:
- 기존 프로젝트의 카드나 위젯 컴포넌트 하나를 골라, 부모 래퍼에
container-type: inline-size를 추가하고 기존 미디어 쿼리를@container쿼리로 변환해보시면 좋습니다. Chrome DevTools의 Elements 패널에서container-type적용 여부를 시각적으로 확인할 수 있어 디버깅도 편합니다. - Sticky 헤더가 있다면,
container-type: scroll-state와@container scroll-state(stuck: top)을 추가해 JavaScript 없이 스크롤 반응 스타일을 구현해보시기를 권장합니다. Chrome에서 바로 확인 가능합니다. - 디자인 시스템이나 공용 컴포넌트를 관리 중이라면, CSS 커스텀 프로퍼티와 Style Queries를 조합해 버튼이나 배지의 변형(variant)을 CSS만으로 제어하는 패턴을 시도해보시면 좋습니다. Firefox 지원이 완료되기 전까지는 기본 스타일을 먼저 정의하고 Style Queries를 enhancement로 추가하는 방식으로 안전하게 진행할 수 있습니다.
다음 글: CSS Cascade Layers(
@layer)와@scope를 Container Queries와 함께 활용해 컴포넌트 스타일을 완전히 격리하는 디자인 시스템 설계 패턴을 살펴봅니다.
참고 자료
- CSS container queries | MDN Web Docs
- Using container size and style queries | MDN Web Docs
- Using container scroll-state queries | MDN Web Docs
- Container Queries (Learn CSS) | web.dev
- CSS scroll-state() queries | Chrome for Developers
- Container queries in 2026: Powerful, but not a silver bullet | LogRocket
- When and how to choose between media queries and container queries | LogRocket
- Media Queries vs Container Queries | freeCodeCamp
- A Friendly Introduction to Container Queries | Josh W. Comeau
- Scrollytelling on Steroids With Scroll-State Queries | CSS-Tricks
- CSS Container Queries: Use-Cases And Migration Strategies | Smashing Magazine
- Scroll State Container Queries | nerdy.dev
- Directional CSS with scroll-state(scrolled) | una.im