6/28/23, 4:48 PM Web Components Can Now Be Native Form Elements | JavaScript in Plain English
Open in app
This is your last free member-only story this month. Upgrade for unlimited access.
Member-only story
Web Components Can Now Be Native Form
Elements
Here’s the complete guide to customised form controls
Danny Moerkerke · Follow
Published in JavaScript in Plain English
7 min read · Aug 10, 2022
Listen Share More
Photo by Chris Hainto on Unsplash
Custom Form Controls
One area in which developers have always wanted to customize elements is forms.
https://javascript.plainenglish.io/web-components-can-now-be-native-form-elements-107c7a93386 1/19
6/28/23, 4:48 PM Web Components Can Now Be Native Form Elements | JavaScript in Plain English
Historically, it has often been hard to style form controls to give them the look and
feel you want.
The styling options are often limited and to this day, form controls like date and
color pickers are still inconsistent across browsers.
Many websites also need more advanced form controls that the native platform
simply doesn’t provide (yet).
Web Components are ideal candidates for customised form controls because they
are first-class HTML tags but unfortunately, they don’t provide the functionality of
built-in form controls out of the box.
For example, when a form is submitted, customised form controls built with
Custom Elements are not included in the form data.
Other functionalities like form validation and autofill are also not available to
Custom Elements and are hard to replicate.
Luckily, there are two solutions available for these issues: the formdata event and the
ElementInternals interface.
FormData event
The formdata event is fired when a form is submitted and enables any JavaScript
code to add arbitrary data to the form data that is submitted.
You can set an event listener for this event that will receive the event object with a
formData property that contains all data being submitted by the form.
Each event listener can then add to or modify the data before it’s submitted:
const form = document.querySelector(‘form’);
form.addEventListener(‘formdata’, ({formData}) => {
// add data
formData.append(‘custom-control’, customValue);
// modify data
formData.set(‘email’, formData.get(‘email’).toLowerCase());
});
https://javascript.plainenglish.io/web-components-can-now-be-native-form-elements-107c7a93386 2/19
6/28/23, 4:48 PM Web Components Can Now Be Native Form Elements | JavaScript in Plain English
The formData property of the event is a FormData object.
ElementInternals interface
While the formData event is very handy, it is limited to adding arbitrary data to the
data being submitted by the form.
For a Custom Element to be a true form control it should also be automatically
associated with the form and participate in form validation.
That is what the ElementInternals interface is for.
It enables Custom Elements to be associated with a form so they are added to the
elements property of the form and the value of the element will be automatically
submitted with the form.
It also enables Custom Elements to participate in form validation.
The element can indicate whether its value is valid or invalid and prevent the form
from being submitted when it’s invalid.
It can also be labeled with a <label> element which means the label is
programmatically associated with the form element.
When a user clicks or taps the label, the form element will receive focus.
This increases the area for focusing on the element which provides a good user
experience, especially on mobile devices.
It will also cause screen readers to read out the label when the user focuses on the
form element.
Associating a Custom Element with a form
The first step to creating a custom form control with a Custom Element is to
associate it with the form.
This means it will be part of the form’s elements property, which is an
HTMLFormControlsCollection containing all form controls contained by the form.
In the previous example, you have seen how you can add entries to a FormData
object using its append method.
https://javascript.plainenglish.io/web-components-can-now-be-native-form-elements-107c7a93386 3/19
6/28/23, 4:48 PM Web Components Can Now Be Native Form Elements | JavaScript in Plain English
It’s also possible to create a FormData object directly from all elements and their
values in a form by passing the form to the FormData constructor:
const form = document.querySelector(‘form’);
// formData now contains all data of form
const formData = new FormData(form);
formData now contains all data of the form including any Custom Elements that
have been associated with the form.
Here’s the class of a form-associated Custom Element:
class FormInput extends HTMLElement {
static formAssociated = true;
constructor() {
super();
this.internals = this.attachInternals();
}
get value() {
return this._value;
}
set value(value) {
this._value = value;
this.internals.setFormValue(value);
}
get form() {
return this.internals.form;
}
get name() {
return this.getAttribute(‘name’);
}
get type() {
return this.localName;
}
}
This example shows a minimal implementation of a form-associated Custom
Element. Let’s look at the individual parts.
https://javascript.plainenglish.io/web-components-can-now-be-native-form-elements-107c7a93386 4/19
6/28/23, 4:48 PM Web Components Can Now Be Native Form Elements | JavaScript in Plain English
static formAssociated = true;
By setting the static formAssociated property to true , the Custom Element will be
associated with the form and it will be included in the form’s elements property.
This won’t however include the value of the Custom Element in the form data yet.
For that, the ElementInternals interface will need to be attached to the element:
this.internals = this.attachInternals();
attachInternals returns the ElementInternals object which is then stored in the
internals property of the Custom Element.
In order to get the value of the Custom Element that will be submitted with the
form, this value needs to be set with:
this.internals.setFormValue(value);.
In the example, this line has been added to the setter for the value property to
make sure that whenever value is set, its form value will also be set.
This is the value that will be submitted with the form.
The other getters for form , name , and type are standard properties that native form
elements have.
This is the bare minimum your component should have to become a custom form element.
Of course, this component doesn’t render anything yet so it’s not very useful. Let’s
change that by adding a Shadow Root with an <input> element:
constructor() {
super();
this.internals = this.attachInternals();
https://javascript.plainenglish.io/web-components-can-now-be-native-form-elements-107c7a93386 5/19
6/28/23, 4:48 PM Web Components Can Now Be Native Form Elements | JavaScript in Plain English
const shadowRoot = this.attachShadow({mode: ‘open’});
shadowRoot.innerHTML = `
<style>
:host {
display: inline-block;
}
input {
display: block;
padding: 5px;
}
</style>
<input type=”text”>
`;
}
Now that the component has an <input> , we first need to make sure that the value
of the input is available as the value of the component that is submitted with the
form.
Recall that we created a setter for the value property that calls
this.internals.setFormValue(value) .
We can get the value of the input each time it changes through its change event.
When this event is fired, we simply set the value property of our component which
will call the setter which calls this.internals.setFormValue(value) .
Let’s add the needed event listener:
this.input = this.shadowRoot.querySelector(‘input’);
this.input.addEventListener(‘change’, (e) => {
this.value = this.input.value;
});
Now every time the value of the input changes, the value of the component is
updated and the correct value will be submitted with the form.
Of course, the other way around should also work: when the value property of the
component is set, the value property of the <input> should also be set.
https://javascript.plainenglish.io/web-components-can-now-be-native-form-elements-107c7a93386 6/19
6/28/23, 4:48 PM Web Components Can Now Be Native Form Elements | JavaScript in Plain English
The setter should be changed to this:
set value(value) {
this._value = value;
this.input.value = value; // set the value of the input
this.internals.setFormValue(value);
}
And the code that uses our component will also expect a change event to be fired
since it now contains an <input> element.
Therefore, the event from the <input> should be forwarded.
We can’t simply dispatch it again, since that will throw an error saying the event was
already dispatched but we can clone it and then forward it:
this.input.addEventListener(‘change’, (e) => {
const clone = new e.constructor(e.type, e); // clone the event
this.dispatchEvent(clone); // and then forward it
this.value = this.input.value;
});
Now the value of the component and the input are kept in sync and the component
fires a change event.
Labeling the custom form control
Now that you have associated your Custom Element with the form, you can also
associate it with a <label> .
The benefit of this is that the Custom Element will also be programmatically
associated with the label.
This means, for example, that a screen reader will read out the label text when a
user focuses the input and that when the label is clicked or tapped, the associated
input will be focused.
This increases the touch area of the input, making it easier to focus, especially on a
mobile device.
https://javascript.plainenglish.io/web-components-can-now-be-native-form-elements-107c7a93386 7/19
6/28/23, 4:48 PM Web Components Can Now Be Native Form Elements | JavaScript in Plain English
There are two ways of associating a form control with a <label> .
The simplest is to place the form control inside the <label> .
This makes the association very clear:
<label>
City
<input type=”text” name=”city”>
</label>
The other way is to use the for attribute of the <label> .
The value of this attribute should be the id of the form control you want to
associate it with:
<label for=”city”>City</label>
<input type=”text” id=”city”>
This may come in handy when the label and the form control are in different places
in the DOM or when you can’t place the control inside the label for some reason.
There are two additional things that need to be done to make clicking the label
focus the <input> inside your custom form control.
First, the component should be made focusable. This is done by adding a tabindex
attribute.
This attribute indicates that an element is focusable and the order in which
elements will be focused when a user uses the TAB key to navigate through form
controls.
Normally, the focus order relies on the order in which the elements occur in the
DOM but with tabindex this can be altered.
It means that for example an element with tabindex 4 will be focused before an
element with tabindex 5 but after an element with tabindex 3.
https://javascript.plainenglish.io/web-components-can-now-be-native-form-elements-107c7a93386 8/19
6/28/23, 4:48 PM Web Components Can Now Be Native Form Elements | JavaScript in Plain English
When two elements have the same tabindex the order in the DOM takes precedence
so if you want to keep this order but need to add tabindex to make elements
focusable, you can use tabindex="0" for all of them.
To make sure the element always has a tabindex attribute, you can check if it’s
present and add it if it’s not:
if (!this.hasAttribute(‘tabindex’)) {
this.setAttribute(‘tabindex’, ‘0’);
}
Now that your component is focusable, it will be focused when the <label> it’s
associated with is clicked or tapped.
But since the <input> inside it needs to be focused, the focus should be delegated
with the following code:
this.addEventListener(‘focus’, () => this.input.focus());
Now when the <label> is clicked, the <input> inside the component will be
focused.
Here’s the complete component:
class FormInput extends HTMLElement {
static formAssociated = true;
constructor() {
super();
this.internals = this.attachInternals();
const shadowRoot = this.attachShadow({mode: ‘open’});
shadowRoot.innerHTML = `
<style>
:host {
display: inline-block;
}
input {
display: block;
padding: 5px;
https://javascript.plainenglish.io/web-components-can-now-be-native-form-elements-107c7a93386 9/19
6/28/23, 4:48 PM Web Components Can Now Be Native Form Elements | JavaScript in Plain English
}
</style>
<input type=”text”>
`;
}
connectedCallback() {
this.input = this.shadowRoot.querySelector(‘input’);
this.input.addEventListener(‘change’, (e) => {
const clone = new e.constructor(e.type, e);
this.dispatchEvent(clone);
this.value = this.input.value;
});
this.addEventListener(‘focus’, () => this.input.focus());
if (!this.hasAttribute(‘tabindex’)) {
this.setAttribute(‘tabindex’, ‘0’);
}
}
get value() {
return this._value;
}
set value(value) {
this._value = value;
this.internals.setFormValue(value);
}
get form() {
return this.internals.form;
}
get name() {
return this.getAttribute(‘name’);
}
get type() {
return this.localName;
}
}
And here’s a working codepen:
HTML JS Result
https://javascript.plainenglish.io/web-components-can-now-be-native-form-elements-107c7a93386 10/19
6/28/23, 4:48 PM Web Components Can Now Be Native Form Elements | JavaScript in Plain English
Custom Element form control
Be sure to check my follow-up article that explains how Web Components can
participate in native form validation.
Join Modern Web Weekly, my weekly newsletter on the modern web platform, web
components and Progressive Web Apps.
JavaScript Web Development Web Components Programming Coding
Follow
Written by Danny Moerkerke
1.6K Followers · Writer for JavaScript in Plain English
https://javascript.plainenglish.io/web-components-can-now-be-native-form-elements-107c7a93386 11/19
6/28/23, 4:48 PM Web Components Can Now Be Native Form Elements | JavaScript in Plain English
I write about what the modern web is capable of, Web Components and PWA, creator of
https://whatpwacando.today
More from Danny Moerkerke and JavaScript in Plain English
Danny Moerkerke in ITNEXT
How To Hydrate A Server-Side Rendered Web Component
An in-depth guide to lazy loading Web Components
· 12 min read · May 11
39
https://javascript.plainenglish.io/web-components-can-now-be-native-form-elements-107c7a93386 12/19
6/28/23, 4:48 PM Web Components Can Now Be Native Form Elements | JavaScript in Plain English
The woman in JavaScript in Plain English
A 20-year-experienced CTO’s Advice “Don’t Be a Humble Developer”
I think 90% of the developers are victims of their modesty!!
· 4 min read · Jun 12
1.5K 26
Somnath Singh in JavaScript in Plain English
Bill Gates: People Don’t Realize What’s Coming
https://javascript.plainenglish.io/web-components-can-now-be-native-form-elements-107c7a93386 13/19
6/28/23, 4:48 PM Web Components Can Now Be Native Form Elements | JavaScript in Plain English
Tech Jobs Won’t Exist in 5 Years
· 13 min read · Apr 13
14.1K 347
Danny Moerkerke in ITNEXT
How To Server-Side Render A Web Component
Blazing fast lazy-loaded Web Components
· 6 min read · Apr 29
78 1
See all from Danny Moerkerke
See all from JavaScript in Plain English
https://javascript.plainenglish.io/web-components-can-now-be-native-form-elements-107c7a93386 14/19
6/28/23, 4:48 PM Web Components Can Now Be Native Form Elements | JavaScript in Plain English
Recommended from Medium
Danny Moerkerke in ITNEXT
The Hidden Power of Custom States For Web Components
A crucial step in the evolution of Custom Elements
· 6 min read · Nov 16, 2022
299 2
https://javascript.plainenglish.io/web-components-can-now-be-native-form-elements-107c7a93386 15/19
6/28/23, 4:48 PM Web Components Can Now Be Native Form Elements | JavaScript in Plain English
Jennifer Fu in Better Programming
Ant Design System Dark Mode and Other Theme Customization
It takes one line of code to set up dark mode for an Ant Design System app
· 12 min read · Feb 21
225 1
Lists
General Coding Knowledge
20 stories · 27 saves
Stories to Help You Grow as a Software Developer
19 stories · 149 saves
It's never too late or early to start something
10 stories · 9 saves
Coding & Development
11 stories · 15 saves
https://javascript.plainenglish.io/web-components-can-now-be-native-form-elements-107c7a93386 16/19
6/28/23, 4:48 PM Web Components Can Now Be Native Form Elements | JavaScript in Plain English
pandaquests in Level Up Coding
Difference between service workers and web workers
Web workers have been quite around for a while. Service worker is a new-ish concept. Both
enable you to run JavaScript in the background…
· 4 min read · Jan 3
81
JP Brown
https://javascript.plainenglish.io/web-components-can-now-be-native-form-elements-107c7a93386 17/19
6/28/23, 4:48 PM Web Components Can Now Be Native Form Elements | JavaScript in Plain English
What Really Happens to a Human Body at Titanic Depths
A Millisecond-by-Millisecond Explanation
· 4 min read · 5 days ago
23K 276
Rakia Ben Sassi in Level Up Coding
Unleash the Cheetah: A Real-World Case Study of Optimizing Angular
Rendering Performance
Boost your application’s speed and improve user experience with these proven techniques
· 11 min read · Feb 6
108
https://javascript.plainenglish.io/web-components-can-now-be-native-form-elements-107c7a93386 18/19
6/28/23, 4:48 PM Web Components Can Now Be Native Form Elements | JavaScript in Plain English
Israel Miles in Level Up Coding
Build Reusable and Customized Web Components Using Stencil.js
Increase your web development flexibility with your own unique HTML tags.
· 3 min read · Jan 8
52
See more recommendations
https://javascript.plainenglish.io/web-components-can-now-be-native-form-elements-107c7a93386 19/19