How to use Web Components like a Pro
A quick guide on how (and how not) to use Web Components as a developer on the web
Having issues using the Toolkit? Ask for help! If there are feature gaps, bugs, or just general questions, the Booster team will help!
What are Web Components?
Web Components are a collection of web APIs that enable developers to create reusable, encapsulated elements that work for everyone, regardless of framework or language choice.
The features that are relevant are:
Within each area are more features; Shadow DOM, for example, unlocks the <slot>
element and new CSS features, such as ::part
.
For more authoritative information, see MDN: https://developer.mozilla.org/en-US/docs/Web/API/Web_components.
Why are Web Components relevant to Booster?
Booster, the "Design System", provides one universal implementation for all teams via "the Toolkit". To achieve maximum support across multiple tools, the toolkit is implemented using Web Components, with the aid of the lit library. This means we can provide support for React, Vue, PHP, .NET, and other languages without having to author and maintain multiple implementations.
How to use Web Components
Did you know? The API of every component in the Booster toolkit is fully documented, including: attributes, properties, slots, methods, events, CSS parts, CSS custom properties, and CSS states.
To see how this documentation comes together, check out our customelement-manifest-element renderer.
Styling best practices
zui-button {
--zui-button-text-color: white;
--zui-button-color: var(--zui-green);
}
const button = document.querySelector('zui-button');
const div = button.shadowRoot.querySelector('div');
div.style.color = 'white';
div.style.backgroundColor = 'green';
✅ DO use CSS custom properties, shadow parts, and states
As part of the API of a Web Component, an author can choose to intentionally expose hooks for customization.
There are three main ways these can be accomplished.
CSS custom properties
CSS custom properties —sometimes also referred to as CSS variables— are a way to expose smaller pieces of CSS for customization, such as changing background colors.
Good example:
zui-button {
--zui-button-text-color: white;
--zui-button-color: var(--zui-green);
}
Did you know? ZUI exposes a number of CSS custom properties globally via @zywave/zui-base-styles
, such as colors.
CSS shadow parts
CSS shadow parts offer Web Component authors a way to expose specific elements in the shadow DOM of their custom elements. Think of CSS shadow parts as custom pseudo-elements, like ::before
and ::after
.
Good example:
zui-button::part(button) {
font-weight: bold;
color: red;
}
Note: It is only possible to target specifically the element exposed with the part
attribute. It is not possible to chain CSS selectors together to target a descendent element of the part.
Bad example:
/* This will not style the <option> child elements */
zui-select::part(control) > option {
color: hotpink;
}
CSS states
CSS states allow Web Component authors to provide style hooks for when an element is in a given internal state.
Think of CSS states as a way for authors to create custom pseudo-classes, like :invalid
.
Good example:
zui-progress:state(indeterminate) {
background: var(--zui-green);
}
❌ DO NOT pierce shadow roots
Avoid programmatically reaching into a component’s shadow root with JavaScript to manipulate its internal DOM (element.shadowRoot.querySelector(...)
, etc.). Doing so breaks encapsulation, makes components fragile, and tightly couples consumers to internal implementation details. Instead, rely only on the intentional customization APIs (custom properties, shadow parts, and states).
If there's a requirement that the current API of the component doesn't support, this is the perfect time to start a conversation with your designer and the Booster team to decide how best to proceed!
Bad example:
const button = document.querySelector('my-button');
const shadowLabel = button.shadowRoot.querySelector('div');
shadowLabel.style.color = 'red';
Manipulation of behavior/data
✅ DO use properties, methods, events, etc.
Web Components expose their intended APIs through well-defined properties, attributes, events, and more. This establishes a contract, so to speak, between authors and consumers and helps prevent breakage of applications.
Good example:
const dialog = document.querySelector('zui-dialog');
console.log(dialog.opened);
dialog.open();
dialog.addEventListener('close', () => {
console.log('Dialog closed');
});
❌ DO NOT use underscore prefixed properties/methods
A common practice in the JavaScript community is to denote private features of an object with a prefix, i.e. _myPrivateField
. While JavaScript added native private field support in ECMAScript 2022, some objects may not have been fully updated OR may have special reasons for why private fields aren't compatible (such as attaining similar functionality as protected
properties in other object-oriented programming languages).
Bad example:
const dialog = document.querySelector('zui-dialog');
dialog._opened = false;
Conclusion
When building and consuming Web Components, always work with the public and documented APIs.
As the authors of a Web Component library, we try extremely hard to not rollout breaking changes, as can be seen by over 5 years of automated updates to tens of applications at Zywave. But, when components are used in unexpected ways, unexpected consequences are sure to follow.