1. 소개
이 섹션은 규범적이지 않습니다.
이 모듈은 스타일 규칙을 다른 스타일 규칙 안에 중첩하여, 내부 규칙의 선택자가 외부 규칙에 의해 매칭된 요소를 참조할 수 있도록 하는 기능을 설명합니다. 이 기능을 통해 관련 스타일을 CSS 문서 내에서 하나의 구조로 집계할 수 있어, 가독성과 유지보수성이 향상됩니다.
1.1. 모듈 상호작용
이 모듈은 [CSS21] 파서 모델을 확장하는 새로운 파서 규칙을 도입합니다. 또한 [SELECTORS4] 모듈을 확장하는 선택자를 도입합니다.
1.2. 값
이 명세는 새로운 속성이나 값을 정의하지 않습니다.
1.3. 동기
중간 정도로 복잡한 웹 페이지의 CSS는 관련된 콘텐츠를 스타일링하기 위해 많은 중복을 포함하는 경우가 많습니다. 예를 들어, 다음은 [CSS-COLOR-3] 모듈의 한 버전에 대한 CSS 마크업의 일부입니다:
table.colortable td{ text-align : center; } table.colortable td.c{ text-transform : uppercase; } table.colortable td:first-child, table.colortable td:first-child+td{ border : 1 px solid black; } table.colortable th{ text-align : center; background : black; color : white; }
중첩을 사용하면 관련 스타일 규칙을 다음과 같이 그룹화할 수 있습니다:
table.colortable{ & td{ text-align : center; &.c{ text-transform : uppercase} &:first-child, &:first-child + td{ border : 1 px solid black} } & th{ text-align : center; background : black; color : white; } }
중복을 제거하는 것뿐만 아니라, 관련 규칙의 그룹화는 결과 CSS의 가독성과 유지보수성을 높여줍니다.
2. 스타일 규칙 중첩
스타일 규칙은 다른 스타일 규칙 안에 중첩될 수 있습니다.
이러한 중첩 스타일 규칙은 일반적인 스타일
규칙과 동일하게 동작하며—
중첩 스타일 규칙은 일반 스타일 규칙과 동일하지만, 선택자가 식별자 또는 함수 표기법으로 시작할 수 없습니다. 또한, 중첩 스타일 규칙은 상대 선택자를 사용할 수 있습니다.
.foo{ color : red; .bar{ color : blue; } }
유효하며, 다음과 동일합니다:
.foo{ color : red; } .foo .bar{ color : blue; }
중첩 규칙은 중첩 선택자를 사용하여 부모 규칙의 매칭된 요소를 직접 참조하거나, 상대 선택자 문법을 사용하여 "자손" 이외의 관계를 지정할 수 있습니다.
.foo{ color : red; &:hover{ color : blue; } } /* 다음과 동일합니다: */ .foo{ color : red; } .foo:hover{ color : blue; }
.foo{ color : red; + .bar{ color : blue; } } /* 다음과 동일합니다: */ .foo{ color : red; } .foo + .bar{ color : blue; }
div{ color : red; input{ margin : 1 em ; } } /* "input"이 식별자이기 때문에 유효하지 않습니다. */
이러한 선택자도 작성할 수 있지만, 약간 다르게 표현해야 합니다:
div{ color : red; & input{ margin : 1 em ; } /* 유효, 더 이상 식별자로 시작하지 않음 */ :is ( input) { margin : 1 em ; } /* 유효, 콜론으로 시작하며 이전 규칙과 동일합니다. */ }
중첩 규칙 선택자에 제한이 있는 이유는 무엇입니까?
스타일 규칙을 단순하게 다른 스타일 규칙 안에 중첩하는 것은, 불행히도, 모호합니다—
예를 들어, 파서가 color:hover ...를 처음 본다면,
이것이 color 속성
(잘못된 값으로 설정...)인지,
아니면 <color>
요소에 대한 선택자인지 알 수 없습니다.
지원하는 속성을 기준으로 식별하는 것도 불가능합니다;
구현이 지원하는 속성이 바뀌면 파싱 결과도 바뀌게 되기 때문입니다.
중첩 스타일 규칙이 식별자로 시작하는 것을 금지하면 이 문제가 해결됩니다—
일부 브라우저가 아닌 중첩 규칙 구현에서는 이러한 요구 사항을 적용하지 않습니다. 대부분의 경우 결국 속성과 선택자를 구분할 수 있지만, 그렇게 하려면 파서에 무제한 선행 분석이 필요합니다; 즉, 파서는 어느 쪽으로 해석해야 하는지 알기 전까지 알 수 없는 양의 콘텐츠를 보관해야 할 수도 있습니다. CSS는 지금까지 파싱에 소량의, 알려진 양의 선행 분석만 요구해왔으며, 이를 통해 더 효율적인 파싱 알고리즘을 사용할 수 있습니다. 따라서 무제한 선행 분석은 일반적으로 브라우저 구현에서 받아들여지지 않습니다.
2.1. 문법
스타일 규칙의 내용은 이제 기존의 선언뿐만 아니라 중첩 스타일 규칙과 at-규칙도 허용합니다.
중첩 스타일 규칙은 비중첩 규칙과 다음과 같이 다릅니다:
-
중첩 스타일 규칙은 프렐루드로 <relative-selector-list>를 허용합니다 (단순히 <selector-list>만 허용하는 것이 아니라). 상대 선택자는 모두 중첩 선택자가 나타내는 요소를 기준으로 해석됩니다.
-
<relative-selector-list> 내의 선택자가 결합자로 시작하지 않으면서 중첩 선택자를 포함하는 경우, 이는 비-상대 선택자로 해석됩니다.
중첩 스타일 규칙이 어떻게 파싱되는지에 대한 정확한 세부사항은 [CSS-SYNTAX-3]에서 정의되어 있습니다.
CSSWG는 현재 파싱 선행분석의 결과를 탐구하고 있으며, 이에 따라 허용되는 문법이 조정될 수 있습니다. [이슈 #7961]
잘못된 중첩 스타일 규칙은 그 내용과 함께 무시되지만, 부모 규칙을 무효화하지는 않습니다.
/* & 단독 사용 가능 */ .foo{ color : blue; & > .bar{ color : red; } > .baz{ color : green; } } /* .foo { color: blue; } .foo > .bar { color: red; } .foo > .baz { color: green; } */ /* 합성 선택자 내에서 부모 선택자를 세분화함 */ .foo{ color : blue; &.bar{ color : red; } } /* .foo { color: blue; } .foo.bar { color: red; } */ /* 리스트의 여러 선택자는 모두 부모 기준 */ .foo, .bar{ color : blue; + .baz, &.qux{ color : red; } } /* .foo, .bar { color: blue; } :is(.foo, .bar) + .baz, :is(.foo, .bar).qux { color: red; } */ /* 한 선택자 내에서 & 여러 번 사용 가능 */ .foo{ color : blue; & .bar & .baz & .qux{ color : red; } } /* .foo { color: blue; } .foo .bar .foo .baz .foo .qux { color: red; } */ /* &는 선택자 앞에 있을 필요 없음 */ .foo{ color : red; .parent &{ color : blue; } } /* .foo { color: red; } .parent .foo { color: blue; } */ .foo{ color : red; :not ( &) { color : blue; } } /* .foo { color: red; } :not(.foo) { color: blue; } */ /* 상대 선택자를 사용하면, 초기에 &가 자동으로 암시됨 */ .foo{ color : red; + .bar + &{ color : blue; } } /* .foo { color: red; } .foo + .bar + .foo { color: blue; } */ /* 다소 특이하지만 & 단독 사용도 가능 */ .foo{ color : blue; &{ padding : 2 ch ; } } /* .foo { color: blue; } .foo { padding: 2ch; } // 또는 .foo { color: blue; padding: 2ch; } */ /* 역시 특이하지만, &를 반복할 수도 있음 */ .foo{ color : blue; &&{ padding : 2 ch ; } } /* .foo { color: blue; } .foo.foo { padding: 2ch; } */ /* 부모 선택자는 임의로 복잡할 수 있음 */ .error, #404{ &:hover > .baz{ color : red; } } /* :is(.error, #404):hover > .baz { color: red; } */ .ancestor .el{ .other-ancestor &{ color : red; } } /* .other-ancestor :is(.ancestor .el) { color: red; } */ /* 중첩 선택자도 복잡할 수 있음 */ .foo{ &:is ( .bar, &.baz) { color : red; } } /* .foo :is(.bar, .foo.baz) { color: red; } */ /* 여러 단계의 중첩은 선택자를 "쌓아올림" */ figure{ margin : 0 ; > figcaption{ background : hsl ( 0 0 % 0 % /50 % ); > p{ font-size : .9 rem ; } } } /* figure { margin: 0; } figure > figcaption { background: hsl(0 0% 0% / 50%); } figure > figcaption > p { font-size: .9rem; } */ /* Cascade Layers 예시 */ @layer base{ html{ block-size : 100 % ; & body{ min-block-size : 100 % ; } } } /* @layer base { html { block-size: 100%; } html body { min-block-size: 100%; } } */ /* Cascade Layers 중첩 예시 */ @layer base{ html{ block-size : 100 % ; @layer support{ & body{ min-block-size : 100 % ; } } } } /* @layer base { html { block-size: 100%; } } @layer base.support { html body { min-block-size: 100%; } } */ /* Scoping 예시 */ @scope ( .card) to( > header) { :scope{ inline-size : 40 ch ; aspect-ratio : 3 /4 ; > header{ border-block-end : 1 px solid white; } } } /* @scope (.card) to (> header) { :scope { inline-size: 40ch; aspect-ratio: 3/4; } :scope > header { border-block-end: 1px solid white; } } */ /* Scoping 중첩 예시 */ .card{ inline-size : 40 ch ; aspect-ratio : 3 /4 ; @scope ( &) to( > header > *) { :scope > header{ border-block-end : 1 px solid white; } } } /* .card { inline-size: 40ch; aspect-ratio: 3/4; } @scope (.card) to (> header > *) { :scope > header { border-block-end: 1px solid white; } } */
다음과 같은 경우는 유효하지 않습니다:
/* 선택자가 식별자로 시작 */ .foo{ color : blue; div{ color : red; } }
예를 들어, 한 컴포넌트가 .foo 클래스를 사용하고 중첩된 컴포넌트가 .fooBar를 사용할 때, 이를 Sass에서 다음과 같이 작성할 수 있습니다:
.foo{ color : blue; &Bar{ color : red; } } /* Sass에서는 다음과 동일함 .foo { color: blue; } .fooBar { color: red; } */
하지만, 이런 문자열 기반 해석은 작성자가 중첩 규칙에서 타입 선택자를 추가하려는 경우와 모호해질 수 있습니다. 예를 들어 Bar는 HTML에서 유효한 커스텀 요소 이름이기도 합니다.
CSS는 이렇게 하지 않습니다: 중첩 선택자의 각 구성 요소를 원자적으로 해석하며, 문자열로 이어붙이지 않습니다:
.foo{ color : blue; &Bar{ color : red; } } /* CSS에서는 다음과 동일함 .foo { color: blue; } Bar.foo { color: red; } */
2.2. 기타 At-규칙 중첩
중첩 스타일 규칙뿐만 아니라, 이 명세는 중첩 그룹 규칙을 스타일 규칙 안에 허용합니다: 본문에 스타일 규칙이 포함되는 모든 at-규칙은 스타일 규칙 안에 중첩될 수 있습니다.
이렇게 중첩될 때, 중첩 그룹 규칙의 내용은 <style-block>으로 파싱되며, <stylesheet>로 파싱되지 않습니다:
-
속성은 직접 사용할 수 있으며,
& {...}
블록에 중첩된 것처럼 동작합니다.
이러한 중첩 그룹 규칙의 의미와 동작은 별도로 명시되지 않는 한 변경되지 않습니다.
/* 속성을 직접 사용할 수 있음 */ .foo{ display : grid; @media ( orientation: landscape) { grid-auto-flow : column; } } /* 동일하게 해석됨 .foo { display: grid; @media (orientation: landscape) { & { grid-auto-flow: column; } } } */ /* 최종적으로 해석됨 .foo { display: grid; } @media (orientation: landscape) { .foo { grid-auto-flow: column; } } */ /* 조건부는 추가로 중첩 가능 */ .foo{ display : grid; @media ( orientation: landscape) { grid-auto-flow : column; @media ( min-width >1024 px ) { max-inline-size : 1024 px ; } } } /* 동일하게 해석됨 .foo { display: grid; } @media (orientation: landscape) { .foo { grid-auto-flow: column; } } @media (orientation: landscape) and (min-width > 1024px) { .foo { max-inline-size: 1024px; } } */ /* Cascade Layers 중첩 예시 */ html{ @layer base{ block-size : 100 % ; @layer support{ & body{ min-block-size : 100 % ; } } } } /* 동일하게 해석됨 @layer base { html { block-size: 100%; } } @layer base.support { html body { min-block-size: 100%; } } */ /* Scoping 중첩 예시 */ .card{ inline-size : 40 ch ; aspect-ratio : 3 /4 ; @scope ( &) { :scope{ border : 1 px solid white; } } } /* 동일하게 해석됨 .card { inline-size: 40ch; aspect-ratio: 3/4; } @scope (.card) { :scope { border-block-end: 1px solid white; } } */
직접 중첩된 모든 속성은 순서대로 모아 중첩 스타일
규칙의 & 선택자에 중첩된
것처럼 처리되며,
모든 자식 규칙 앞에 위치합니다.
이것은 OM에서도 동일하게 적용됩니다.
(childRules
속성은
실제로 이 중첩 스타일 규칙으로 시작하며,
직접 중첩된 모든 속성을 포함합니다.)
예를 들어, 앞선 예시:
.foo{ display : grid; @media ( orientation: landscape) { grid-auto-flow : column; } } /* 동일하게 해석됨 .foo { display: grid; @media (orientation: landscape) { & { grid-auto-flow: column; } } } */
은 실제로 정확히 동일하며,
동일한 CSSOM 구조를 생성합니다.
CSSMediaRule
객체는
CSSStyleRule
객체를
속성에 하나만 가지며,
grid-auto-flow 속성을 포함합니다.
참고: 이런 규칙의 직렬화는 원래 작성된 방식과 다를 수 있으며, 직렬화 시에는 직접 중첩된 속성이 전혀 나타나지 않습니다.
2.2.1. 중첩 @scope 규칙
@scope 규칙이 중첩 그룹 규칙인 경우, &는 <scope-start> 선택자에서 가장 가까운 상위 스타일 규칙에 의해 매칭된 요소를 참조합니다.
본문의 스타일 규칙과 <scope-end> 선택자에 대해, @scope 규칙은 상위 스타일 규칙처럼 동작하며, <scope-start> 선택자에 의해 매칭된 요소를 대상으로 합니다.
.parent{ color : blue; @scope ( & > .scope) to( & .limit) { & .content{ color : red; } }
다음과 동일합니다:
.parent{ color : blue; } @scope ( .parent > .scope) to( .parent > .scope .limit) { .parent > .scope .content{ color : red; } }
2.3. 중첩 규칙과 선언 혼합
스타일 규칙에 선언과 중첩 스타일 규칙 또는 중첩 조건부 그룹 규칙이 모두 포함된 경우, 세 가지는 임의로 혼합될 수 있습니다. 하지만, 선언과 다른 규칙의 상대적 순서는 어떤 방식으로도 보존되지 않습니다.
article{ color : green; &{ color : blue; } color: red; } /* 동일하게 해석됨 */ article{ color : green; color : red; &{ color : blue; } }
등장 순서(Order Of Appearance)를 결정할 때, 중첩 스타일 규칙과 중첩 조건부 그룹 규칙은 부모 규칙 이후로 간주됩니다.
article{ color : blue; &{ color : red; } }
두 선언 모두 specificity가 (0,0,1)로 같지만, 중첩 규칙은 부모 규칙 이후로 간주되므로, color: red 선언이 캐스케이드에서 우선합니다.
반면, 아래 예시에서는:
article{ color : blue; :where ( &) { color : red; } }
:where() 의 의사클래스는 중첩 선택자의 specificity를 0으로 낮춰주므로, color: red 선언은 (0,0,0) specificity를 갖게 되어, 등장순서 고려 이전에 color: blue 선언에 패배하게 됩니다.
참고: 선언과 중첩 규칙을 자유롭게 섞을 수 있지만, 읽기 어렵고 다소 혼란스러울 수 있습니다. 모든 속성이 마치 모든 규칙보다 앞에 오는 것처럼 동작하기 때문입니다. 가독성을 위해 작성자는 스타일 규칙에서 속성을 먼저 작성하고, 그 다음에 중첩 규칙을 두는 것이 좋습니다. (이 방식은 오래된 사용자 에이전트에서도 더 잘 동작합니다; 파싱 및 오류 복구 방식 특성상, 중첩 규칙 이후에 나오는 속성은 건너뛰어질 수 있습니다.)
참고: 다른 규칙 유형과 마찬가지로, 중첩이 있는 스타일 규칙의 직렬화는 원래 작성된 방식과 달라질 수 있습니다. 특히, 직접 중첩된 속성은 모든 중첩 규칙보다 먼저 직렬화되므로, 속성을 먼저 작성하는 것이 또 하나의 이유가 됩니다.
3. 중첩 선택자: & 선택자
중첩 스타일 규칙을 사용할 때, 반드시 부모 규칙에 의해 매칭된 요소를 참조할 수 있어야 합니다. 즉, 중첩의 주요 목적입니다. 이를 위해, 이 명세는 새로운 선택자인 중첩 선택자를 정의하며, & (U+0026 AMPERSAND)로 작성합니다.
중첩 스타일 규칙의 선택자에서 사용할 때, 중첩 선택자는 부모 규칙에 의해 매칭된 요소를 나타냅니다. 다른 컨텍스트에서 사용할 때는, 해당 컨텍스트에서 :scope와 동일한 요소를 나타냅니다 (별도 정의가 없는 한).
a, b{ & c{ color : blue; } }
는 다음과 동일합니다:
:is ( a, b) c{ color : blue; }
중첩 선택자는 가상 요소(pseudo-element)를 나타낼 수 없습니다 (:is() 가상 클래스의 동작과 동일).
.foo, .foo::before, .foo::after{ color : red; &:hover{ color : blue; } }
&는 .foo에 매칭된 요소만 나타내며, 즉 다음과 동일합니다:
.foo, .foo::before, .foo::after{ color : red; } .foo:hover{ color : blue; }
이 제한을 완화하고 싶지만, :is()와 & 모두에 대해 동시에 완화해야 합니다. 둘은 일부러 동일한 기반 메커니즘 위에 구축되었기 때문입니다. (Issue 7433)
specificity는 중첩 선택자에서 부모 스타일 규칙의 선택자 리스트 중 가장 큰 specificity와 동일합니다 (:is()와 동일).
#a, b{ & c{ color : blue; } } .foo c{ color : red; }
그리고 DOM 구조가 다음과 같을 때:
< b class = foo > < c > Blue text</ c > </ b >
텍스트는 파란색이 됩니다. &의 specificity는 #a ([1,0,0])와 b ([0,0,1]) 중 더 큰 [1,0,0]이므로, 전체 & c 선택자는 [1,0,1] specificity를 가지며, .foo c ([0,1,1])보다 큽니다.
특히, 이는 중첩을 수동으로 비중첩 규칙으로 확장했을 때와 다른 결과입니다. 이 경우 color: blue 선언은 b c 선택자([0,0,2])로 매칭되기 때문입니다, #a c([1,0,1])가 아니라.
왜 non-nested 규칙과 specificity가 다릅니까?
중첩 선택자는 :is() 가상클래스와 동일한 specificity 규칙을 고의적으로 사용합니다. 즉, 매칭된 선택자가 무엇인지 추적하지 않고, 인자 중 가장 큰 specificity만 사용합니다.
이는 성능상의 이유로 필요합니다; 하나의 선택자가 매칭 방식에 따라 여러 specificity를 가질 수 있다면, 선택자 매칭이 훨씬 더 복잡하고 느려집니다.
그렇다면 왜 &를 :is() 기준으로 정의합니까? 브라우저가 아닌 일부 중첩 기능 구현은 :is()로 변환(desugar)하지 않으며, 주로 :is() 도입 이전에 만들어졌기 때문입니다. 대신 직접 변환하지만, 이 경우 심각한 문제를 야기할 수 있습니다. 일부 경우에는 매우 흔한 상황에서도 엄청나게 많은 선택자가 생성될 수 있습니다.
.a1, .a2, .a3{ .b1, .b3, .b3{ .c1, .c2, .c3{ ...; } } } /* 단순하게 변환하면 */ .a1 .b1 .c1, .a1 .b1 .c2, .a1 .b1 .c3, .a1 .b2 .c1, .a1 .b2 .c2, .a1 .b2 .c3, .a1 .b3 .c1, .a1 .b3 .c2, .a1 .b3 .c3, .a2 .b1 .c1, .a2 .b1 .c2, .a2 .b1 .c3, .a2 .b2 .c1, .a2 .b2 .c2, .a2 .b2 .c3, .a2 .b3 .c1, .a2 .b3 .c2, .a2 .b3 .c3, .a3 .b1 .c1, .a3 .b1 .c2, .a3 .b1 .c3, .a3 .b2 .c1, .a3 .b2 .c2, .a3 .b2 .c3, .a3 .b3 .c1, .a3 .b3 .c2, .a3 .b3 .c3{ ...}
여기서, 세 단계의 중첩에 각 선택자 리스트에 세 개씩 있으면 27개의 변환된 선택자가 생성됩니다. 리스트에 더 많은 선택자를 추가하거나, 중첩 단계가 늘어나거나, 중첩 규칙이 더 복잡해지면, 작은 규칙이 수 메가바이트(혹은 훨씬 더 많은) 선택자로 확장될 수 있습니다.
일부 CSS 도구는 일부 변형을 휴리스틱으로 생략하여 출력량을 줄이고 아마도 올바른 결과를 내지만, UA에서는 이런 방식이 불가합니다.
:is()로 변환하면 이 문제를 완전히 제거할 수 있으며, specificity가 다소 덜 유용해지지만 합리적인 트레이드오프로 판단했습니다.
중첩 선택자는 복합 선택자 안 어디서든 사용할 수 있으며, 타입 선택자 앞에도 올 수 있어 복합 선택자의 기존 순서 제한을 위반할 수 있습니다.
div
요소만"을 의미합니다.
div&로 써도 같은 의미지만, 중첩 스타일 규칙의 시작에 사용할 수 없고, 선택자의 다른 위치에서는 가능할 수 있습니다.
4. CSSOM
4.1. CSSStyleRule
의
변경 사항
CSS 스타일 규칙은 중첩 규칙을 가질 수 있게 됩니다:
partial interface CSSStyleRule { [SameObject ]readonly attribute CSSRuleList cssRules ;unsigned long insertRule (CSSOMString ,
rule optional unsigned long = 0);
index undefined deleteRule (unsigned long ); };
index
cssRules
속성은
CSSRuleList
객체를 반환해야 하며,
이는 자식 CSS 규칙을 나타냅니다.
insertRule(rule, index)
메서드는
CSS
규칙 삽입을 rule에 대해 자식 CSS 규칙의 index 위치에 실행한 결과를 반환해야 합니다.
deleteRule(index)
메서드는
CSS
규칙 제거를 자식 CSS 규칙의 index 위치에서 실행해야 합니다.
참고: 중첩 규칙이 있는 CSSStyleRule
의
직렬화는
[CSSOM]에서 이미 CSS 규칙 직렬화로 잘 정의되어 있습니다.
참고: 중첩 스타일 규칙의 선택자가 무엇으로 시작할 수 있는지에 대한 제한은
CSS 규칙 삽입 5단계의 "CSS에서 부과하는 제약"으로 간주됩니다
(중첩 스타일 규칙을 허용하는 모든 것에 대해, CSSStyleRule
자체뿐만 아니라).
selectorText
를
설정할 때,
CSSStyleRule
이
중첩 스타일 규칙이고,
반환된 선택자 그룹이
식별자(ident) 또는 함수 토큰으로 시작하는 선택자로 시작하면,
아무 것도 하지 않고 반환합니다.