5 things I learned building Web Components

Over the past few years, I’ve had the opportunity to develop two distinct web component libraries for different clients. Throughout this journey, I discovered that adhering to a set of fundamental principles when creating new components helped me circumvent common pitfalls that often trap developers who are new to building reusable web components.

Shadow DOM

The Shadow DOM is crucial for making web components reusable. Its isolation allows us to create a clear API between the app and the web component, preventing unintended side-effects. However, this isolation comes with some challenges that need to be addressed.

  • Do not disable the Shadow DOM
    When encountering specific issues with the Shadow DOM, a common suggestion online is to disable it. If your goal is to create truly reusable components, avoid disabling the Shadow DOM. There are very few and exceptional scenarios where a reusable component should not use this isolation.

  • Be careful with :host styling
    To minimize DOM elements, you might use the host element to style the inside of your component. However, these styles can be altered from outside the component, leading to unexpected behavior. This is precisely what we want to avoid with most components.

  • Allow styling extensibility with custom properties
    For flexibility, utilize attributes/properties, slots, and CSS custom properties. To ensure adaptable styling, use CSS custom properties that can penetrate the Shadow DOM.

  • Server-side rendering (SSR)
    Shadow trees can't be rendered server-side. To address this, use declarative Shadow DOM and hydrate it client-side. If you're using Lit, refer to their SSR package: Lit SSR Overview. There are also plugins for integrating with common frameworks. Your solution will depend on how you create your web components.

Slots

Designing a new component often involves deciding how users will pass data. Attributes/properties are ideal for plain data, and CSS custom properties for style extensions. For more flexible structures, use the <slot> element.

Allowing flexible structures might lead to unintended uses, but it often enhances maintainability and reduces the need for adaptations.

For instance, a button component might need an icon. Instead of a property for specifying the icon, use a slot to let users pass in their desired icon, allowing them to directly set properties like size and color.

The ::slotted selector

As mentioned above, we might introduce many slots to allow flexibility with our components. Especially for elements like the <input> with tons of attributes and event handlers, over time, this will be way less effort to maintain. But now we also want to have control over how this element is being styled.

The ::slotted selector allows us to style slotted elements in a very limited fashion. It only allows us to target the top layer of slotted elements with compound selectors. You can find more about selectors here: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_selectors/Selector_structure#compound_selector.

In many cases, this isn't enough to implement our desired design. One interesting thing about slotted elements is, that they are not in the Shadow DOM, but they sit in what is called the "Light DOM". These elements can be styled from the outside. What we did in our library is to have styles in the form of: dss-input input {}. This makes sure, only the slotted input gets targeted, but allows us to style the input with full complex selectors.

To apply these styles, they must be present in both the web component and the user's app, since our input could be used directly by the user but also as a child in another component like a dropdown. We created a BaseElement for our components to inherit and include these styles in our base styles, which users must import. Check out our implementation in our Github repo: https://github.com/Zuehlke/design-system-starter.

Form Elements

As soon, as form elements are inside the Shadow DOM, an outside form will not see them as form fields and therefore not include them in the resulting FormData. This can be mitigated in two ways:

  1. Pass the actual form field into a slot, like in the example above with the <input> element. Since slotted elements will be in the Light DOM, they will be visible to the form and included in FormData.

  2. Use ElementInternals to let the browser know, that your web component is actually a form field. You can learn more about the details of this, in this blog post: https://software-engineering-corner.zuehlke.com/finally-custom-form-elements-that-dont-suck.

Accessibility for Testing

We use Testing Library for testing our components and the apps that use them. Initially, we encountered issues with selectors due to the Shadow DOM. To address this, we use the Shadow DOM Testing Library.

Additionally, we reviewed all components, setting roles and aria attributes to ensure selectors target useful elements. For form elements, carefully assign roles to improve usability and testing accuracy. We noticed that especially with form elements we initially had a hard time getting the elements that are actually useful to interact with.

Conclusion

I hope some of my advice might help you build better web components. Let me know what your thoughts on this are, if you agree with me or if you have other learnings that you want to share!

Did you find this article valuable?

Support The Web Engineer's Notebook by becoming a sponsor. Any amount is appreciated!