-
Notifications
You must be signed in to change notification settings - Fork 22.5k
/
index.md
566 lines (441 loc) · 20 KB
/
index.md
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
---
title: CustomStateSet
slug: Web/API/CustomStateSet
page-type: web-api-interface
browser-compat: api.CustomStateSet
---
{{APIRef("Web Components")}}
The **`CustomStateSet`** interface of the [Document Object Model](/en-US/docs/Web/API/Document_Object_Model) stores a list of states for an [autonomous custom element](/en-US/docs/Web/API/Web_components/Using_custom_elements#types_of_custom_element), and allows states to be added and removed from the set.
The interface can be used to expose the internal states of a custom element, allowing them to be used in CSS selectors by code that uses the element.
## Instance properties
- {{domxref("CustomStateSet.size")}}
- : Returns the number of values in the `CustomStateSet`.
## Instance methods
- {{domxref("CustomStateSet.add()")}}
- : Adds a value to the set.
- {{domxref("CustomStateSet.clear()")}}
- : Removes all elements from the `CustomStateSet` object.
- {{domxref("CustomStateSet.delete()")}}
- : Removes one value from the `CustomStateSet` object.
- {{domxref("CustomStateSet.entries()")}}
- : Returns a new iterator with the values for each element in the `CustomStateSet` in insertion order.
- {{domxref("CustomStateSet.forEach()")}}
- : Executes a provided function for each value in the `CustomStateSet` object.
- {{domxref("CustomStateSet.has()")}}
- : Returns a {{jsxref("Boolean")}} asserting whether an element is present with the given value.
- {{domxref("CustomStateSet.keys()")}}
- : An alias for {{domxref("CustomStateSet.values()")}}.
- {{domxref("CustomStateSet.values()")}}
- : Returns a new iterator object that yields the values for each element in the `CustomStateSet` object in insertion order.
## Description
Built in HTML elements can have different _states_, such as "enabled" and "disabled, "checked" and "unchecked", "initial", "loading" and "ready".
Some of these states are public and can be set or queried using properties/attributes, while others are effectively internal, and cannot be directly set.
Whether external or internal, element states can generally be selected and styled using [CSS pseudo-classes](/en-US/docs/Web/CSS/Pseudo-classes) as selectors.
The `CustomStateSet` allows developers to add and delete states for autonomous custom elements (but not elements derived from built-in elements).
These states can then be used as custom state pseudo-class selectors in a similar way to the pseudo-classes for built-in elements.
### Setting custom element states
To make the `CustomStateSet` available, a custom element must first call {{domxref("HTMLElement.attachInternals()")}} in order to attach an {{domxref("ElementInternals")}} object.
`CustomStateSet` is then returned by {{domxref("ElementInternals.states")}}.
Note that `ElementInternals` cannot be attached to a custom element based on a built-in element, so this feature only works for autonomous custom elements (see [github.com/whatwg/html/issues/5166](https://github.com/whatwg/html/issues/5166)).
The `CustomStateSet` instance is a [`Set`-like object](/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set#set-like_browser_apis) that can hold an ordered set of state values.
Each value is a custom identifier.
Identifiers can be added to the set or deleted.
If an identifier is present in the set the particular state is `true`, while if it is removed the state is `false`.
Custom elements that have states with more than two values can represent them with multiple boolean states, only one of which is `true` (present in the `CustomStateSet`) at a time.
The states can be used within the custom element but are not directly accessible outside of the custom component.
### Interaction with CSS
You can select a custom element that is in a specific state using the [`:state()`](/en-US/docs/Web/CSS/:state) _custom state pseudo-class_.
The format of this pseudo-class is `:state(my-state-name)`, where `my-state-name` is the state as defined in the element.
The custom state pseudo-class matches the custom element only if the state is `true` (i.e., if `my-state-name` is present in the `CustomStateSet`).
For example, the following CSS matches a `labeled-checkbox` custom element when the element's `CustomStateSet` contains the `checked` state, and applies a `solid` border to the checkbox:
```css
labeled-checkbox:state(checked) {
border: solid;
}
```
CSS can also be used to match a custom state [within a custom element's shadow DOM](/en-US/docs/Web/CSS/:state#matching_a_custom_state_in_a_custom_elements_shadow_dom) by specifying `:state()` within the [`:host()`](/en-US/docs/Web/CSS/:host_function) pseudo-class function.
Additionally, the `:state()` pseudo-class can be used after the [`::part()`](/en-US/docs/Web/CSS/::part) pseudo-element to match the [shadow parts](/en-US/docs/Web/CSS/CSS_shadow_parts) of a custom element that are in a particular state.
> [!WARNING]
> Browsers that do not yet support [`:state()`](/en-US/docs/Web/CSS/:state) will use a CSS `<dashed-ident>` for selecting custom states, which is now deprecated.
> For information about how to support both approaches see the [Compatibility with `<dashed-ident>` syntax](#compatibility_with_dashed-ident_syntax) section below.
## Examples
### Matching the custom state of a custom checkbox element
This example, which is adapted from the specification, demonstrates a custom checkbox element that has an internal "checked" state.
This is mapped to the `checked` custom state, allowing styling to be applied using the `:state(checked)` custom state pseudo class.
#### JavaScript
First we define our class `LabeledCheckbox` which extends from `HTMLElement`.
In the constructor we call the `super()` method, add a listener for the click event, and call {{domxref("HTMLElement.attachInternals()", "this.attachInternals()")}} to attach an {{domxref("ElementInternals", "ElementInternals")}} object.
Most of the rest of the "work" is then left to `connectedCallback()`, which is invoked when a custom element is added to the page.
The content of the element is defined using a `<style>` element to be the text `[]` or `[x]` followed by a label.
What's noteworthy here is that the custom state pseudo class is used to select the text to display: `:host(:state(checked))`.
After the example below, we'll cover what's happening in the snippet in more detail.
```js
class LabeledCheckbox extends HTMLElement {
constructor() {
super();
this._boundOnClick = this._onClick.bind(this);
this.addEventListener("click", this._boundOnClick);
// Attach an ElementInternals to get states property
this._internals = this.attachInternals();
}
connectedCallback() {
const shadowRoot = this.attachShadow({ mode: "open" });
shadowRoot.innerHTML = `<style>
:host {
display: block;
}
:host::before {
content: "[ ]";
white-space: pre;
font-family: monospace;
}
:host(:state(checked))::before {
content: "[x]";
}
</style>
<slot>Label</slot>
`;
}
get checked() {
return this._internals.states.has("checked");
}
set checked(flag) {
if (flag) {
this._internals.states.add("checked");
} else {
this._internals.states.delete("checked");
}
}
_onClick(event) {
// Toggle the 'checked' property when the element is clicked
this.checked = !this.checked;
}
static isStateSyntaxSupported() {
return CSS.supports("selector(:state(checked))");
}
}
customElements.define("labeled-checkbox", LabeledCheckbox);
// Display a warning to unsupported browsers
document.addEventListener("DOMContentLoaded", () => {
if (!LabeledCheckbox.isStateSyntaxSupported()) {
if (!document.getElementById("state-warning")) {
const warning = document.createElement("div");
warning.id = "state-warning";
warning.style.color = "red";
warning.textContent = "This feature is not supported by your browser.";
document.body.insertBefore(warning, document.body.firstChild);
}
}
});
```
In the `LabeledCheckbox` class:
- In the `get checked()` and `set checked()` we use `ElementInternals.states` to get the `CustomStateSet`.
- The `set checked(flag)` method adds the `"checked"` identifier to the `CustomStateSet` if the flag is set and delete the identifier if the flag is `false`.
- The `get checked()` method just checks whether the `checked` property is defined in the set.
- The property value is toggled when the element is clicked.
We then call the {{domxref("CustomElementRegistry/define", "define()")}} method on the object returned by {{domxref("Window.customElements")}} in order to register the custom element:
```js
customElements.define("labeled-checkbox", LabeledCheckbox);
```
#### HTML
After registering the custom element we can use the element in HTML as shown:
```html
<labeled-checkbox>You need to check this</labeled-checkbox>
```
#### CSS
Finally we use the `:state(checked)` custom state pseudo class to select CSS for when the box is checked.
```css
labeled-checkbox {
border: dashed red;
}
labeled-checkbox:state(checked) {
border: solid;
}
```
#### Result
Click the element to see a different border being applied as the checkbox `checked` state is toggled.
{{EmbedLiveSample("Labeled Checkbox", "100%", 50)}}
### Matching a custom state in a shadow part of a custom element
This example, which is adapted from the specification, demonstrates that custom states can be used to target the [shadow parts](/en-US/docs/Web/CSS/CSS_shadow_parts) of a custom element for styling.
Shadow parts are sections of the shadow tree that are intentionally exposed to pages that use the custom element.
The example creates a `<question-box>` custom element that displays a question prompt along with a checkbox labeled "Yes".
The element uses the `<labeled-checkbox>` defined in the [previous example](#matching_the_custom_state_of_a_custom_checkbox_element) for the checkbox.
#### JavaScript
```js hidden
class LabeledCheckbox extends HTMLElement {
constructor() {
super();
this._boundOnClick = this._onClick.bind(this);
this.addEventListener("click", this._boundOnClick);
// Attach an ElementInternals to get states property
this._internals = this.attachInternals();
}
connectedCallback() {
const shadowRoot = this.attachShadow({ mode: "open" });
shadowRoot.innerHTML = `<style>
:host {
display: block;
}
:host::before {
content: "[ ]";
white-space: pre;
font-family: monospace;
}
:host(:state(checked))::before {
content: "[x]";
}
</style>
<slot>Label</slot>
`;
}
get checked() {
return this._internals.states.has("checked");
}
set checked(flag) {
if (flag) {
this._internals.states.add("checked");
} else {
this._internals.states.delete("checked");
}
}
_onClick(event) {
// Toggle the 'checked' property when the element is clicked
this.checked = !this.checked;
}
static isStateSyntaxSupported() {
return CSS.supports("selector(:state(checked))");
}
}
customElements.define("labeled-checkbox", LabeledCheckbox);
document.addEventListener("DOMContentLoaded", () => {
if (!LabeledCheckbox.isStateSyntaxSupported()) {
if (!document.getElementById("state-warning")) {
const warning = document.createElement("div");
warning.id = "state-warning";
warning.style.color = "red";
warning.textContent = "This feature is not supported by your browser.";
document.body.insertBefore(warning, document.body.firstChild);
}
}
});
```
First, we define the custom element class `QuestionBox`, which extends `HTMLElement`.
As always, the constructor first calls the `super()` method.
Next, we attach a shadow DOM tree to the custom element by calling [`attachShadow()`](/en-US/docs/Web/API/Element/attachShadow).
```js
class QuestionBox extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({ mode: "open" });
shadowRoot.innerHTML = `<div><slot>Question</slot></div>
<labeled-checkbox part="checkbox">Yes</labeled-checkbox>
`;
}
}
```
The content of the shadow root is set using [`innerHTML`](/en-US/docs/Web/API/ShadowRoot/innerHTML).
This defines a {{HTMLElement("slot")}} element that contains the default prompt text "Question" for the element.
We then define a `<labeled-checkbox>` custom element with the default text `"Yes"`.
This checkbox is exposed as a shadow part of the question box with the name `checkbox` using the [`part`](/en-US/docs/Web/HTML/Global_attributes/part) attribute.
Note that the code and styling for the `<labeled-checkbox>` element are exactly the same as in the [previous example](#matching_the_custom_state_of_a_custom_checkbox_element), and are therefore not repeated here.
Next, we call the {{domxref("CustomElementRegistry/define", "define()")}} method on the object returned by {{domxref("Window.customElements")}} to register the custom element with the name `question-box`:
```js
customElements.define("question-box", QuestionBox);
```
#### HTML
After registering the custom element, we can use the element in HTML as shown below.
```html
<!-- Question box with default prompt "Question" -->
<question-box></question-box>
<!-- Question box with custom prompt "Continue?" -->
<question-box>Continue?</question-box>
```
#### CSS
The first block of CSS matches the exposed shadow part named `checkbox` using the [`::part()`](/en-US/docs/Web/CSS/::part) selector, styling it to be `red` by default.
```css
question-box::part(checkbox) {
color: red;
}
```
The second block follows `::part()` with `:state()`, in order to match `checkbox` parts that are in the `checked` state:
```css
question-box::part(checkbox):state(checked) {
color: green;
}
```
#### Result
Click either of the checkboxes to see the color change from `red` to `green` when the `checked` state toggles.
{{EmbedLiveSample("Question box", "100%", 100)}}
### Non-boolean internal states
This example shows how to handle the case where the custom element has an internal property with multiple possible value.
The custom element in this case has a `state` property with allowed values: "loading", "interactive" and "complete".
To make this work, we map each value to its custom state and create code to ensure that only the identifier corresponding to the internal state is set.
You can see this in the implementation of the `set state()` method: we set the internal state, add the identifier for the matching custom state to `CustomStateSet`, and remove the identifiers associated with all the other values.
Most of the remaining code is similar to the example that demonstrates a single boolean state (we show different text for each state as the user toggles through them).
#### JavaScript
```js
class ManyStateElement extends HTMLElement {
constructor() {
super();
this._boundOnClick = this._onClick.bind(this);
this.addEventListener("click", this._boundOnClick);
// Attach an ElementInternals to get states property
this._internals = this.attachInternals();
}
connectedCallback() {
this.state = "loading";
const shadowRoot = this.attachShadow({ mode: "open" });
shadowRoot.innerHTML = `<style>
:host {
display: block;
font-family: monospace;
}
:host::before {
content: "[ unknown ]";
white-space: pre;
}
:host(:state(loading))::before {
content: "[ loading ]";
}
:host(:state(interactive))::before {
content: "[ interactive ]";
}
:host(:state(complete))::before {
content: "[ complete ]";
}
</style>
<slot>Click me</slot>
`;
}
get state() {
return this._state;
}
set state(stateName) {
// Set internal state to passed value
// Add identifier matching state and delete others
if (stateName === "loading") {
this._state = "loading";
this._internals.states.add("loading");
this._internals.states.delete("interactive");
this._internals.states.delete("complete");
} else if (stateName === "interactive") {
this._state = "interactive";
this._internals.states.delete("loading");
this._internals.states.add("interactive");
this._internals.states.delete("complete");
} else if (stateName === "complete") {
this._state = "complete";
this._internals.states.delete("loading");
this._internals.states.delete("interactive");
this._internals.states.add("complete");
}
}
_onClick(event) {
// Cycle the state when element clicked
if (this.state === "loading") {
this.state = "interactive";
} else if (this.state === "interactive") {
this.state = "complete";
} else if (this.state === "complete") {
this.state = "loading";
}
}
static isStateSyntaxSupported() {
return CSS.supports("selector(:state(loading))");
}
}
customElements.define("many-state-element", ManyStateElement);
document.addEventListener("DOMContentLoaded", () => {
if (!LabeledCheckbox.isStateSyntaxSupported()) {
if (!document.getElementById("state-warning")) {
const warning = document.createElement("div");
warning.id = "state-warning";
warning.style.color = "red";
warning.textContent = "This feature is not supported by your browser.";
document.body.insertBefore(warning, document.body.firstChild);
}
}
});
```
#### HTML
After registering the new element we add it to the HTML.
This is similar to the example that demonstrates a single boolean state, except we don't specify a value and use the default value from the slot (`<slot>Click me</slot>`).
```html
<many-state-element></many-state-element>
```
#### CSS
In the CSS we use the three custom state pseudo classes to select CSS for each of the internal state values: `:state(loading)`, `:state(interactive)`, `:state(complete)`.
Note that the custom element code ensures that only one of these custom states can be defined at a time.
```css
many-state-element:state(loading) {
border: dotted grey;
}
many-state-element:state(interactive) {
border: dashed blue;
}
many-state-element:state(complete) {
border: solid green;
}
```
#### Results
Click the element to see a different border being applied as the state changes.
{{EmbedLiveSample("Non-boolean internal states", "100%", 50)}}
## Compatibility with `<dashed-ident>` syntax
Previously custom elements with custom states were selected using a `<dashed-ident>` instead of the [`:state()`](/en-US/docs/Web/CSS/:state) function.
Browser versions that don't support `:state()` will throw an error when supplied with an ident that is not prefixed with the double dash.
If support for these browsers is required, either use a [try...catch](/en-US/docs/Web/JavaScript/Reference/Statements/try...catch) block to support both syntaxes, or use a `<dashed-ident>` as the state's value and select it with both the `:--my-state` and `:state(--my-state)` CSS selector.
### Using a try...catch block
This code shows how you can use `try...catch` to attempt adding a state identifier that does not use a `<dashed-ident>`, and fall back to `<dashed-ident>` if an error is thrown.
#### JavaScript
```js
class CompatibleStateElement extends HTMLElement {
constructor() {
super();
this._internals = this.attachInternals();
}
connectedCallback() {
// The double dash is required in browsers with the
// legacy syntax, not supplying it will throw
try {
this._internals.states.add("loaded");
} catch {
this._internals.states.add("--loaded");
}
}
}
```
#### CSS
```css
compatible-state-element:is(:--loaded, :state(loaded)) {
border: solid green;
}
```
### Using double dash prefixed idents
An alternative solution can be to use the `<dashed-ident>` within JavaScript.
The downside to this approach is that the dashes must be included when using the CSS `:state()` syntax.
#### JavaScript
```js
class CompatibleStateElement extends HTMLElement {
constructor() {
super();
this._internals = this.attachInternals();
}
connectedCallback() {
// The double dash is required in browsers with the
// legacy syntax, but works with the modern syntax
this._internals.states.add("--loaded");
}
}
```
#### CSS
```css
compatible-state-element:is(:--loaded, :state(--loaded)) {
border: solid green;
}
```
## Specifications
{{Specifications}}
## Browser compatibility
{{Compat}}
## See also
[Using custom elements](/en-US/docs/Web/API/Web_components/Using_custom_elements)