Baseline | Widely available *
Chrome, Edge, Firefox, Safari에서 지원돼요.
이 기능은 잘 확립되어 있고 많은 기기와 브라우저 버전에서 작동해요. 2023년 2월부터 모든 브라우저에서 사용 가능했어요.
* 이 기능의 일부 부분은 지원 수준이 다를 수 있어요.
@container CSS at-rule은 컨테인먼트 컨텍스트에 스타일을 적용하는 조건부 그룹 규칙이에요. 스타일 선언은 조건에 의해 필터링되고 조건이 참이면 컨테이너 내의 요소에 적용돼요. 조건은 쿼리된 컨테이너 크기, <style-feature>, 또는 스크롤 상태가 변경될 때 평가돼요.
조건은 container-name과 <container-query> 중 하나 또는 둘 다 지정해야 해요.
container-name 프로퍼티는 쿼리 컨테이너 이름 목록을 지정해요. 이 이름들은 @container 규칙이 대상으로 하는 컨테이너를 필터링하는 데 사용돼요. <container-query>의 컨테이너 기능은 선택된 컨테이너에 대해 평가돼요. <container-name>이 지정되지 않으면, <container-query> 기능은 일치하는 container-type을 가진 가장 가까운 조상 쿼리 컨테이너에 대해 평가돼요. <container-query>가 지정되지 않으면, 이름 있는 컨테이너가 선택돼요.
/* 크기 쿼리(<size-query>)를 사용하는 경우 */
@container (width > 400px) {
h2 {
font-size: 1.5em;
}
}
/* 선택 사항인 컨테이너 이름(<container-name>)을 포함하는 경우 */
@container tall (height > 30rem) {
p {
line-height: 1.6;
}
}
/* 컨테이너 이름(<container-name>)만 사용하는 경우 (쿼리는 생략 가능) */
@container sidebar {
h2 {
background: blue;
}
}
/* 스크롤 상태(<scroll-state>)를 사용하는 경우 */
@container scroll-state(scrollable: top) {
.back-to-top-link {
visibility: visible;
}
}
/* 앵커 기반(anchored) 쿼리를 사용하는 경우 */
@container anchored(fallback: bottom) {
.infobox::before {
content: "▲";
bottom: 100%;
top: auto;
}
}
/* 컨테이너 이름과 스크롤 상태를 결합하는 경우 */
@container sticky-heading scroll-state(stuck: top) {
h2 {
background: purple;
color: white;
}
}
/* 하나의 조건 안에 여러 쿼리를 사용하는 경우 */
@container (width > 400px) and style(--responsive: true) {
h2 {
font-size: 1.5em;
}
}
/* 조건 리스트 (여러 조건 중 하나라도 만족하면 적용) */
@container card (width > 400px), style(--responsive: true), scroll-state(stuck: top) {
h2 {
font-size: 1.5em;
}
}
<container-condition>
<container-name>과 <container-query> 중 하나 또는 둘 다를 의미해요. 조건이 true가 되면 스타일 시트에 정의된 스타일들이 적용됩니다.
<container-name> 선택 사항
쿼리할 컨테이너의 이름이에요. <ident> 형식으로 지정합니다. 쿼리 결과가 true이면, 선언된 스타일이 해당 컨테이너의 자손 요소들에게 적용돼요.
<container-query> 선택 사항
컨테이너의 크기, 스타일 특징, 스크롤 상태, 또는 적용된 position-try 폴백(fallback)이 변경될 때 쿼리 컨테이너를 평가하는 기준들의 집합이에요.
컨테이너 조건을 정의할 때 아래와 같은 논리 키워드를 사용할 수 있어요.
and: 두 개 이상의 조건을 모두 만족해야 할 때 결합합니다.or: 두 개 이상의 조건 중 하나라도 만족할 때 결합합니다.not: 조건을 부정합니다. 컨테이너 쿼리당 단 하나의 'not' 조건만 허용되며, and나 or 키워드와 함께 사용할 수 없어요.@container (width > 400px) and (height > 400px) {
/* 두 조건 모두 만족할 때 적용될 스타일 */
}
@container (width > 400px) or (height > 400px) {
/* 둘 중 하나라도 만족하면 적용될 스타일 */
}
@container not (width < 400px) {
/* 너비가 400px보다 작지 않을 때 적용될 스타일 */
}
컨테이너 컨텍스트는 container-name 속성을 사용해서 이름을 붙일 수 있어요.
.post {
container-name: sidebar;
container-type: inline-size;
}
이걸 축약형인 container 속성을 써서 container: <name> / <type> 형태로 쓸 수도 있답니다.
.post {
container: sidebar / inline-size;
}
컨테이너 쿼리에서 container-name 속성은 쿼리 컨테이너 이름이 일치하는 컨테이너들만 필터링해서 스타일을 적용할 때 사용해요.
@container sidebar (width > 400px) {
/* 이름이 'sidebar'인 컨테이너가 400px보다 넓을 때만 적용 */
}
사용법이나 이름 지정 제한에 대한 자세한 내용은 container-name 문서를 참고하세요.
<container-condition> 쿼리에는 크기, 스크롤 상태, 앵커 기반 컨테이너 디스크립터가 포함됩니다.
<container-condition>은 괄호로 묶인 하나 이상의 불리언(boolean) 크기 쿼리를 포함할 수 있어요. 크기 쿼리는 크기 디스크립터, 값, 그리고 디스크립터에 따른 비교 연산자로 구성되죠. 쿼리는 항상 콘텐츠 박스(content box)를 기준으로 측정합니다. 여러 조건을 포함하는 문법은 @media의 크기 특징 쿼리와 같아요.
@container (min-width: 400px) {
/* … */
}
@container (orientation: landscape) and (width > 400px) {
/* … */
}
@container (15em <= block-size <= 30em) {
/* … */
}
aspect-ratio
컨테이너의 너비 대비 높이 비율인 종횡비(aspect-ratio)이며, <ratio> 값으로 표현해요.
block-size
컨테이너의 블록 방향 크기(block-size)이며, <length> 값으로 표현해요.
height
컨테이너의 높이이며, <length> 값으로 표현해요.
inline-size
컨테이너의 인라인 방향 크기(inline-size)이며, <length> 값으로 표현해요.
orientation
컨테이너의 방향(orientation)이며, landscape(가로) 또는 portrait(세로) 중 하나입니다.
width
컨테이너의 너비이며, <length> 값으로 표현해요.
스크롤 상태 컨테이너 디스크립터는 <container-condition> 내부에서 scroll-state() 함수의 인자로 지정됩니다. 예를 들면 이렇죠.
@container scroll-state(scrollable: top) {
/* … */
}
@container scroll-state(scrolled: block-end) {
/* … */
}
@container scroll-state(stuck: inline-end) {
/* … */
}
@container scroll-state(snapped: both) {
/* … */
}
스크롤 상태 디스크립터에서 지원하는 키워드에는 물리적(physical) 값과 흐름 상대적(flow relative) 값이 포함돼요.
scrollable
스크롤바를 드래그하거나 트랙패드 제스처를 사용하는 등 사용자가 시작한 스크롤을 통해 해당 방향으로 스크롤이 가능한지 확인해요. 즉, 해당 방향에 스크롤해서 볼 수 있는 넘치는 콘텐츠가 있는지를 묻는 거죠. 사용할 수 있는 값은 다음과 같아요.
none
컨테이너가 스크롤 컨테이너가 아니거나 어느 방향으로도 스크롤할 수 없습니다.
top
위쪽 가장자리 방향으로 스크롤할 수 있습니다.
right
오른쪽 가장자리 방향으로 스크롤할 수 있습니다.
bottom
아래쪽 가장자리 방향으로 스크롤할 수 있습니다.
left
왼쪽 가장자리 방향으로 스크롤할 수 있습니다.
x
왼쪽이나 오른쪽, 혹은 양쪽 모두로 수평 스크롤이 가능합니다.
y
위쪽이나 아래쪽, 혹은 양쪽 모두로 수직 스크롤이 가능합니다.
block-start
블록 시작(block-start) 방향으로 스크롤할 수 있습니다.
block-end
블록 끝(block-end) 방향으로 스크롤할 수 있습니다.
inline-start
인라인 시작(inline-start) 방향으로 스크롤할 수 있습니다.
inline-end
인라인 끝(inline-end) 방향으로 스크롤할 수 있습니다.
block
블록 방향(시작 또는 끝)으로 스크롤이 가능합니다.
inline
인라인 방향(시작 또는 끝)으로 스크롤이 가능합니다.
테스트를 통과하면 @container 블록 내부의 규칙이 스크롤 컨테이너의 자손 요소들에게 적용돼요.
방향에 상관없이 컨테이너가 스크롤 가능한지만 확인하고 싶다면 none 값에 not 연산자를 쓰면 됩니다.
@container not scroll-state(scrollable: none) {
/* … */
}
scrolled
컨테이너가 특정 방향으로 최근에 스크롤되었는지 확인해요. 사용할 수 있는 값들은 다음과 같습니다.
none
컨테이너가 스크롤 컨테이너가 아니거나 이전에 어느 방향으로도 스크롤된 적이 없습니다.
top
가장 최근에 위쪽 가장자리 방향으로 스크롤되었습니다.
right
가장 최근에 오른쪽 가장자리 방향으로 스크롤되었습니다.
bottom
가장 최근에 아래쪽 가장자리 방향으로 스크롤되었습니다.
left
가장 최근에 왼쪽 가장자리 방향으로 스크롤되었습니다.
x
가장 최근에 수평 방향 중 한 곳으로 스크롤되었습니다.
y
가장 최근에 수직 방향 중 한 곳으로 스크롤되었습니다.
block-start
가장 최근에 블록 시작 방향으로 스크롤되었습니다.
block-end
가장 최근에 블록 끝 방향으로 스크롤되었습니다.
inline-start
가장 최근에 인라인 시작 방향으로 스크롤되었습니다.
inline-end
가장 최근에 인라인 끝 방향으로 스크롤되었습니다.
block
가장 최근에 블록 방향 중 한 곳으로 스크롤되었습니다.
inline
가장 최근에 인라인 방향 중 한 곳으로 스크롤되었습니다.
테스트가 true를 반환하면 @container 블록의 규칙이 스크롤 컨테이너 자손들에게 적용됩니다.
방향 상관없이 최근에 스크롤이 발생했는지만 보려면 역시 not scroll-state(scrolled: none)을 쓰면 돼요.
snapped
컨테이너가 특정 축을 따라 스크롤 스냅(scroll snap) 컨테이너 조상에 스냅될 예정인지 확인해요. 사용 가능한 키워드는 다음과 같아요.
none
컨테이너가 조상 스크롤 컨테이너의 스냅 대상(snap target)이 아닙니다. 이 경우 스타일이 적용되지만, 실제 스냅 대상인 컨테이너들은 스타일이 적용되지 않아요.
x
수평 스크롤 스냅 대상입니다.
y
수직 스크롤 스냅 대상입니다.
block
블록 축 스크롤 스냅 대상입니다.
inline
인라인 축 스크롤 스냅 대상입니다.
both
수평과 수직 양쪽 모두의 스냅 대상입니다. 한쪽 축에만 스냅되어 있으면 만족하지 않아요.
none이 아닌 snapped 쿼리를 평가하려면 조상 중 하나가 scroll-snap-type이 none이 아니어야 해요. 반면 snapped: none은 스크롤 조상이 없어도 매칭됩니다.
평가는 스크롤 스냅 컨테이너에서 scrollsnapchanging 이벤트가 발생할 때 일어납니다.
stuck
position: sticky가 적용된 컨테이너가 스크롤 조상의 가장자리에 '붙어(stuck)' 있는지 확인해요. 사용 가능한 값은 다음과 같습니다.
none
어떤 가장자리에도 붙어있지 않습니다. position: sticky가 설정되지 않은 컨테이너도 이 조건에 매칭돼요.
top
컨테이너의 위쪽 가장자리에 붙어있습니다.
right
컨테이너의 오른쪽 가장자리에 붙어있습니다.
bottom
컨테이너의 아래쪽 가장자리에 붙어있습니다.
left
컨테이너의 왼쪽 가장자리에 붙어있습니다.
block-start
블록 시작 가장자리에 붙어있습니다.
block-end
블록 끝 가장자리에 붙어있습니다.
inline-start
인라인 시작 가장자리에 붙어있습니다.
inline-end
인라인 끝 가장자리에 붙어있습니다.
none이 아닌 stuck 쿼리를 쓰려면 반드시 position: sticky가 설정되어 있고 스크롤 컨테이너 안에 있어야 해요.
인접한 두 축의 값이 동시에 매칭될 수도 있어요 (예: 위쪽이면서 왼쪽).
@container scroll-state((stuck: top) and (stuck: left)) {
/* … */
}
하지만 서로 반대되는 가장자리가 동시에 매칭될 수는 없겠죠?
앵커 기반 디스크립터는 anchored() 함수의 인자로 지정됩니다.
@container anchored(fallback: top) {
/* … */
}
@container anchored(fallback: flip-block flip-inline) {
/* … */
}
@container anchored(fallback: --custom-fallback) {
/* … */
}
fallback
position-try-fallbacks 속성을 통해 지정된 특정 위치 시도 폴백(position-try fallback)이 현재 활성화되어 있는지 확인해요. 현재 해당 폴백이 사용 중이라면 테스트를 통과하고 내부 스타일이 적용됩니다.
@container =
@container <container-condition># { <block-contents> }
<container-condition> =
[ <container-name>? <container-query>? ]!
<container-name> =
<custom-ident>
<container-query> =
not <query-in-parens> |
<query-in-parens> [ [ and <query-in-parens> ]* | [ or <query-in-parens> ]* ]
<query-in-parens> =
( <container-query> ) |
( <size-feature> ) |
style( <style-query> ) |
scroll-state( <scroll-state-query> ) |
<general-enclosed>
<style-query> =
not <style-in-parens> |
<style-in-parens> [ [ and <style-in-parens> ]* | [ or <style-in-parens> ]* ] |
<style-feature>
<scroll-state-query> =
not <scroll-state-in-parens> |
<scroll-state-in-parens> [ [ and <scroll-state-in-parens> ]* | [ or <scroll-state-in-parens> ]* ] |
<scroll-state-feature>
<general-enclosed> =
[ <function-token> <any-value>? ) ] |
[ ( <any-value>? ) ]
<style-in-parens> =
( <style-query> ) |
( <style-feature> ) |
<general-enclosed>
<style-feature> =
<style-feature-plain> |
<style-feature-boolean> |
<style-range>
<scroll-state-in-parens> =
( <scroll-state-query> ) |
( <scroll-state-feature> ) |
<general-enclosed>
<style-feature-plain> =
<style-feature-name> : <style-feature-value>
<style-feature-boolean> =
<style-feature-name>
<style-range> =
<style-range-value> <mf-comparison> <style-range-value> |
<style-range-value> <mf-lt> <style-range-value> <mf-lt> <style-range-value> |
<style-range-value> <mf-gt> <style-range-value> <mf-gt> <style-range-value>
<style-range-value> =
<custom-property-name> |
<style-feature-value>
<mf-comparison> =
<mf-lt> |
<mf-gt> |
<mf-eq>
<mf-lt> =
'<' '='?
<mf-gt> =
'>' '='?
<mf-eq> =
'='
이 구문은 CSS Conditional Rules Module Level 5, Media Queries Level 5의 최신 표준을 따릅니다.
제목과 텍스트가 있는 카드 컴포넌트 예시를 볼게요.
<div class="post">
<div class="card">
<h2>Card title</h2>
<p>Card content</p>
</div>
</div>
container-type 속성을 사용해서 컨테이너 컨텍스트를 만듭니다. 여기서는 .post 클래스에 inline-size 값을 주었죠. 이제 @container 규칙을 써서 컨테이너 너비가 650px보다 좁을 때 .card 요소에 스타일을 적용할 수 있어요.
/* 인라인 크기 기반의 컨테이너 컨텍스트 */
.post {
container-type: inline-size;
}
/* 컨테이너 너비가 650px보다 좁으면 스타일 적용 */
@container (width < 650px) {
.card {
width: 50%;
background-color: lightgray;
font-size: 1em;
}
}
위와 같은 카드 컴포넌트 구조에서 이름을 붙여볼게요.
먼저 container-type과 container-name으로 이름을 정의합니다.
.post {
container-type: inline-size;
container-name: summary;
}
그다음 쿼리에 이름을 추가해서 해당 컨테이너를 타겟팅합니다.
@container summary (width >= 400px) {
.card {
font-size: 1.5em;
}
}
하나의 쿼리문 안에서 여러 컨테이너를 동시에 타겟팅할 수는 없지만, 쿼리를 중첩(nesting)하면 같은 효과를 낼 수 있어요.
아래 쿼리는 summary라는 이름의 컨테이너가 400px보다 넓고, 동시에 그 조상 컨테이너가 800px보다 넓을 때 스타일을 적용해요.
@container summary (width > 400px) {
@container (width > 800px) {
/* 두 조건을 모두 만족할 때 적용될 스타일 */
}
}
컨테이너 요소의 '계산된 스타일(computed style)'을 평가할 수도 있어요. 이걸 컨테이너 스타일 쿼리라고 부르며 style() 함수를 사용합니다.
@container style(--themeBackground),
not style(background-color: red),
style(color: green) and style(background-color: transparent),
style(--themeColor: blue) or style(--themeColor: purple) {
/* 스타일 조건에 맞는 경우 적용 */
}
style()의 인자인 <style-feature>는 유효한 CSS 선언, 속성, 또는 커스텀 속성 이름이 될 수 있어요.
style(--themeBackground))은 해당 속성의 계산된 값이 초기값과 다를 때 true가 됩니다.style(--accent-color: blue))라면 컨테이너의 실제 계산된 값과 일치할 때 true가 돼요.참고: 커스텀 속성값이
blue일 때, 만약@property로 해당 속성을 색상 타입으로 정의해두지 않았다면 브라우저가#0000ff와 같은 16진수 코드와 제대로 비교하지 못할 수 있어요.
단축 속성(shorthand)을 쿼리할 때는 그 안에 포함된 모든 개별 속성(longhand)이 일치해야 true가 됩니다. 또한 !important는 스타일 쿼리 내에서 허용되지만 실제로는 무시돼요. revert나 revert-layer 전역 키워드는 스타일 쿼리에서 유효하지 않은 값으로 처리되어 결과가 false가 됩니다.
자세한 예제는 컨테이너 스크롤 상태 쿼리 사용하기 가이드를 참고해 보세요.
자세한 예제는 앵커 기반 컨테이너 쿼리 사용하기 가이드를 참고해 보세요.