Best practices to build Web Components

Carlos Muñoz
ING Blog
Published in
9 min readMar 23, 2021
Photo by Xavi Cabrera on Unsplash

We all know by now how useful Web Components can be. They provide a multitude of advantages that can help you and enhance your experience of building Web Applications, or a Design System or simply when building something that is going to be reused everywhere in your applications.

However, what are the key things that you should take into account when you start building a new Web Component?

Based on my first year of experience at ING, I think these are some common good practices:

  • One Web Component, one responsibility
  • Name-spacing and its global Scope issue
  • Attributes/Properties for data in, custom events for data out
  • Decide whether to fail in silence or not
  • Create a good README for your component
  • Make it responsive, accessible, and customizable
  • Think about performance
  • Make it maintainable

One Web Component, one responsibility 🧐

First things first though the best approach when developing Web Components is to keep it as simple as possible. Try to create simple components with one kind of functionality. Do not try to resolve all the problems at the same time. If you have complex requirements, split them into different smaller components and finally mix them into one main component with all the required functionality. Remember that if your component can only handle one thing, it needs very little attributes/properties and events and it can be reused more than once.

If you notice your file becoming too large, it could be time to reconsider and refactor or split some of the functionality between different components or mixins.

Name-spacing and its global Scope issue 🤔

As you know the Custom Elements API allows you to extend HTML and define custom tags for your Web Components. The issue here is that you need to be very careful with the name of your tag because the tag will be shared in the whole HTML document and cannot be defined again:

class MyAwesomeComponent extends HTMLElement {...}window.customElements.define('my-awesome-component', MyAwesomeComponent);class MyAwesomeComponentV2 extends HTMLElement {...}// This will cause an error
window.customElements.define('my-awesome-component', MyAwesomeComponentV2);
Error: Failed to execute ‘define’ on ‘CustomElementRegistry’: the name “my-awesome-component” has already been used with this registry

This means that you cannot have two Custom Elements within the same tag.

There is a remote chance that this will happen to you or your organization. If you create a library of components that can be shared in your application between different pages, A and B, and then page B uses a new version of that same component, you will get this error.

This will also happen if you import two files that are defining the same component:

header.js

Class Header extends HTMLElement { ... }window.customElements.define('my-header', Header);

pageA.js

