Custom State Pseudo Class

Draft Community Group Report,

This version:
https://wicg.github.io/custom-state-pseudo-class/
Editors:
(Google)
(Google)
Domenic Denicola (Google)

Abstract

This specification defines a way to expose custom element’s internal states, and defines the :state() pseudo-class matching to a custom element exposing a state. This specification is intended to be merged to [DOM], [HTML], and [selectors-4] in the future.

Status of this document

This specification was published by the Web Platform Incubator Community Group. It is not a W3C Standard nor is it on the W3C Standards Track. Please note that under the W3C Community Contributor License Agreement (CLA) there is a limited opt-out and other conditions apply. Learn more about W3C Community and Business Groups.

1. Introduction

1.1. Motivation

Build-in elements provided by user agents have certain “states” that can change over time depending on user interaction and other factors, and are exposed to web authors through pseudo classes. For example, some form controls have the “invalid” state, which is exposed through the :invalid pseudo-class.

Like built-in elements, custom elements can have various states to be in too, and custom element authors want to expose these states in a similar fashion as the built-in elements.

1.2. Solution

This specification defines an API to inform custom element's states to the user agent, and a pseudo-class to select elements with specific states. The former is the states IDL attribute of ElementInternals, and the latter is the :state() pseudo-class.

Assume that LabeledCheckbox doesn’t expose its "checked" state via a content attribute.

<!DOCTYPE html>
<body>
<!-- Basic usage: -->
<script>
class LabeledCheckbox extends HTMLElement {
  constructor() {
    super();
    this._internals = this.attachInternals();
    this.addEventListener('click', this._onClick.bind(this));

    const shadowRoot = this.attachShadow({mode: 'closed'});
    shadowRoot.innerHTML =
      `<style>
       :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.contains('checked'); }

  set checked(flag) {
    this._internals.states.toggle('checked', !!flag);
  }

  _onClick(event) {
    this.checked = !this.checked;
  }
}

customElements.define('labeled-checkbox', LabeledCheckbox);
</script>

<style>
labeled-checkbox { border: dashed red; }
labeled-checkbox:state(checked) { border: solid; }
</style>

<labeled-checkbox>You need to check this</labeled-checkbox>

<script>
class QuestionBox extends HTMLElement {
  constructor() {
    super();
    const shadowRoot = this.attachShadow({mode: 'closed'});
    shadowRoot.innerHTML =
      `<div><slot>Question</slot></div>
       <labeled-checkbox part='checkbox'>Yes</labeled-checkbox>`;
  }
}
customElements.define('question-box', QuestionBox);
</script>

<style>
question-box::part(checkbox) { color: red; }
question-box::part(checkbox):state(checked) { color: green; }
</style>

<question-box>Continue?</question-box>
</body>

2. Exposing custom element states

Each autonomous custom element has states token list, a non-attribute DOMTokenList object associated with the custom element, and initially associated with an empty set of strings.

partial interface ElementInternals {
  [SameObject, PutForwards=value] readonly attribute DOMTokenList states;
};

The states IDL attribute returns the states token list of this’s target element.

States token list can expose boolean states represented by existence/non-existence of string tokens. If an author wants to expose a state which can have three values, it can be converted to three exclusive boolean states. For example, a state called readyState with "loading", "interactive", and "complete" values can be mapped to three exclusive boolean states, "loading", "interactive", and "complete".
// Change the readyState from anything to "complete".
this._readyState = "complete";
this._internals.states.remove("loading", "interactive");
this._internals.states.add("complete");
// If this has no states other than _readyState, the following also works in
// addition to remove() and add().
// this._internals.states = "complete";

2.1. Non-attribute DOMTokenList

This section defines a variant of DOMTokenList, called non-attribute DOMTokenList. It is defined by [DOM] as if following edits were applied to the definition of DOMTokenList.

3. Selecting a custom element with a speicfic state

The :state() pseudo-class applies while an element has a certain state. "State" is a per-element information which can change over time depending on user interaction and other extrinsic factors. The :state() pseudo-classs must have one <ident> argument, otherwise the selector is invalid.

The :state() pseudo-class must match any element that is an autonomous custom element and whose states token list contains the specified <ident>.

:state() takes just one argument, and an element can expose multiple states. Authors can use :state() with logical pseudo-classes like x-foo:is(:state(state1), :state(state2)), x-foo:not(:state(state2)), and x-foo:state(state1):state(state2).

Conformance

Conformance requirements are expressed with a combination of descriptive assertions and RFC 2119 terminology. The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in the normative parts of this document are to be interpreted as described in RFC 2119. However, for readability, these words do not appear in all uppercase letters in this specification.

All of the text of this specification is normative except sections explicitly marked as non-normative, examples, and notes. [RFC2119]

Examples in this specification are introduced with the words “for example” or are set apart from the normative text with class="example", like this:

This is an example of an informative example.

Informative notes begin with the word “Note” and are set apart from the normative text with class="note", like this:

Note, this is an informative note.

Index

Terms defined by this specification

Terms defined by reference

References

Normative References

[CSS-VALUES-4]
Tab Atkins Jr.; Elika Etemad. CSS Values and Units Module Level 4. URL: https://drafts.csswg.org/css-values-4/
[DOM]
Anne van Kesteren. DOM Standard. Living Standard. URL: https://dom.spec.whatwg.org/
[HTML]
Anne van Kesteren; et al. HTML Standard. Living Standard. URL: https://html.spec.whatwg.org/multipage/
[INFRA]
Anne van Kesteren; Domenic Denicola. Infra Standard. Living Standard. URL: https://infra.spec.whatwg.org/
[RFC2119]
S. Bradner. Key words for use in RFCs to Indicate Requirement Levels. March 1997. Best Current Practice. URL: https://tools.ietf.org/html/rfc2119
[SELECTORS-4]
Elika Etemad; Tab Atkins Jr.. Selectors Level 4. URL: https://drafts.csswg.org/selectors/
[WebIDL]
Boris Zbarsky. Web IDL. URL: https://heycam.github.io/webidl/

IDL Index

partial interface ElementInternals {
  [SameObject, PutForwards=value] readonly attribute DOMTokenList states;
};