Learn how to build a JavaScript UI component-first dev tool startup & understand why many move away from web components to framework-specific components.
Vincent
Created: July 6, 2024
Updated: September 10, 2024
Our mission is to make the Internet a safer place, and the new login standard passkeys provides a superior solution to achieve that. That's why we want to help you understand passkeys and its characteristics better.
3.1 Custom Elements
3.2 Shadow DOM
3.3 How to Build a Web Component
3.3.1 How to Build a Web Component with Vanilla JavaScript
3.3.1 How to Build a Web Component with Lit
4.1 Usable in Most JavaScript Frameworks
4.3 Progressively Enhance Existing HTML
5.1 Lack of Framework-Specific Features
5.2 Web Components have a Marketing Problem
5.3 Web Components and Server-Side Rendering (SSR) is a Pain
5.3.1 How React Code Works in Next.js to Support SSR
5.3.2 The Hydration Challenge with Web Components
5.4 How to Fix SSR Support for Web Components
5.4.1 What is Declarative Shadow DOM?
5.4.2 Imperative Shadow DOM
5.4.3 Declarative Shadow DOM
5.4.4 Advantages of Declarative Shadow DOM
5.4.5 Disadvantages of Declarative Shadow DOM
6.1 Web Components
6.3 React Component and Web Component in Parallel
6.4 Vanilla JavaScript SDK for Multiple Frameworks
6.4.1 The New Overall Architecture
6.4.2 Advantages of Corbado's Component Strategy
6.4.3 Disadvantages of Corbado's Component Strategy
6.4.4 Our Current State of Transition from Web Components to UI Components
I’ve been working on Corbado, a component-first developer tool startup, to provide developers with fast and easy-to-implement passkeys for user authentication. Our technical approach is based on JavaScript UI components that we continually refine to meet the evolving needs of developers and optimize the developer experience (DX).
Recently, we've made a significant shift from using web components to framework-specific components (we call them UI components or web-js components). If I started all over again, I would immediately build framework-specific components instead of using web components.
In this blog post, I want to share our learnings while working with web components, provide the reasons behind the transition away from web components and explain why many other JavaScript UI component-first startups and developers follow this path.
This blog post should help everyone who is thinking of building a JavaScript UI component-first, developer tool startup determine the right tech stack and benefit from our learnings. Specifically, we try to answer the following questions:
We’ll start by defining certain use cases and scenarios before taking a deeper look at web components (especially their drawbacks) until we showcase potential strategies to use components. Let’s dive in!
Recent Articles
⚙️
Why Passkey Implementation is 100x Harder Than You Think – Misconceptions, Pitfalls and Unknown Unknowns
🔑
Web Components are the New APIs
⚙️
The 30 Best Passkey Tutorials for 14 Languages / Frameworks
⚙️
How to Build a Vanilla JavaScript Passkey Login
⚙️
Passkey Tutorial: How to Implement Passkeys in Web Apps
Before going into the technical details, let’s have a look at different business use cases for using a component-first strategy. In general, we assume that following a component-first strategy (in contrast to an API-first strategy) is preferred if you want to ship a frontend / UI layer of your devtool. This means that APIs are encapsulated in your component, business logic is built-in and UX / UI flows are also part of your product. Typical use cases for using component-first products can include:
Besides these use cases, we define three common company scenarios that we want to use in our analysis. In section 7, you’ll find specific recommendations on which strategy to follow for these scenarios.
Scenario 1 | Scenario 2 | Scenario 3 | |
---|---|---|---|
Name | Regular Startup | DevTool Startup | Enterprise |
Description | - Startup that builds a digital product - Wants to use an external solution for non-core features - Is the customer of a developer tool startup | - Startup that builds components - Components are sold to external customers | - Larger company with many departments - Builds components for internal use across different departments - Internal colleagues are the “customers” of the components |
Main goals | - Save time & outsource stuff - Use something pre-built from experts - Focus on core features | - Provide great developer experience - Support many frameworks - Decrease complexity for maintaining components | - Re-use components - Have one maintainer and multiple applications - Use component in different frameworks in different dedicated teams |
Technical requirements | - Use SSR for SEO purposes and get technology benefit - Use component in one JavaScript framework | - Offer component in all major JavaScript frameworks | - SSR is nice-to-have - Use components in own design system |
We know that we’re probably deep in the UI JavaScript component-first devtool startup bubble and not all readers of this blog post have an idea what companies are actually behind that. Therefore, we provide an exemplary list of companies that we assess as being a JavaScript UI component-first developer tool company:
After we’ve presented the different use cases, scenarios and sample developer tool companies in the component-first universe, let’s go back to the tech part. We start with web components and answer the question why many move away from them. To better understand the delusion about web components and their abundance, we need to understand their characteristics first.
Subscribe to our Passkeys Substack for the latest news, insights and strategies.
SubscribeWeb components are framework-agnostic components that are built on browser APIs. They can bundle a lot of intelligence behind a couple of lines of code, so that other developers can quickly reuse them. You can basically implement custom HTML tags and use them like <my-component name=”component1></my-component>
besides regular HTML tags like <b>
.
A great collection of useful resources around web components can be found here or here.
Let’s have a look at the inner blocks of web components and two core concepts: custom elements and shadow DOM.
Custom Elements enable you to attach a JavaScript class to an HTML element on your page. This allows you to add behaviors and JavaScript-generated content to any element instances, automatically initializing them for the full lifecycle of the page, whether they are server-rendered, JavaScript-generated, or injected via fetch()
.
However, custom elements cannot be void elements (like <img>
or <meta>
). They must have both a start and an end tag. Additionally, custom elements must include a dash in their tag name to avoid conflicts with future web platform additions.
The shadow DOM is a web standard that encapsulates a section of the DOM and its styles, providing isolation from the main document's DOM and CSS to avoid conflicts. It enables the creation of reusable, self-contained web components by ensuring that styles and scripts do not interfere with the rest of the document.
It addresses the repetitive authoring of markup at the cost of client-side rendering. This trade-off introduces complexities such as managing layout shifts and the Flash Of Unstyled Content (FOUC). Common solutions to these issues often involve putting all JavaScript into the critical rendering path (in the <head>
) or hiding components until they are defined via JavaScript – both of which can impact performance for critical content.
Taken from https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM
Building web components can be accomplished using various frameworks and libraries, each offering unique benefits. Popular options include Vanilla JavaScript, Lit, Stencil, and frameworks like Angular and Vue. Here’s a brief overview of how to create a basic web component using Vanilla JavaScript and Lit.
Vanilla JavaScript provides a straightforward approach to creating web components. Here’s a simple example:
The web component can then be used in the HTML like:
In this example, we have defined a custom element my-element
with a shadow DOM that contains a simple paragraph (“Hello, Web Component!”).
Lit is a library that simplifies the creation of web components with declarative templates and reactive properties. Here’s how you can create a similar component using Lit:
In this Lit example, we define a custom element my-lit-element
with scoped styles and a template that renders a styled paragraph.
Other Frameworks to Create Web Components:
Using these frameworks and libraries, you can build powerful, reusable web components. Each approach offers different levels of abstraction and features, allowing you to choose the best tool for your project's specific needs.
At this point, we don’t want to go into more detail about building web components, as there’s already a plethora of good content available. To read more about web components in general, I recommend having a look at Zach Leatherman’s blog. Also, this list of some prominent web component proponents is a nice overview to see who’s using web components.
Become part of our Passkeys Community for updates and support.
JoinOften hailed as the holy grail for web development, web components have been described as a potential replacement for all JavaScript frameworks (I would be interested in what the React, Vue.js or Next.js maintainers actually think of those statements).
Let's explore the key advantages that make web components so appealing to some.
One of the most significant benefits of web components is their framework-agnostic nature. They can seamlessly integrate with various JavaScript frameworks, making them highly versatile. Initially described as "a browser-native alternative to React," web components have gained widespread support across the modern web ecosystem. For a detailed overview of their compatibility with different frameworks, you can check out this website. It's worth noting that React scores poorly on this compatibility test (for a more future-friendly and compatible experience, consider using Preact).
Web components enjoy wide browser support, making them a reliable choice for cross-browser compatibility. Modern browsers, including Chrome, Firefox, Safari, and Edge, all support web components (basically since the end of life of Internet Explorer in June 2022, they have gained universal browser support). This broad compatibility simplifies development and reduces the need for polyfills or workarounds.
Where web components truly shine is in their ability to progressively enhance existing HTML. They serve as an excellent mechanism for adding interactivity to your HTML without requiring a complete rewrite. Here are some practical examples of how web components can enhance your forms and content:
In this example, we use custom elements to add AJAX functionality to a form (custom element called <ajax-form>
) and a show/hide toggle to a password field (custom element called <show-password>
). Web components allow you to generate and inject HTML, respond to user interactions, and encapsulate functionality in a convenient wrapper.
With web components, there's no need to instantiate libraries or pass selectors for target elements. They handle multiple elements and their behavior effortlessly, providing a clean and efficient way to enhance your web pages. This convenience makes web components an attractive choice for developers looking to streamline their workflow and deliver rich, interactive experiences.
Despite all the advantages that web components promise, many developer-first startups, that focus on building JavaScript UI components, ditch web components in favor of framework-specific components.
Let’s try to understand why (for more detailed insights about the issues of web components we also recommend Carlana Johnson’s and Dave Rupert’s blog).
While web components offer substantial flexibility, they lack many of the framework-specific features that developers have come to rely on.
Here’s a list of some framework-specific characteristics and features that web components lack:
In summary, while web components work with the fabric of the web and offer significant flexibility, their lack of framework-specific features and less seamless integration can result in a less favorable developer experience. For those deeply embedded in framework ecosystems, the transition to web components might involve significant trade-offs.
The marketing journey of web components has been rocky according to Dave Rupert, primarily due to pushy advocacy and early confusion between web components (the specifications) and Polymer (the Google UI framework). Polymer's early years were fraught with issues: it was positioned as a competitor to other UI frameworks, including Google's own Angular and faced setbacks like Mozilla's removal of HTML imports. These factors contributed to a shaky foundation that hindered widespread adoption.
Despite these real adoption concerns, some Google advocates took to Twitter with an aggressive stance, dismissing React as inferior and slow while suggesting that anyone using it was making a poor choice. This confrontational strategy did not help in winning over developers. Simultaneously, Google's AMP project, which also utilized web components, pushed a similar narrative, presenting itself as a necessary remedy for the bloated mobile web. This approach further alienated many in the web development community.
However, much has changed over the years. The introduction of Lit has addressed many of the issues and confusion that plagued Polymer, and web components are no longer seen as just "a Google thing." Today, there are numerous non-Google web component libraries that are gaining traction. Still, there are many misconceptions around web components.
One of the most significant challenges with web components is their lack of support for Server-Side Rendering (SSR) for quite a long time. SSR can greatly enhance the performance of web applications, particularly for initial page loads, by rendering HTML on the server before sending it to the client. This approach is especially advantageous for SEO and improves the UX, particularly for those on slower connections. However, achieving SSR with web components is notoriously difficult.
Web components rely heavily on browser-specific DOM APIs, such as customElements.define()
, HTMLElement
or template.content.cloneNode()
, which are unavailable on the server. This presents a major hurdle when trying to integrate web components with SSR frameworks like Next.js.
Many developers integrate web components in server-side code and expect that things are working out-of-the-box. However, this is not the case (as we see below). Moreover, many JavaScript frameworks offer the possibility to define certain components or pages to be client-side-rendered while the rest of the website is server-side rendered. However, often this causes a lot of confusion and is for unexperienced developers complex to implement.
To grasp the full extent of web components and SSR, it's essential to understand the typical SSR workflow (see example for React and Next.js below) and how this workflow diverges when web components are involved.
React is a client-side rendered framework. Next.js, which builds upon React, supports SSR. If you are using SSR components in a React/Next.js application, the flow looks as follows:
ReactDOMServer.renderToString(<App/>)
generates a string of HTML for the entire React component tree.ReactDOM.hydrate(<App/>, document.getElementById('root'))
connects the React components to the server-rendered HTML, effectively "hydrating" the static content to become fully interactive.This all results in the following benefits:
You have seen the process for SSR with framework-specific components above. Web components have not been supporting SSR natively for a long time and caused the following challenge:
During hydration, JavaScript code that registers web components is executed. Typically, this code is included in the application’s JavaScript bundle via imports. As a result, there is a period between the initial render of the SSR HTML and the completion of the hydration process where web components might not display the correct content. This leads to a flash of unstyled content (FOUC), where the web page initially appears unstyled or incorrectly styled until the JavaScript fully loads and executes.
This delay can cause layout shifts and visual inconsistencies, negatively impacting user experience. While theoretically, you could try to match the final rendered output with placeholder markup, this is highly impractical, especially for complex or third-party web components.
Nevertheless, recently, there are some developments on the way that try to fix SSR support for web components.
One motion to fix SSR support for web components is through the efforts of the different underlying frameworks, each attempting to add SSR support in various ways. However, many of these initiatives are not yet stable. For instance, Lit’s SSR capabilities are still part of "Lit Labs," highlighting the experimental nature of these solutions.
A robust, standardized solution for SSR in web components will likely require the ability to render all web components on the server allowing them to compose and hydrate seamlessly, independently of the framework used to create them. This goal is still a few years away, indicating that significant advancements are needed before SSR for web components can be considered fully resolved.
One promising approach to achieving this goal is Declarative Shadow DOM (DSD) which allows for SSR across frameworks. However, DSD presents several challenges and many existing web components have to be rewritten to use DSD and support SSR.
To better understand the potential and challenges of Declarative Shadow DOM, let’s explore it in more detail.
Declarative Shadow DOM is a web specification that enables server-side rendering (SSR) for custom elements by allowing developers to define shadow roots directly in HTML. This approach simplifies the use of shadow DOM in web components.
Traditionally, creating custom elements required defining a class and constructing a shadow root within the constructor which resulted in the shadow DOM. Before Declarative Shadow DOM, a major SSR challenge was the need for client-side JavaScript to attach the shadow DOM, which couldn’t be executed server-side. Servers would return a pre-rendered version of the web component, complete with classes and layouts for the client-side JavaScript to replace with the actual shadow DOM during hydration. For example, Stencil uses this method to support SSR for web components.
The general support for DSD is already pretty high among browsers.
Let’s better understand the difference between imperative (the old and existing way) and declarative shadow DOM.
With imperative shadow DOM, for instance, a custom element named AppCard
would be imperatively constructed as follows:
In this example, the AppCard
custom element creates its shadow root in the constructor, providing encapsulation for its styles and structure.
Interactivity within the web component is still given even though the template is reduced to a string. The web component would be imperatively constructed for the client-side, but since the shadow root is already instantiated, you don’t need to instantiate the template.
With declarative shadow DOM, you can define the same template for the custom element in a more declarative manner. Here is the same shadow root definition using declarative shadow DOM:
In this DSD approach, the shadowroot
attribute in the HTML template is recognized by the HTML parser and applied as the shadow root of its parent element (<app-card>
). The template slots allow dynamic content injection into the custom element template. Any HTML outside of the template is considered "Light DOM" and can be projected into the "Shadow DOM" via slots.
Let’s briefly summarize the advantages and disadvantages of DSD.
Apparently, web components have some drawbacks (as described above). That’s why we tried to conduct more holistic research and find alternative ways to follow a component-first strategy. While doing so, we found different approaches which can be summarized into the following categories.
The first category are classical web components that we have already described in detail above. The classical web components would be written in one framework, e.g. Vue.js or Lit, and could be used in most other JavaScript frameworks.
Pros:
Cons:
The second category of components use React as foundation (as React is the most popular JavaScript framework according to many surveys and usage data). This means you would write React components (so no web components) and use them as follow:
<div>
, which will be used as the component. For these other frameworks, SSR would not be supported.Pros:
Cons:
The third category was a mixture of both worlds. Here, you would build a React component and besides a separate web component for all other frameworks (here are free to decide for an underlying framework e.g. Vue.js or Lit).
For React, SSR would be supported. For all other frameworks, that are using the web component, SSR and other framework-specific features would not be supported necessarily.
Pros:
Cons:
The fourth category is the one that we decided on. To understand why we came up with this, you need to understand where we came from.
Initially, we decided to go for the web component category, as we thought that there must be a smooth way to get SSR working for our existing web that we were already using (written in Vue.js). So, we thought there shouldn’t be too much of rework to be done (just some minor adjustments should do the job).
This approach would allow us to stay framework-agnostic and we had the following options:
<div>
<div>
(however SSR would still only work then for React-based frameworks).While evaluating the approaches for the new component strategy, it became evident that all the five options were not sufficient. So, we shifted our strategy towards a new approach.
The strategy we ended up with is an extension of option 5.
General
Our components are usable for both JavaScript and TypeScript developers. We employ a mono repository approach, consolidating all frontend TypeScript code into a single, open-source GitHub repository. This repository houses multiple packages, each of which can be independently published on npm.
We leverage OpenAPI code generation to streamline the creation of REST clients and types for both our Frontend API and Backend API. Our testing framework primarily utilizes Playwright, complemented by unit tests in Jest and Mocha. For bundling, we use webpack, with a future consideration to transition to turbopack as it matures. Additionally, our setup ensures compatibility with IE11 and supports both ES5 and ES6 standards.
@corbado/web-core
Our component architecture is built around a Vanilla JavaScript SDK (@corbado/web-core) that encapsulates business logic and data. This Vanilla JavaScript SDK does not include any UI components, allowing it to serve as a centralized repository for our business logic, thus eliminating the need to maintain this logic in multiple places. Its versatility allows it to be used not only within pre-built framework-specific components by Corbado, but also to create custom logic, build other framework-specific components, or apply unique UIs on top of it. @corbado/web-core is an internal package and does not need to be installed separately.
@corbado/react
As the majority of our customers are from the React / Next.js / Vercel ecosystem, we decided to focus on React and build a dedicated React component (@corbado/react). This React component consumes the Vanilla JavaScript SDK, handles the UI and leverages React-specific features, such as session management through providers.
@corbado/react-sdk
To allow for more custom implementations, we also built a React SDK (@corbado/react-sdk) which is tailored for React applications that require more control over authentication flows without prebuilt UI components.
@corbado/web-js
To support other frameworks as well, the React component (@corbado/react) is rendered into a <div>
, which can then be integrated in other JavaScript frameworks that we do not yet support with dedicated components (yet). These components are called web-js components (@corbado/web-js).
@corbado/types
To provide TypeScript support, this package (@corbado/types) holds the TypeScript type declarations for all Corbado packages and is automatically included when installing the other packages.
@corbado/shared-ui
This package (@corbado/shared-ui) contains shared files for all UI components (so currently for @corbado/react and @corbado/web-js). It’s automatically installed when any of these packages is installed.
We spent much time on coming up with this component strategy and decided to take this route based on the following advantages:
So far, we have fully replaced web components with the new component structure described above. This means that there is currently a React component and a web-js / UI component for all other frameworks. Other framework-specific components will follow soon. In one of our next major releases, we will add SSR capabilities to our React component and therefore provide a dedicated @corbado/nextjs package. Then, also @corbado/vuejs, @corbado/angular, @corbado/svelte, @corbado/nuxtjs are planned.
As we want to help other developers and developer tool companies in making use our learnings and research, we try to provide specific recommendations on which component strategy to follow based on different scenarios.
Scenario 1 | Scenario 2 | Scenario 3 | |
---|---|---|---|
Name | Regular Startup | DevTool Startup | Enterprise |
Recommended Component-First Strategy | - Use framework-specific components in the framework that you’re already using - Don’t try to reinvent the wheel by coming up with own component-strategies | - Build a Vanilla JavaScript SDK that holds the business logic and data - Use this base repository to build many framework-specific components without the need to maintain too many code bases | - Use web components (if support of SSR is not a hard requirement) |
Benefit | Best performance and DX in a framework that you already know | Wide support of frameworks; SSR for the most important frameworks while having as little code bases as possible | Only one code base to maintain |
Building a JavaScript UI component-first developer tool startup requires careful consideration of the right tech stack. From our experience with Corbado, transitioning from web components to framework-specific components has proven beneficial.
Framework-specific components offer better integration, performance, and developer experience, especially with critical features like SSR. By centralizing business logic in a Vanilla JavaScript SDK and leveraging framework-specific UI components, you can achieve broad compatibility and maintainable code. This approach ensures flexibility, performance, and the ability to quickly adapt to evolving developer needs.
Table of Contents
Enjoyed this read?
🤝 Join our Passkeys Community
Share passkeys implementation tips and get support to free the world from passwords.
🚀 Subscribe to Substack
Get the latest news, strategies, and insights about passkeys sent straight to your inbox.
We provide UI components, SDKs and guides to help you add passkeys to your app in <1 hour
Start for free