CSS web components

Custom elements as a styling solution

Dan Webb
Author
Dan Webb
Published
Mar 14, 2024
Topics
Industry, Engineering, How-to

Since we’re building out a showcase for Diamond UI heavily favouring HTML web components seeing web components wrapping HTML elements has become totally normal.

<custom-button>
<button>Button</button>
</custom-button>

Any element with a “-” in its name will be treated as a valid HTML element, no JavaScript necessary, the element can be used in CSS right away.

custom-button button {
background: pink;
}

So, the custom element only exists to enhance an HTML element style. We call this a CSS web component. The name might not be technically correct because it’s not a fully-fledged web component, but it feels right.

Nothing new here

This has always been possible, even before web components came along, and we’re certainly not the first ones to use it.

But the introduction of web components legitimises the syntax, where before it would have been strange to see HTML markup containing custom elements it’s quickly becoming expected.

While the approach isn’t new, there are some hidden benefits to using this styling solution that might not be immediately obvious.

Classes or web components

Until now, classes would have been used to apply these custom styles on top of the base HTML.

We’re not replacing every class with a custom element, but we think there’s a strong case to consider doing so as soon as multiple classes are required to manage variations for a single element.

Take this example with classes.

<style>
.custom-button {
background: var(--button-default-background);
&.custom-button--primary {
background: var(--button-primary-background);
}
&.custom-button--secondary {
background: var(--button-secondary-background);
}
&.custom-button--size-sm {
font-size: var(--font-size-sm);
}
&.custom-button--rounded {
border-radius: var(--border-radius);
}
}
</style>
<button
class="custom-button custom-button--primary custom-button--size-sm custom-button--secondary custom-button--rounded"
>
Button
</button>

Compared to the same example with a custom element and attribute selectors instead of classes:

<style>
custom-button {
button {
background: var(--button-default-background);
}
&[variant="primary"] button {
background: var(--button-primary-background);
}
&[variant="secondary"] button {
background: var(--button-secondary-background);
}
&[size="sm"] button {
font-size: var(--font-size-sm);
}
&[rounded] button {
border-radius: var(--border-radius);
}
}
</style>
<custom-button variant="primary" size="sm" rounded>
<button>Button</button>
</custom-button>

We think the custom element offers a more elegant API and that’s important – did you spot the error in the example with classes?

Both the primary and secondary classes are applied causing a clash. This is an easy oversight to make given a long string of classes with lots of duplication.

Typing

One big bonus of using custom elements is type hints and autocomplete inside the code editor.

CSS web components can be typed the same as any other web component!

export interface CustomButtonAttributes {
size?: 'sm' | 'md' | 'lg';
variant?: 'primary' | 'secondary' | 'text';
}
declare global {
interface HTMLElementTagNameMap {
'custom-button': CustomButtonAttributes;
}
}

Adding classes

Something that might not be immediately obvious is there’s no need to make a tradeoff between classes or CSS web components. Classes can still be applied as usual to add further modifications to the element.

<custom-button
class="space-bottom"
variant="primary"
size="sm"
rounded
>
<button>Button</button>
</custom-button>

Adding functionality

If we do end up needing to apply some JavaScript-based functionality, a web component can always be registered later without introducing any HTML changes.

class CustomButton extends HTMLElement {}
customElements.define('custom-button', CustomButton);

We haven’t felt the need to apply any further scoping to the styles above what the element name already provides, but if stronger encapsulation is needed then shadow DOM could be enabled.

Usage in frameworks

A pattern typically used whilst working with frameworks like React is to take the component props and use a dependency like classnames to build a list of classes that gets added to an element.

function CustomButton({ variant, size, rounded }) {
return (
<button
className={classNames('custom-button', {
[`custom-button--${variant}`]: variant,
[`custom-button--size-${size}`]: size,
'custom-button--rounded': rounded,
})}
>
Button
</button>
);
}

The JavaScript to build the classes ends up being added to a component which also needs to be created with more JavaScript.

CSS web components avoid all this overhead, the attributes are styled without needing to be mapped. No JavaScript needs to be involved, but they are versatile enough to work with these type of framework components if needed by either wrapping the framework component or being placed inside it.

<custom-button variant="primary" size="sm" rounded>
<FrameworkButton>
Button
</FrameworkButton>
</custom-button>

Give it a go

We’ve been enjoying using this approach for months now and haven’t found any major gotchas to report.

Right now it feels like we’re exploring a new untapped part of the web. But I’m hoping in a year or so when I look back at this article that applying styles through custom elements will have become as fundamental as classes.