import './header.js';Class PageA extends HTMLElement {// We use the header component somewhere inside this PageA component.}window.customElements.define('my-page-a', PageA);

pageB.js

import './header.js';Class PageB extends HTMLElement {// We use the header component somewhere inside this PageB component.}window.customElements.define('my-page-b', PageA);

app.js

import './pageA.js';
import './pageB.js'; // this will cause the same error
Class App extends HTMLElement {// We use page A and B in this component}window.customElements.define('my-app', App);

So how should we solve this big issue?

A proposal has been put forward to create Custom Element Registries but while this is being prepared, you use Lit Element to use 💪 scoped-elements.

If you are not using LitElement, my advice here is to be careful with the tag that you choose for your component:

  • Try to make it unique with a good tag name
  • Try to put the name of your company or design system in the tag
  • Define a good naming convention for your components:
  • Example: “nameOfMyCompany-UniqueComponentName” -> “ing-input”

Attributes/Properties for data in, custom events for data out

I agree with you that the difference between between properties and attributes can be confusing. Attributes are provided in the HTML itself and properties are available on a DOM node. Also some properties reflect their values as attributes, so if a property is changed using JavaScript, the corresponding attribute is also changed at the same time to reflect the new value.

Have a look to this example:

<!-- index.html --><img src="ingLogo.svg" class="orange" alt="ING logo" width="150" height="250" />
<!-- class, alt, width and height are attributes -->
// index.jsconst ingLogo = document.querySelector('.orange');// className is a property
ingLogo.className = 'blackAndWhite'; // the attribute class is changed from 'orange' to 'blackAndWhite'

Attributes

Use attributes to receive non-complex data in your component. Try to create them for boolean values that are self-explanatory, like ‘disabled’, ‘read-only’, ‘special-behavior-mode’…use them as flags to pass to your components so that they will behave one way or another.

Do not override author-set or global attributes that exist in all HTML Elements, like “tabindex” or role. Remember this part belongs to the final developer who is going to use your component.

Do not self-apply classes. If you need to express a change in your component, do it by using attributes like disabled, but not adding a class. Another way of doing this is by sending an event.

<my-awesome-component disabled></my-awesome-component>

Properties

Properties are best suited for complex data, like arrays or objects. If your component starts to get a lot of properties, try to simplify it by creating one big object of properties:

const myConfig = {
name: 'My name',
surName: 'My name',
address: 'My name',
phone: 'My name',
...
}

Note: This example is using lit-html syntax.

<my-awesome-component disabled .config="${myConfig}"></my-awesome-component>

Custom events

Dispatch custom events when your component needs to communicate something to the outside world. Just be careful with the name and how you use the bubbles and composed properties:

class MyComponent extends HTMLElement {
connectedCallback() {
const event = new CustomEvent('my-component-hello-event', {
detail: { message: 'Hello from MyComponent class' },
bubbles: true,
composed: true,
});
this.dispatchEvent(event);
}
}

Remember, if bubbles property is true, it means that your event will bubble up through the DOM to the top element, so it is imperative to pay attention to the name you give to the event, because another component could be listening and do something about it.

If composed is true, your event will propagate across the Shadow DOM boundary into the standard DOM, so again consider if this is something you need or not. The use of this property is not recommended when not needed since it breaks encapsulation.

Dispatch events in response to internal component activity. For example, when you have a portion of a form in your component and you need to inform the parent component about the changes the user just made in your input elements.

Do not dispatch events in response to the host setting a property (downward data flow). For example, when a parent component sets a new property value to a child component, the child component does not need to inform the parent about that change since the parent already knows about that.

Decide whether to fail in silence or not 🤫

Imagine that you develop a new component that needs a property to work properly. If this property is not initialized then you have two options.

The first one, fail silently as the DOM elements do. For example, have you ever noticed an error in your browser when you do something like this?

<h1>
<li>Link 1</li>
<li>Link 2</li>
<li>Link 3</li>
<li>Link 4</li>
</h1>

Or when you do something like this:

<input type="randomTypeIHaveJustInvented" value="123" />

For this last example, I think it will be better to throw an exception and help the developer that is using your component. For example:

<my-custom-input type="randomTypeIHaveJustInvented" value="123"></my-custom-input>
Error: Type 'randomTypeIHaveJustInvented' is not allowed. Please use these types: text, number, or password.

You should validate the key important things in your component and throw an error if the requirements of your component are not fulfilled. However, try to avoid console logs or console warnings for little things. Simplify, your component works or it does not work. If it does not work, then throw an exception.

Why? Simply because you will never know who and where someone is using your component. This is very difficult to understand when you are building an application full of Web Components. You try to solve the requirements of that application… But what happens if in the future some component gets used by another application?

Create a good README for your component 🤓

Bad documentation is better than no documentation, but in this case you need to create perfect documentation for your component. Remember, the key here is that you do not know where your component will be used and by whom, so document everything to facilitate the process.

There is an ongoing discussion about the standard way to do this, which is called “custom-elements-manifest”.

Also, there are some tools like “web-component-analyzer” that could help you out here. This tool can automatically generate a readme file for your component.

But if you prefer to do it manually, this is a skeleton that I usually use for my components:

# TitleDescription in a few words.# InstallationSnippet to copy and paste with your install script.# UsageA snippet of how any Web Application can call your component with some parameters# Attributes| Name | Type | Description | Optional |# Properties| Name | Type | Description | Optional |# Events| Name | Type | Description |# NotesAdd extra things to consider or a _how to contribute_ section.

Make it responsive, accessible, and customizable

I have already mentioned this in this article you will never know when and where someone is using your component, so it is extremely important to create a component that auto-adapts itself to the given space. A component that can be operational with a screen reader, with the keyboard, with the mouse, or under a touch screen is accessible for everyone. A component that can have several languages, currencies, or even different themes (darkMode).

This is critical because if your component cannot adapt to the requirements, this means your component is unusable.

Don’t go overboard here. You do not need to create a component that can be a plug & play everywhere, but at least you should create a component that can be customizable for the final developer by allowing it to receive new languages, currencies, change some CSS custom properties, etc…

Also, it is advised to use the shadowDom to encapsulate your component’s styles so that your CSS rules do not overflow onto the main document or your component does not get affected by the external styles; only by CSS custom properties or font-family.

Set a :host display style (e.g. block, inline-block, flex) unless you prefer the default of inline. Try to use SVGs for images embedded in your component, do not use base64 images here, it will make your component very big and these images will not adapt to screen size changes.

Think about performance 🚀

Web Components are fast. They do not require any library to be imported since they use standard Web APIs to work in the browser. However, I recommend you pay close attention to the first paint.

The first paint is what the final user is going to see first, so if your component is rendering nothing at the beginning because it is parsing or loading some external data you must show some feedback to the user.

The advice here is not to do anything that you do not strictly need at the beginning and also to try to have fewer external dependencies. If you could do something manually instead of importing a library it would work faster than importing that library.

Make it maintainable 🛠

As you know, Web Components are a great way to encapsulate code with some functionality that will work pretty much on any Web Application. However, this awesomeness goes away if you do not make your component easily maintainable and you create a new Web Component every time you need almost the same functionality.

For this, I advise you to use these techniques:

  • Use Semantic Versioning
  • Include a CHANGELOG file to easily track all changes in your component
  • Use _ as a prefix for private methods
  • Write unit test, try to get at least 80% coverage
  • Use jsDoc to properly document your code
  • Use Storybook to show off the features of your component
  • Use the latest standard EcmaScript
  • Import any 3rd party node modules using bare imports.

Conclusion

Web Components are going be a important part in the future of Web Development. They are standard, fast, and highly reusable.

I have tried to create a nice compilation of great tips in this article. I hope you found it useful, however this is not set in stone so I would be open to hearing your feedback and thoughts on the matter.

🎉 Thanks for reading!

--

--