웹 구성요소 스타일 지정

Shadow DOM은 스타일 경계입니다. 상속된 속성 (맞춤 속성 포함)을 제외하고 페이지 규칙은 도달하지 않으며 구성요소 규칙은 누출되지 않습니다. 아래 채널을 통해 의도적으로 교차합니다.

:host, :host(), :host-context()

/* The component's own host element, styled from inside its shadow tree. */
:host { display: block; }

/* Only when the host matches a selector (e.g. a reflected state attribute). */
:host([disabled]) { opacity: 0.5; pointer-events: none; }

/* Based on an ancestor in the Light DOM; useful for theming. */
:host-context([theme="dark"]) { background: #111; color: #eee; }

:host-context()는 피하세요. 경계 외부로 확장되어 상위 요소를 테스트하지만, Firefox와 Safari는 이를 지원하지 않기로 결정했으며 CSSWG도 더 이상 개발하지 않는 방향으로 기울고 있습니다. 따라서 이를 미래 지향적인 옵션이 아닌 Chromium 전용의 막다른 길로 간주해야 합니다. 대신 상속된 CSS 맞춤 속성을 통해 테마를 지정합니다 (아래 참고). 소비자가 상위 요소에 속성을 설정하고 구성요소가 경계를 넘어 이를 읽습니다. 이는 어디서나 작동하며 양쪽을 분리된 상태로 유지합니다.

상속된 속성과 맞춤 속성이 경계를 넘나듦

상속된 CSS 속성 (color, font-family, line-height, CSS 맞춤 속성)은 섀도우 경계를 통과합니다. 따라서 맞춤 속성이 기본 테마 API가 됩니다. 적절한 대체와 함께 노출하세요.

:host {
  /* Consumers override --card-bg from outside; the fallback keeps it usable. */
  background: var(--card-bg, white);
  color: var(--card-fg, black);
}

:host에 정의된 맞춤 속성은 호스트 요소를 구체적으로 타겟팅하는 규칙으로만 재정의할 수 있습니다. 트리의 어느 위치에서나 상속되고 재정의될 수 있도록 하려면 :host에서 공개 속성을 설정하지 않은 상태로 두고 각 var()의 대체에 의존하세요. :host에서 공개 속성을 시드하지 마세요.

모든 var()에서 반복하는 대신 대체가 한 번 선언되도록 하려면 공개 속성을 읽는 :host에 비공개 '섀도' 속성을 정의하고 비공개 속성을 내부적으로 참조합니다.

:host {
  --_card-bg: var(--card-bg, white);
  --_card-fg: var(--card-fg, black);
}
.card { background: var(--_card-bg); color: var(--_card-fg); }

소비자는 여전히 외부에서 --card-bg를 재정의합니다. 선행 밑줄은 별칭이 내부임을 나타내며 대체는 정확히 한 위치에 있습니다.

::slotted(): 프로젝션된 Light DOM 스타일 지정

::slotted(selector)는 슬롯에 할당된 최상위 노드를 타겟팅합니다. 하위 요소에 도달할 수 없으며 Light DOM 규칙 (소비자가 자체 콘텐츠를 소유함)에 지므로 제어가 아닌 가벼운 수정에 사용하세요. ::slotted(...) !important는 소비자 규칙을 이길 수 있지만, 그렇게 하면 소비자가 소유할 권리가 있는 스타일이 재정의됩니다. 미적 재정의가 아닌 구조적 수정에만 사용하세요.

/* Matches a slotted <p>, but NOT a <p> nested inside a slotted <div>. */
::slotted(p) { margin: 0; }

::part() 및 exportparts: 테마 설정을 위해 내부 요소 노출

내부 요소를 part="name"로 표시하여 소비자가 내부 구조를 학습하지 않고도 모든 속성으로 스타일을 지정할 수 있도록 합니다.

<!-- inside the shadow template -->
<button part="submit">Submit</button>
/* consumer stylesheet */
my-card::part(submit) { background: navy; color: white; }

모든 <div>가 아닌 몇 가지 상위 수준 부분 (버튼, 입력, 라벨)을 노출합니다. 광범위한 부분 노출 영역은 취약한 API가 됩니다. 자체 구성요소를 통해 중첩된 구성요소의 부분을 위로 다시 노출하려면 exportparts를 사용하여 전달합니다.

<inner-widget exportparts="submit: card-submit"></inner-widget>

두 채널 중 하나를 신중하게 선택하세요. 제한적이고 디자인 시스템에 안전한 값 집합에는 맞춤 속성을, 특정 요소의 제한 없는 스타일 지정에는 부분을 사용하세요.

래핑된 네이티브 요소에 스타일 전달

맞춤 요소가 기본 컨트롤을 래핑하는 경우(실제 <button> 주위에 <my-button>) 일반적으로 다음 세 가지 문제가 발생합니다.

  • 양식 컨트롤은 서체를 상속하지 않습니다. color 및 맞춤 속성은 경계를 넘지만 <button>, <input>, <select>는 UA 스타일시트에서 font 및 친구를 가져오며 이는 상속되지 않습니다. 따라서 섀도 트리 내에서 font: inherit (일반적으로 color: inherit)를 설정할 때까지 래핑된 컨트롤은 페이지 글꼴이 아닌 플랫폼 글꼴로 렌더링됩니다. 가장 흔히 발생하는 실수입니다.
  • :host 스타일이 내부 요소에 도달하지 않습니다. :host에서 background/border/padding을 설정하면 호스트 상자의 스타일이 지정되고 내부 컨트롤은 지정되지 않습니다. 의도적으로 전달: 개방형 스타일 지정을 위해 컨트롤을 ::part로 노출하거나 맞춤 속성을 통해 고정된 값 집합을 파이프합니다. :host 규칙은 래핑된 요소로 계단식으로 적용되지 않습니다.
  • display: contents + all: inherit는 지름길이 아니라 실제 절충안입니다. display: contents를 사용하여 호스트를 접고 all: inherit를 사용하여 내부 컨트롤을 재설정하는 것은 현재 소비자가 구성요소별 부분이나 맞춤 속성을 학습하지 않고도 래핑된 네이티브 요소에 임의의 스타일을 전달하는 유일한 일반적인 방법입니다. 호스트 자체 상자를 삭제하는 것이 일반적으로 이 패턴의 핵심입니다 (누락된 기본 제공 상속을 보완하는 구성). 두 가지 실제 주의사항: all: inherit는 UA 기본값을 완전 삭제하므로 특정 속성으로 범위를 지정하거나 기본값을 다시 사용하려면 revert를 사용하세요. 구체적인 차단 요소: getBoundingClientRect() 및 getClientRects()는 display: contents 요소에 대해 0을 반환하므로 호스트에 대한 앵커 위치 지정이 손상됩니다. 이러한 장단점이 적합하지 않은 경우 ::part 또는 맞춤 속성을 사용하세요.

:state(): 구성요소 자체 상태의 스타일 지정

커스텀 요소는 this.#internals.states.add('busy') 및 .delete('busy')를 통해 ElementInternals를 통해 내부 불리언 상태를 노출할 수 있으며(수명 주기 사용은 커스텀 요소 참고), 이러한 상태는 경계를 가로지르는 스타일 지정 채널이 됩니다. ::part 및 맞춤 속성과 유사하지만 구조나 값이 아닌 상태에 적용됩니다. 섀도 트리의 내부에서는 :host(:state(name))를 사용하고 소비자의 스타일시트에서는 tag-name:state(name)를 사용하여 일치시킵니다.

/* Inside the component's shadow tree. */
:host(:state(busy)) button { opacity: 0.6; pointer-events: none; }

/* From the consumer's stylesheet — a read-only public hook. */
favorite-button:state(favorited) { filter: drop-shadow(0 0 4px crimson); }

반영된 속성과 달리 커스텀 상태는 외부에서 읽기 전용입니다. 소비자는 스타일을 지정할 수 있지만 설정하거나 위조할 수는 없으므로 구성요소가 유일한 작성자로 유지됩니다. 상태가 구성요소에 의해 소유되고 스타일만 지정해야 하는 경우 data-* 속성보다 이 속성을 사용하는 것이 좋습니다.

@layer 및 구성요소 내부의 컨테이너 쿼리

  • 계단식 레이어 (@layer)는 shadow root 내에서 작동하며 범위가 지정됩니다. shadow tree의 레이어 이름은 페이지의 동일한 이름의 레이어와 독립적입니다. 이를 사용하여 자체 구성요소 스타일 (예: @layer base, theme;)을 정렬하면 맞춤 속성 및 ::part를 통한 소비자 재정의가 여전히 예측 가능하게 적용됩니다.
  • 컨테이너 쿼리는 반응형 구성요소에 적합한 도구입니다. 구성요소는 표시 영역이 아닌 제공된 공간에 반응해야 합니다. 호스트 또는 래퍼를 컨테이너로 선언하고 (커스텀 요소는 기본적으로 display: inline이고 크기 포함은 inline 또는 contents 요소에 적용되지 않으므로 :host에 display: block와 같은 레이아웃 상자를 제공함) 쿼리하여 동일한 구성요소가 사이드바 또는 전체 너비 영역에 맞게 조정되도록 합니다.
:host {
  display: block; /* Required: size containment does not apply to default display: inline */
  container-type: inline-size;
}

@container (min-width: 30rem) {
  .card { grid-template-columns: 1fr 2fr; }
}

대체 전략

::part, 계단식 레이어, 컨테이너 쿼리는 광범위하게 사용할 수 있습니다. 주로 상속된 맞춤 속성을 통해 테마를 지정하며, 이는 이러한 속성이 작동하지 않는 경우에도 작동합니다. 위에서 언급한 것처럼 :host-context()에 의존하지 마세요. 이는 사실상 Chromium 전용이며 미래 지향적인 옵션이 아닙니다.