Web Component Workarounds Solutions to web component platform constraints Guide

Web Component Workarounds

Semantic UI components are designed to make it easier to work with web components and to provide solutions for some of the sore points of the standard.

However when working with web components it is still possible to run into some constraints that require using a a workaround. These include things like handling empty slots, using some new css pseudo selectors like :slotted

CSS

Cant Use :host-context

Problem

Chrome supports :host-context() which solves context-aware styling entirely, but unfortunately it is not supported by other browsers because browser makers believe this breaks the encapsulation of web components.

component.css
/* this will never work in all browsers */
:host-content(.sidebar) {
background-color: var(--blue-5);
}

Solution

Use style queries for responsive styling by setting CSS variables in context, then using them internally:

style.css
/* Parent context sets the variable based on screen size */
@media (max-width: 768px) {
.container {
--stackable: true;
}
}
component.css
/* Component uses style queries to detect the variable */
@container component style(--stackable: true) {
.component {
display: flex;
flex-direction: column;
}
}

the css framework also provides a --dark-mode token you can use for styling dark mode with style queries

component.css
@container style(--dark-mode: true) {
.card {
--card-link-hover-background: var(--standard-10);
}
}

Note: Style queries may eventually make context-aware styling easier but they are only currently supported in Chrome. Firefox should have it implemented within the next year.

Cant Combine Pseudo Selectors on Host

Problem

You might want to use something like :host:first-child to remove margins on a web component if it’s at the start of a block; however the :host pseudo selector does not work with other pseudo selectors like child selectors.

component.css
/* this doesnt work */
:host:first-child {
margin-top: 0;
}

This is annoying because you often want to expose margins on the host element so it’s easy for a person upstream to change spacing, and it helps to default to reasonable standards like not adding space before the first item in a group.

Solution

Use pageCSS to style from outside with tag names:

my-component-page.css
my-component:first-child {
margin-left: 0;
border-top-left-radius: var(--component-border-radius);
}
my-component:last-child {
margin-right: 0;
border-top-right-radius: var(--component-border-radius);
}

CSS Resets Don’t Pierce Shadow DOM

Problem

CSS resets cannot pierce shadow DOM boundaries.

Solution

You will need to apply any resets like a box-sizing reset inside your component css for it to apply to the shadow dom content.

component.css
:host {
box-sizing: border-box;
}
*, *::before, *::after {
box-sizing: inherit;
}

Events

Custom Events Don’t Bubble by Default

Problem

When creating web components its common to want to dispatch events from your component. However if you naively use new CustomEvent() this will not bubble by default, unlike most native events like click or mouseenter.

If a component is waiting for a bubbled event, like those that use event delegation to attach events they will never ‘see’ the event being fired.

Solution

The dispatchEvent() utility automatically includes { bubbles: true, composed: true }.

component.js
const createComponent = ({ dispatchEvent }) => ({
notifyChange(value) {
dispatchEvent('valueChanged', { value });
}
});

HTML Attributes & Properties

Boolean Attributes Cannot Be False

Problem

Boolean attributes set to the string “false” or empty string are still considered truthy. This can be confusing if you want to naively do something like checked="false" or checked="{isChecked}".

Solution

Templates provide a special syntax for boolean expressions, without quotes. This means the attribute should be omitted entirely if the returned value is falsey.

component.html
<input type="checkbox" checked={isChecked} />

Results:

  • isChecked === true<input checked />
  • isChecked === false<input /> (attribute omitted)

Attribute Data Must Be Serialized

Problem

Arrays and objects passed into attribute need to be serialized using something like JSON.stringify to be read by your component.

Solution

Templates handle serialization automatically:

component.html
<ui-menu items={menuItems} />

You can also use settings for cases where you’re using pure HTML or the value is available in JavaScript:

script.js
const items = [
{ text: 'Home', url: '/' },
{ text: 'About', url: '/about' },
{ text: 'Contact', url: '/contact' }
];
$('ui-menu').settings({ items: items });

Functions Cannot Be Passed Pre-DOM

Problem

You can pass functions to web components by setting properties on the DOM element, but this isn’t possible when the component renders on the server. You can’t use functions to generate content unless you give up SSR.

Solution

Use settings and initialize from Query for imperative configuration after the component is in the page:

script.js
import { $ } from '@semantic-ui/query';
$('ui-dropdown').settings({
onChange: (value) => console.log(value)
});

Note: You can use this in an inline script tag directly after the component.

Alternatively, use settings in parent components:

parent-component.js
defineComponent({
// ... component definition
onRendered() {
$('ui-dropdown').settings({
onChange: (value) => this.handleDropdownChange(value)
});
}
});

Build Tools

ES Modules Cannot Import HTML/CSS

Problem

If you want your CSS or HTML to not be in the same file as your web component you might consider using something like import ButtonCSS from './css/button-shadow.css'. However this does not work natively with ES modules, you will have to use a build tool.

You can use fetch() to grab the contents of text and CSS or special workarounds like ?raw flag in Vite, or other build tools. There are some proposals on how to do this with assertions but this isn’t fleshed out yet.

See: wicg/webcomponents/870

Solution

Use getText() for runtime file loading:

component.js
import { getText } from '@semantic-ui/component';
const template = await getText('./component.html');
const css = await getText('./component.css');
defineComponent({
tagName: 'my-component',
template,
css
});

Slotted Content

Slotted Parts Cannot Be Styled

Problem

Cannot combine ::slotted() and ::part() selectors to style parts of slotted web components.

Solution

Use first-party components with consistent APIs:

page.html
<ui-card>
<ui-button primary>Action</ui-button>
<ui-icon name="star" large />
</ui-card>

Override CSS variables for slotted content:

component.css
::slotted(ui-button) {
--button-margin: var(--card-button-margin);
}
::slotted(ui-icon) {
--icon-color: var(--card-icon-color);
}

Use subtemplates for shared shadow root styling:

parent-component.html
<div class="container">
{> childPart data=itemData}
</div>

Empty Slots Cannot Be Styled

Problem

You may want to include slots that may not always be filled in your web component. However, if these slots have styling they will always be present because :blank pseudoselector does not work with slots.

See wicg/webcomponents/936 and w3c/csswg-drafts/6867

Solution

Use settings instead of slots for conditional rendering:

See: Settings vs Slots for guidance on when to use each approach.

component.js
const defaultSettings = {
items: []
};
component.html
{#if hasAny items}
<div class="menu">
{#each items}
<menu-item>{text}</menu-item>
{/each}
</div>
{#else}
<div class="empty-state">No items available</div>
{/if}

Slots Unavailable During SSR

Problem

Server-side rendering cannot know what content will be slotted. You cannot use {#if slottedContent} in templates because slotted content is not available during the server rendering phase.

This occurs because SSR renders components in isolation using their shadow DOM template, but slotted content comes from the light DOM where the component is used. The server has no knowledge of this context until the component is hydrated on the client.

See: Settings vs Slots for the complete decision framework and wicg/webcomponents/936 for related specification discussions.

Solution

Use settings instead of slots when it’s essential for content to vary depending on what is slotted.

Use Settings When:

  • Content needs to drive conditional rendering
  • SSR compatibility is required
  • Component needs to respond to empty states

Use Slots When:

  • Flexible HTML content is needed
  • Client-side rendering is acceptable
  • SEO benefits of slotted content are important
Previous
Advanced Usage
Next
Server Side Rendering