Estilização de componentes da Web

O Shadow DOM é um limite de estilo: as regras da página não entram e as regras do componente não saem, exceto as propriedades herdadas (incluindo as personalizadas). Cruze-as deliberadamente pelos canais abaixo.

: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; }

Evite :host-context(). Ele alcança fora do limite para testar ancestrais, mas o Firefox e o Safari se recusaram a enviá-lo, e o CSSWG está inclinado a não desenvolvê-lo mais. Trate-o como um beco sem saída exclusivo do Chromium, não como uma opção voltada para o futuro. Tema por uma propriedade personalizada de CSS herdada (abaixo): o consumidor define a propriedade em um ancestral, e o componente a lê além do limite, o que funciona em qualquer lugar e mantém os dois lados separados.

As propriedades herdadas e personalizadas cruzam o limite

As propriedades CSS herdadas (color, font-family, line-height e propriedades CSS personalizadas) atravessam o limite de sombra. Isso faz com que as propriedades personalizadas sejam a principal API de temas: exponha-as com substitutos sensíveis.

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

Uma propriedade personalizada definida em :host só pode ser substituída por regras que segmentam especificamente o elemento host. Para permitir que ela seja herdada e substituível em qualquer lugar da árvore, deixe a propriedade pública sem definição em :host e use o substituto em cada var(). Não defina a propriedade pública em :host.

Para manter o substituto declarado uma vez em vez de repetido em cada var(), defina uma propriedade "shadow" particular em :host que leia a propriedade pública e faça referência à propriedade particular internamente:

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

Os consumidores ainda substituem --card-bg de fora. O sublinhado inicial indica que o alias é interno, e o substituto fica em um único lugar.

::slotted(): estilização do Light DOM projetado

::slotted(selector) segmenta os nós de nível superior atribuídos a um slot. Ele não pode alcançar os descendentes e perde para qualquer regra do DOM leve (o consumidor é o proprietário do próprio conteúdo). Portanto, use-o para retoques leves, não para controle. ::slotted(...) !important pode substituir as regras do consumidor, mas isso substitui o estilo que o consumidor tem direito de possuir. Reserve-o para correções estruturais, não para substituições estéticas.

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

::part() e exportparts: expor elementos internos para temas

Marque um elemento interno com part="name" para permitir que os consumidores o estilizem com qualquer propriedade, sem precisar aprender sua estrutura interna.

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

Exponha algumas partes de alto nível (botão, entrada, rótulo), não todas as <div>. Superfícies de partes amplas se tornam uma API frágil. Para reexpor as partes de um componente aninhado no seu próprio componente, encaminhe-as com exportparts:

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

Escolha entre os dois canais de forma deliberada: propriedades personalizadas para um conjunto de valores restrito e seguro para o sistema de design; partes para estilização irrestrita de um elemento específico.

Como encaminhar estilos para um elemento nativo encapsulado

Quando um elemento personalizado envolve um controle nativo (<my-button> em torno de um <button> real), três coisas costumam confundir as pessoas:

  • Os controles de formulário não herdam a tipografia. color e as propriedades personalizadas cruzam o limite, mas <button>, <input> e <select> usam font e outros elementos da folha de estilo do UA, que não é herdada. Portanto, o controle encapsulado é renderizado na fonte da plataforma, não na fonte da página, até que você defina font: inherit (e geralmente color: inherit) nele dentro da árvore de sombra. Esse é o erro mais comum.
  • Os estilos de :host não alcançam o elemento interno. Definir background/border/padding em :host estiliza a caixa de host, não o controle dentro dela. Encaminhe deliberadamente: exponha o controle como um ::part para estilização aberta ou canalize um conjunto fixo de valores por propriedades personalizadas. Uma regra :host não se propaga em cascata para o elemento encapsulado.
  • display: contents + all: inherit é uma compensação real, não um atalho. Recolher o host com display: contents e redefinir o controle interno com all: inherit é, no momento, a única maneira genérica de transmitir estilos arbitrários para um elemento nativo encapsulado sem que os consumidores aprendam partes por componente ou propriedades personalizadas. Além disso, remover a caixa do host geralmente é o objetivo do padrão (composição que compensa uma herança integrada ausente). Duas ressalvas reais: all: inherit limpa os padrões do UA, então limite o escopo a propriedades específicas ou use revert quando quiser os padrões de volta; e o bloqueador concreto: getBoundingClientRect() e getClientRects() retornam zero para elementos display: contents, então o posicionamento de âncora em relação ao host está quebrado. Use ::part ou propriedades personalizadas quando essas compensações não forem adequadas.

:state(): estilizar os próprios estados do componente

Um elemento personalizado pode expor estados booleanos internos por meio de ElementInternals usando this.#internals.states.add('busy') e .delete('busy') (consulte Elementos personalizados para uso do ciclo de vida), e esses estados se tornam um canal de estilo além do limite, como ::part e propriedades personalizadas, mas para estado em vez de estrutura ou valores. Faça a correspondência deles dentro da árvore de sombra com :host(:state(name)) e de uma folha de estilo do consumidor com 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); }

Ao contrário de um atributo refletido, um estado personalizado é somente leitura para o exterior: os consumidores podem estilizá-lo, mas não podem defini-lo ou forjá-lo, então o componente continua sendo o único gravador. Prefira-o em vez de um atributo data-* sempre que o estado for de propriedade do componente e só precisar ser estilizado.

@layer e consultas de contêiner dentro de componentes

  • As camadas em cascata (@layer) funcionam dentro de uma raiz de sombra e têm escopo definido para ela. Um nome de camada na árvore de sombra é independente de uma camada com o mesmo nome na página. Use-as para ordenar seus próprios estilos de componente (por exemplo, @layer base, theme;) para que as substituições do consumidor por propriedades personalizadas e ::part ainda vençam de forma previsível.
  • As consultas de contêiner são a ferramenta certa para componentes responsivos: um componente precisa responder ao espaço que recebe, não à janela de visualização. Declare o host ou um wrapper como um contêiner (fornecendo a :host uma caixa de layout como display: block, já que os elementos personalizados usam display: inline por padrão e a contenção de tamanho não se aplica a elementos inline ou contents) e consulte-o para que o mesmo componente se adapte em uma barra lateral ou em uma região de largura total.
: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; }
}

Estratégias de fallback

::part, camadas em cascata e consultas de contêiner estão amplamente disponíveis. Use temas principalmente com propriedades personalizadas herdadas, que funcionam mesmo onde essas não funcionam. Como observado acima, não use :host-context(). Ele é exclusivo do Chromium e não é uma opção para o futuro.