In the previous article, we explored the new features that accompany Svelte 4, including its enhanced handling of Custom Elements. However, before delving further, it is necessary to mention the motivation behind using Custom Elements. Some of you who attended our Winter Tech Event may recall the section of the discussion dedicated to Web Components and the concept of Microfrontends.
For those unfamiliar with it, the idea of Microfrontends is akin to Microservices, from which its name originates. Microfrontends represent the next stage in the evolution of decomposing frontend applications, aiming to break down the frontend monolith. An application employing the microfrontends architecture consists of a macro or container application and multiple micro or component applications. These microfrontend applications achieve this by utilizing the Web Components technology suite, of which the Custom Elements API is a part.
The primary advantage of decoupling the frontend monolith in this manner lies in its scalability across multiple teams. Each team can develop their own smaller, more cohesive, and often more maintainable codebase, in comparison to the monolith. Additionally, each team has the flexibility to develop their micro application using their framework of choice. They can deploy, update, or replace parts of their micro application incrementally without disrupting the work of other teams or the macro application.
However, this approach is not without its challenges. The most significant issue associated with this approach is what some refer to as “microfrontend anarchy.” This term describes the state of an application that utilizes multiple frontend frameworks, which, although technically possible, is likely suboptimal. It can result in unnecessary bundle size, codebase inconsistencies, and lack of cohesion. This problem can be avoided with proper planning and a thoughtful approach. However, failure to consider these aspects from the beginning can doom the project.
The role of Custom Elements in microfrontends is actually quite straightforward. Custom Elements allow us to encapsulate certain functionality within a single HTML element. In general, frameworks handle the Web API aspect of the solution for us. For instance, Svelte’s Custom Element API serves as an abstraction layer on top of the Custom Elements API within the Web Components suite. This abstraction enables us to generate Custom Elements from Svelte components and/or applications without the need for directly using lower-level abstractions or dealing with the CustomElementRegistry for micro application creation, as the Svelte compiler takes care of that for us.
Using Custom Elements generated via the Svelte compiler is as simple as importing the output file, which is an ES module. The underlying functionality is also straightforward. Svelte generates a class object that defines the behavior of the element, containing all the inner workings like any other Svelte component. It registers this class object on the CustomElementRegistry so that it can be used by the consumer application. Here’s an example:
<!-- in Svelte 4--> <svelte:options customElement="my-element" /> <!-- in Svelte 3--> <svelte:options tag="my-element" /> <script> export let name = 'world'; </script> <h1>Hello {name}!</h1> <slot />
This mocked component would be compiled to:
import {
SvelteComponent,
create_custom_element
// ... other imports
} from 'svelte/internal';
// ... Svelte component compiled output
class App extends SvelteComponent {
// ...
}
customElements.define('my-element', create_custom_element(App, {}, [], [], true));
It can then be used as follows:
<my-element>
<p>Nice!</p>
</my-element>
Svelte also provides developers with the ability to customize various compilation options for its Custom Elements. For example, the “shadow” property controls whether a shadow root will be created.
For instance, the shadow property controls whether the shadow root will be created to encapsulate the Custom Element. This means that the styles will be inherited from the consuming application, but it comes at the cost of losing the ability to use slots. The second option is the props object, which determines how the Custom Element props are updated, reflected to the DOM, and the type to which they are converted when reflected.
While the usage of Custom Elements is a useful way to package Svelte components for consumption by non-Svelte apps, it does have its limitations. Specifically, styles are encapsulated by default, meaning that global CSS won’t affect the Custom element unless `shadow: “none”` is used. Additionally, the styles are inlined into the component as JavaScript strings. Slots function differently compared to Svelte, as they load differently, and the Svelte `let:` directive has no effect since Custom Elements cannot pass data to the parent component filling the slot. Moreover, they are not suitable for server-side rendering due to the presence of the shadow DOM.
Based on our experience, developing Svelte components to be used as Custom Elements is an excellent choice for decoupling parts of the application that can be self-contained, reused, and focused on by smaller teams. SvelteKit serves as an ideal consumer application, particularly when utilizing lazy-loaded Custom Elements. This approach helps reduce the amount of code shipped to the client via the Application server compared to using a CDN or an object storage service like AWS S3. While there is potential for an extensive library of books to be written in the next 10 years on perfecting the methodology and selecting the best technology for different types of applications, we are confident in betting on Svelte and SvelteKit as the current leading option in this field.
Josip Ante Cindrić

Software Engineer who joined our team because it seemed like a great opportunity to develop great things while growing as a person in a healthy environment. He loves dogs and board games.