Signatur
Beschreibung
ElementInternals.states liefert ein CustomStateSet. Das ist eine Set-ähnliche Sammlung von Zeichenketten, die die aktuellen Zustände eines autonomen benutzerdefinierten Elements (Custom Element) darstellt. Jeder Eintrag in dieser Menge lässt sich in CSS mit der Pseudoklasse :state(name) ansprechen, also etwa my-toggle:state(checked). Genauso funktioniert es von außen über ::part() oder innerhalb des Shadow DOM mit :host(:state(name)).
Das ist nützlich, wenn eine Komponente internen Zustand (z. B. checked, loading, invalid, expanded) nach außen stylbar machen soll, ohne dafür öffentliche Attribute oder Klassen am Host-Element zu setzen. Attribute kann der Nutzer der Komponente von außen verändern, und sie „verschmutzen“ das DOM. Custom States dagegen kann nur die Komponente selbst setzen. Sie bleiben ihr Implementierungsdetail und lassen sich trotzdem per CSS abfragen, ähnlich wie eingebaute Pseudoklassen wie :checked oder :disabled.
Die Eigenschaft selbst ist schreibgeschützt: Man kann ihr kein neues Set zuweisen, verändert den Inhalt aber über die Set-Methoden add(), delete(), has(), clear(), forEach(), entries(), values() und die Eigenschaft size. An das ElementInternals-Objekt kommt man nur über this.attachInternals() im Konstruktor oder in einer Methode des Custom Elements.
Rückgabewert
string). Jeder enthaltene Name bewirkt, dass :state(name) auf das Element zutrifft.Beispiele
Umschalter mit eigenem checked-Zustand
class MyToggle extends HTMLElement {
#internals;
constructor() {
super();
this.#internals = this.attachInternals();
this.attachShadow({ mode: 'open' }).innerHTML = `
<style>
:host { display: inline-block; padding: 4px 12px; border: 1px solid #888; cursor: pointer; }
:host(:state(checked)) { background: seagreen; color: white; }
</style>
<slot></slot>`;
this.addEventListener('click', () => this.toggle());
}
get checked() {
return this.#internals.states.has('checked');
}
set checked(value) {
if (value) this.#internals.states.add('checked');
else this.#internals.states.delete('checked');
}
toggle() {
this.checked = !this.checked;
}
}
customElements.define('my-toggle', MyToggle);
const toggle = document.createElement('my-toggle');
toggle.textContent = 'Dark Mode';
document.body.append(toggle);
toggle.toggle();
console.log(toggle.checked);
console.log(toggle.matches(':state(checked)'));
Mehrere Zustände beim Laden von Daten (loading / error / ready)
class UserCard extends HTMLElement {
#internals = this.attachInternals();
#setState(name) {
const { states } = this.#internals;
states.clear();
states.add(name);
}
async connectedCallback() {
this.#setState('loading');
try {
const res = await fetch(`https://jsonplaceholder.typicode.com/users/${this.dataset.id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const user = await res.json();
this.textContent = user.name;
this.#setState('ready');
} catch (err) {
this.textContent = 'Fehler beim Laden';
this.#setState('error');
}
console.log([...this.#internals.states]);
}
}
customElements.define('user-card', UserCard);
// Globales Styling von außen
const sheet = new CSSStyleSheet();
sheet.replaceSync(`
user-card:state(loading) { opacity: .5; }
user-card:state(error) { color: crimson; }
user-card:state(ready) { font-weight: bold; }
`);
document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet];
const card = document.createElement('user-card');
card.dataset.id = '1';
document.body.append(card);
Feature-Erkennung mit Fallback auf ein Attribut
const supportsCustomStates = (() => {
try {
return CSS.supports('selector(:state(x))');
} catch {
return false;
}
})();
class MyPanel extends HTMLElement {
#internals = this.attachInternals();
set expanded(value) {
if (supportsCustomStates) {
value ? this.#internals.states.add('expanded') : this.#internals.states.delete('expanded');
} else {
// Fallback für ältere Browser: Attribut setzen
this.toggleAttribute('data-expanded', value);
}
}
}
customElements.define('my-panel', MyPanel);
const panel = document.body.appendChild(document.createElement('my-panel'));
panel.expanded = true;
console.log(supportsCustomStates ? panel.matches(':state(expanded)') : panel.hasAttribute('data-expanded'));
// Wichtig · Fallstricke
- Veraltete Syntax in Chromium: Chrome 90–124 kannte nur die alte Form mit Bindestrich-Namen (
states.add('--checked')) und den Selektor:--checked. Ab Chrome 125 gilt die standardisierte Syntax:state(checked). Die alte Form ist veraltet und wurde entfernt. Neuer Code sollte nur noch:state()verwenden. - Nur für autonome Custom Elements:
attachInternals()wirft bei eingebauten Elementen und bei angepassten eingebauten Elementen (is="…") einenNotSupportedError. Es wirft auch, wenn das Element schon einmal Internals angefordert hat oder wenn die Klassestatic disabledFeatures = ['internals']setzt. - Kapselung: Gib das
ElementInternals-Objekt nicht öffentlich heraus, sondern speichere es in einem privaten Feld (#internals). Sonst kann fremder Code die Zustände verändern, und der Vorteil gegenüber Attributen geht verloren. - Custom States sind nicht für Barrierefreiheit gedacht. Für Screenreader zusätzlich ARIA-Eigenschaften setzen, z. B.
internals.ariaChecked = 'true'. - Zustände werden nicht als Attribut serialisiert. Sie tauchen also nicht in
outerHTMLauf und gehen beim Klonen mitcloneNode()verloren. - Browserunterstützung: Baseline seit 2024 (Chrome/Edge 125, Firefox 126, Safari 17.4). Für ältere Browser per
CSS.supports('selector(:state(x))')prüfen und auf Attribute ausweichen.