Componentes da Web acessíveis

Um limite de sombra muda o funcionamento da semântica e do ARIA. Duas falhas são mais comuns: semântica ausente dentro da árvore de sombra e referências de ARIA que falham silenciosamente porque não podem cruzar raízes de sombra.

Comece com a marcação semântica: use <button>, <nav> e <ul> reais dentro do modelo para que a maior parte da semântica seja fornecida sem custo financeiro e reserve ElementInternals/ARIA para as lacunas.

Semântica padrão pelo mixin ARIA ElementInternals

Defina o papel e o estado ARIA de um elemento personalizado em JS usando ElementInternals, em vez de gravar atributos ARIA no host. Isso mantém a semântica como padrão do componente, mas permite que um consumidor a substitua por atributos no DOM leve.

class ToggleSwitch extends HTMLElement {
  #internals;
  constructor() {
    super();
    this.#internals = this.attachInternals();
    this.#internals.role = 'switch';          // default role, no host attribute needed
    this.#internals.ariaChecked = 'false';    // exposed to the accessibility tree
    this.addEventListener('click', () => this.toggle());
    this.addEventListener('keydown', (e) => {
      if (e.key === ' ') { e.preventDefault(); this.toggle(); }
    });
  }
  connectedCallback() {
    // Put the host in the tab order on connect (setting tabIndex in the constructor throws on createElement).
    if (!this.hasAttribute('tabindex')) this.tabIndex = 0;
  }
  toggle() {
    const on = this.#internals.ariaChecked !== 'true';
    this.#internals.ariaChecked = String(on);
  }
}
customElements.define('toggle-switch', ToggleSwitch);

Isso evita poluir o DOM com atributos ARIA e impede que os consumidores os substituam acidentalmente, enquanto a árvore de acessibilidade ainda vê o papel e o estado corretos.

Gerenciamento de foco e delegatesFocus

  • Quando o próprio elemento host é o controle interativo e carrega a função e o estado por meio de ElementInternals (como ToggleSwitch acima), coloque o host na ordem de tabulação dentro de connectedCallback (if (!this.hasAttribute('tabindex')) this.tabIndex = 0 — nunca no construtor, que gera um erro em document.createElement) para que o foco seja direcionado ao host e a tecnologia adaptativa anuncie a função e o estado dele. Não defina tabindex no host para componentes estáticos e não interativos (como um balão ou card).
  • Anexe a raiz de sombra com delegatesFocus: true quando o componente envolver um controle focalizável interno que tenha a semântica (ou em um elemento personalizado associado ao formulário com static formAssociated = true). Ao focar o host (e clicar em um <label> associado para elementos associados ao formulário), o foco é encaminhado para o primeiro filho focalizável, e :focus corresponde ao host. Não combine delegatesFocus: true com uma função/estado no host, ou o foco vai ficar no elemento interno e pular o estado do host.
  • Para foco móvel ou para mover o foco para uma região recém-revelada, atribua tabindex="-1" ao destino e chame .focus(). Torne os destinos programaticamente focalizáveis explícitos em vez de depender da ordem de tabulação.

Relações ARIA em raízes de sombra

  • OBRIGATÓRIO: um id referenciado por aria-labelledby, aria-describedby, aria-controls etc. é resolvido apenas na própria árvore do elemento de referência. Um atributo baseado em id no DOM leve não pode apontar para um id dentro de uma árvore de sombra e vice-versa. A referência é ignorada silenciosamente.
  • Para cruzar o limite, use as propriedades de elementos ARIA refletidas (por exemplo, ariaLabelledByElements, ariaDescribedByElements etc.), que usam referências diretas de elementos JS em vez de IDs. Eles são resolvidos para fora: um elemento dentro de uma árvore de sombra pode apontar para um destino no DOM de luz ou em qualquer árvore ancestral (incluindo conteúdo com slot). Assim, é possível rotular um controle interno de sombra do conteúdo do DOM de luz, algo que as referências de id não podem expressar.
  • Eles não são resolvidos para dentro: um elemento não pode fazer referência a uma árvore de sombra descendente (por exemplo, um host não pode ser rotulado pelo próprio conteúdo de sombra). Para essa direção, o mecanismo emergente de destino de referência é a direção dos padrões, mas ainda não está amplamente disponível.
  • Quando uma relação não puder ser expressa de nenhuma das duas formas, mantenha-a em uma árvore (renderize o rótulo na mesma árvore) ou use o texto aria-label em vez de uma referência id.

Estratégias de fallback

Quando o mixin ARIA ElementInternals não estiver disponível, defina o papel e o estado ARIA como atributos no host (this.setAttribute('role', 'switch')). Isso funciona em qualquer lugar, mas coloca atributos no DOM que um consumidor pode substituir.

Quando as propriedades de elementos ARIA refletidas não estiverem disponíveis, use um atributo aria-* baseado em id para relações na mesma árvore. Um rótulo entre limites não tem um equivalente baseado em id, então use o texto aria-label.