How to create a custom checkbox web component?
The HTMLElement.attachInternals()
method returns an ElementInternals
object. This method allows a custom element to participate in HTML forms. The ElementInternals
interface provides utilities for working with these elements in same way you would work with any standard HTML form element, and also exposes the Accessibility Object Model to the element.
Check the Browser compatibility table carefully before using this in production.
Syntax
export default class CustomCheckbox extends HTMLElement {
static formAssociated = true; // this is needed
#internals;
constructor() {
super();
this.#internals = this.attachInternals();
}
connectedCallback() {
console.log(this.#internals); // ElementInternals object
}
}
customElements.define('custom-checkbox', CustomCheckbox);
We need to set a static property formAssociated
to be true
to tell browser engine that our custom checkbox is a form-associated element. More on this property later.
Now we’ve defined our custom checkbox element, we can use it inside a form element.
<form method="POST" name="myForm">
<custom-checkbox></custom-checkbox>
</form>
ElementInternals.shadowRoot
The shadowRoot
read-only property of the ElementInternals
interface returns the ShadowRoot
for this element, or null if the custom element does not have a shadow root.
Now if we add a shadow root to our custom element.
export default class CustomCheckbox extends HTMLElement {
static formAssociated = true; // this is needed
#internals;
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.#internals = this.attachInternals();
}
connectedCallback() {
console.log(this.#internals); // ElementInternals object
console.log(checkbox.#internals.shadowRoot); // a shadowRoot object
}
}
customElements.define('custom-checkbox', CustomCheckbox);
ElementInternals.form
The form
read-only property of the ElementInternals
interface returns the HTMLFormElement
associated with this element.
<form method="POST" name="myForm">
<custom-checkbox></custom-checkbox>
</form>
export default class CustomCheckbox extends HTMLElement {
static formAssociated = true; // this is needed
#internals;
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.#internals = this.attachInternals();
}
connectedCallback() {
console.log(this.#internals); // ElementInternals object
console.log(checkbox.#internals.shadowRoot); // a shadowRoot object
console.log(checkbox.#internals.form); // <form method="POST" name="myForm"> element
console.log(checkbox.#internals.form instanceof HTMLFormElement) // true
console.log(checkbox.#internals.form.name) // myForm
console.log(checkbox.#internals.form.length) // 1
}
}
customElements.define('custom-checkbox', CustomCheckbox);
ElementInternals.states
The states
read-only property of the ElementInternals
interface returns a CustomStateSet representing the possible states of the custom element.
A CustomStateSet which is a Set of strings.
Now we can update our CustomCheckbox
class.
export default class CustomCheckbox extends HTMLElement {
static formAssociated = true; // this is needed
#internals;
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.#internals = this.attachInternals();
}
get checked() {
return this.#internals.states.has('--checked');
}
set checked(flag) {
if (flag) {
this.#internals.states.add('--checked');
} else {
this.#internals.states.delete('--checked');
}
console.log(this.#internals.states.has('--checked'));
}
}
customElements.define('custom-checkbox', CustomCheckbox);
To set the checked
state of our custom checkbox to true
, we can add the checked
attribute. Additionally, we can add a click event listener to toggle the checked state of the checkbox.
<form method="POST" name="myForm">
<custom-checkbox checked></custom-checkbox>
</form>
export default class CustomCheckbox extends HTMLElement {
static formAssociated = true; // this is needed
#internals;
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.#internals = this.attachInternals();
}
get checked() {
return this.#internals.states.has('--checked');
}
set checked(flag) {
if (flag) {
this.#internals.states.add('--checked');
} else {
this.#internals.states.delete('--checked');
}
console.log(this.#internals.states.has('--checked'));
}
connectedCallback() {
this.checked = this.hasAttribute('checked');
this.addEventListener('click', () => this.checked = !this.checked);
}
}
customElements.define('custom-checkbox', CustomCheckbox);
Here’s how to style the custom checkbox outside of its shadow root.
custom-checkbox {
/* unchecked styles */
}
custom-checkbox:--checked {
/* checked styles */
}
Here’s how to style the custom checkbox inside of its shadow root.
:host {
/* unchecked styles */
}
:host(:--checked) {
/* checked styles */
}
ElementInternals.setFormValue()
To complete the form submission process, we need to include a checkbox value that can be sent to FormData(). This can be achieved by using the setFormValue() method of the ElementInternals interface. This method sets the submission value and state of the element, which is then communicated to the user agent.
export default class CustomCheckbox extends HTMLElement {
static formAssociated = true; // this is needed
#internals;
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.#internals = this.attachInternals();
}
get checked() {
return this.#internals.states.has('--checked');
}
set checked(flag) {
if (flag) {
this.#internals.states.add('--checked');
// when checked, we set a form value of 'on'
this.#internals.setFormValue('on', '--checked');
} else {
this.#internals.states.delete('--checked');
// when unchecked, we set a form value of 'off'
this.#internals.setFormValue('off');
}
console.log(this.#internals.states.has('--checked'));
}
connectedCallback() {
this.checked = this.hasAttribute('checked');
this.addEventListener('click', () => this.checked = !this.checked);
}
}
customElements.define('custom-checkbox', CustomCheckbox);
To test our form, we can include a submit button within it.
<form method="POST" name="myForm">
<custom-checkbox checked name="custom-checkbox"></custom-checkbox>
<button type="submit">Submit</button>
</form>
document.querySelector('form[name="myForm"]').addEventListener('submit', (e) => {
e.preventDefault();
const formData = new FormData(e.target);
// prints 'on' if checkbox is checked, 'off' if unchecked
console.log(formData.get('custom-checkbox'));
});