demo · v130

Focus Ring via ::part()

Chrome 130 lets host-page CSS chain :focus-visible and :hover directly after ::part(). Tab through the form below — the blue focus ring is drawn entirely by a host-page selector the parser used to silently discard.

Detecting CSS ::part() + :focus-visible chaining…
Host-page ::part():focus-visible rule active
Chrome 130 mode — host CSS rule in effect Tab between the fields above. The blue outline: 3px solid var(--accent-blue) focus ring is applied by a single host-page selector: form-field::part(input):focus-visible. No JavaScript. No shadow DOM modification.

selector chains unlocked in Chrome 130

still rejected after ::part()

The new grammar is not a deep selector escape hatch. These probes run CSS.supports("selector(...)") for the exact chains below so the supported focus and hover rules stay paired with the limits that still protect shadow-tree structure.

the code

/* Chrome 130: pseudo-class chained after ::part() — previously rejected */

form-field::part(input):focus-visible {
  outline: 3px solid var(--accent-blue);
  outline-offset: 2px;
}

form-field::part(input):hover {
  background: var(--bg-stone);
}

form-field::part(label):focus-within {
  color: var(--accent-blue);
}

/* Still rejected: these would expose deeper structure */
form-field::part(input) > .x {}
form-field::part(input):has(.y) {}
form-field::part(input):nth-child(2) {}

/* Shadow DOM — deliberately minimal, no focus ring inside */
class FormField extends HTMLElement {
  connectedCallback() {
    const root = this.attachShadow({ mode: 'open' });
    const isTextarea = this.getAttribute('type') === 'textarea';
    const label = this.getAttribute('label') ?? '';
    const placeholder = this.getAttribute('placeholder') ?? '';
    const wrapper = document.createElement('label');
    wrapper.part.add('label');
    wrapper.append(document.createTextNode(label));
    const control = document.createElement(isTextarea ? 'textarea' : 'input');
    control.part.add('input');
    control.name = this.id || label.toLowerCase();
    control.id = control.name;
    control.placeholder = placeholder;
    wrapper.append(control);
    root.append(wrapper);
  }
}

see also