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…
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
- ✓
form-field::part(input):focus-visible { outline: 3px solid … } - ✓
form-field::part(input):hover { background: … } - ✓
form-field::part(label):focus-within { color: … } - ✗ pre-130
form-field::part(input):focus-visible { … }— silently dropped - ✗ pre-130
form-field::part(input):hover { … }— silently dropped
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);
}
}