Стилизация веб-компонентов

Теневое дерево 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. Чтобы разрешить наследование и переопределение из любого места в иерархии, оставьте общедоступное свойство в :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>

Выбирайте между двумя каналами осознанно: пользовательские свойства для ограниченного набора значений, безопасного для системы проектирования; части для неограниченного стиля конкретного элемента.

Передача стилей нативному элементу в оболочке

Когда пользовательский элемент оборачивает стандартный элемент управления (<my-button> вокруг реального <button>), обычно возникают три проблемы:

  • Элементы управления формой не наследуют типографику. color и дополнительные параметры пересекают границу, но <button>, <input> и <select> берут font и другие свойства из таблицы стилей UA, которая не наследуется. Поэтому элемент управления отображается шрифтом платформы, а не страницы, пока вы не зададите для него font: inherit (и обычно color: inherit) в теневом дереве. Это самая распространенная ошибка.
  • Стили :host не применяются к внутреннему элементу. Если задать background/border/padding для :host, это повлияет на стиль контейнера, а не элемента управления внутри него. Передавайте только нужные стили. Для этого можно использовать ::part или передавать фиксированный набор значений через дополнительные параметры. Правило :host не применяется к элементу, в который вложен компонент.
  • display: contents + all: inherit – это реальный компромисс, а не ярлык. Сворачивание хоста с помощью display: contents и сброс внутреннего элемента управления с помощью all: inherit в настоящее время является единственным общим способом передачи произвольных стилей обернутому нативному элементу без изучения потребителями частей каждого компонента или дополнительных параметров, а удаление собственного блока хоста обычно является смыслом шаблона (композиция компенсирует отсутствие встроенного наследования). Два реальных предостережения: all: inherit стирает значения по умолчанию UA, поэтому ограничьте область действия определенными свойствами или используйте revert, если вы хотите вернуть значения по умолчанию; и конкретный блокировщик — getBoundingClientRect() и getClientRects() возвращают ноль для элементов display: contents, поэтому позиционирование привязки относительно хоста нарушено. Если эти компромиссы не подходят, используйте ::part или пользовательские свойства.

:state(): стили для собственных состояний компонента.

Пользовательский элемент может предоставлять внутренние логические состояния через ElementInternals с помощью this.#internals.states.add('busy') и .delete('busy') (см. Пользовательские элементы для использования жизненного цикла), и эти состояния становятся каналом стилизации через границу – как ::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) работают внутри корня теневого дерева и ограничены им. Название слоя в теневом дереве не зависит от слоя с таким же названием на странице. Используйте их, чтобы упорядочить собственные стили компонентов (например, @layer base, theme;), чтобы переопределения потребителей через пользовательские свойства и ::part по-прежнему выигрывали предсказуемо.
  • Запросы контейнеров – это подходящий инструмент для адаптивных компонентов. Компонент должен реагировать на предоставленное ему пространство, а не на область просмотра. Объявите хост или оболочку в качестве контейнера (задав :host поле макета, например display: block, поскольку пользовательские элементы по умолчанию имеют значение display: inline, а ограничение размера не применяется к элементам inline или contents) и запросите его, чтобы один и тот же компонент адаптировался на боковой панели или в области полной ширины.
: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 и не является перспективным.