demo · v130

Tooltip via ::part()::after

Chrome 130 lets host-page CSS append ::after directly to a ::part() selector. Hover the cards below — the tooltips are generated entirely by info-card::part(trigger)::after { content: attr(data-tip) } in the host stylesheet. No JavaScript tooltip logic. No shadow DOM changes.

Detecting CSS ::part()::after chaining…
Host-page ::part()::after rule active
Copy Share Download Delete
Chrome 130 mode — host CSS ::part()::after rule in effect Hover or focus any button above. The tooltip is generated by info-card::part(trigger)::after { content: attr(data-tip) } — a ::after pseudo-element chained after ::part(). The shadow DOM has no tooltip logic whatsoever.

Live data-tip editor

selector chains unlocked in Chrome 130

the code

/* Host-page CSS — Chrome 130 required for these chains */

info-card::part(trigger) {
  position: relative;
}

/* ::after chained after ::part() — previously rejected */
info-card::part(trigger)::after {
  content: attr(data-tip);
  position: absolute;
  bottom: calc(100% + 0.4rem);
  left: 50%;
  transform: translateX(-50%);
  background: var(--text-black);
  color: var(--bg-ivory);
  font-family: var(--font-mono);
  font-size: 0.72rem;
  padding: 0.3rem 0.6rem;
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.15s;
}

/* :hover + ::after chained together after ::part() */
info-card::part(trigger):hover::after {
  opacity: 1;
}

/* Shadow DOM — no tooltip logic here */
class InfoCard extends HTMLElement {
  connectedCallback() {
    const root = this.attachShadow({ mode: 'open' });
    root.innerHTML = `
      <style>
        :host { display: block; }
        button { all: unset; display: block; width: 100%; box-sizing: border-box; }
      </style>
      <button part="trigger"><slot></slot></button>
    `;
  }
}

see also