Now that web components have gained more traction over the last few years (used by companies like Salesforce, Github, and Adobe), we should explore how web components can offer progressive enhancement to applications without the bloat of JavaScript frameworks.
Definitions
Web components
Aspects of web components have been around since about 2011, making this technology 14 years old–a little bit older than the ever-popular framework, React! Back then, the industry had specs for the template HTML element, the shadow DOM, and the ability to create custom HTML elements. It was Alex Russell who proposed they all be combined into what we now call “web components” (Alex Russell’s original web components proposal talk and slide deck are still available to browse). However, it wasn’t until 2020 that support for all the key features were finally implemented in the major browser engines and web components adoption started in earnest. Web components have been making waves in the developer community since then, and the appeal is understandable. Web components offer engineering teams the opportunity to create component-based applications, without the overhead of JavaScript frameworks and additional libraries. In particular, the concept of using the shadow DOM ensures that markup, styles, and interactivity are locked away in that particular component, reducing accidental bugs.
...we should consider web components as an extension of our favorite markup language, HTML (yep!)
– Mattia Astorino
Progressive Enhancement
Progressive enhancement is a strategy lots of teams use as a way to build in basic functionality that works for all browsers, and for users that may have slow internet connections or have JavaScript disabled. After the app or website works at a basic level for the majority of users, only then do teams layer on more styling, more interactivity, and more advanced JavaScript features. At Sparkbox, we focus first on building resilient web apps with accessible content for as many users as possible, regardless of network quality or device capability. We use HTML to communicate the semantic meaning of web elements, then add CSS for presentation, using fallbacks and @supports
to account for browser support, and finally sprinkle JavaScript on top for an extra dusting of interactive magic.
Opportunities
Web components are appealing because they offer opportunities for progressive enhancement that other frameworks can’t. Since web components are simply custom, reusable HTML elements, developers aren’t tied to any intricacies or idiosyncrasies of frameworks—they leverage the power and support of the browsers directly. This “native” support makes web components really powerful. With a standardized API, more predictable behavior, browser-level performance optimization potential, and the ability to improve a component’s accessibility, engineering teams can create robust and resilient user experiences, even under challenging web conditions like slow networks and disabled JavaScript. What’s even better is that web components can be used within framework and library tooling if a team decides that approach is ultimately what they need.
Browser Support
There’s a great visual in Mattia Astorino’s article on “Making Web Components for Different Contexts” called the Abstraction Layers Triangle. In the inverted triangle, web components are actually at the lowest level of abstraction (the tip of the triangle), alongside vanilla HTML, CSS, and JavaScript. As you travel upwards through the triangle towards the widest part, more web technologies are needed in order to get from a developer’s component file to a browser-understandable HTML element (think build commands and bundles).
Web components are a part of the web standard, so support comes at the browser level. Meaning, the browser can understand web components without the help of frameworks. If a project has no heavy framework runtime, the project size can often be significantly smaller. Smaller bundle sizes typically translate to fast load times and improved performance, resulting in a smoother experience for the users, especially on less capable devices or slower networks.
Because web components don’t require runtime compilation or unused framework abstractions, they can be small, more lightweight than their framework counterparts, and deliver targeted JavaScript functionality.
At a high level, the less code you send to the user, the faster it should be.
– The Case for Web Components, zeroheight
Semantics and Accessibility Improvements
Web components extend the basic HTMLElement
, and teams can customize them in ways to convey their semantic meaning. If JavaScript fails to load on a page, which does happen to anywhere from 1-3% of users, any static content of the web component is still rendered, the meaning of a semantic, custom web component is still conveyed, and users can still use the app. The app may be less interactive and fancy, but usability and understandability are crucial.
Teams can also improve upon elements’ initial accessibility by using lifecycle callbacks to add ARIA attributes. For example, a team may need to add aria-expanded
or aria-controls
attributes that only apply when JS is enabled. If JavaScript failed but those attributes were added anyways, screen readers and other assistive technologies may have indicated that certain elements were interactive when they actually were not. It can be misleading to users if the assistive tech makes it seem like the page is fully functional. Instead, if a web component adds aria-expanded
and aria-controls
when JavaScript is ready and interactions are supported, the ARIA attributes can still be applied, assistive tech can accurately announce the interactivity, and users can make their way through the site as expected.
Framework-agnostic
Web components are framework-agnostic. Developers can design individual web components, as they need them, and then import into any future projects, regardless of framework. Because of this flexibility, and if there’s an existing web component that already does what they need, teams don’t have to write components specific to React, Vue, or whatever framework they’re using. In large organizations, this can be especially useful where there are a variety of projects that may be fully legacy, in the middle of a migration, or using different frameworks. Web components can bridge the gaps between all of these things, and provide consistent experiences where they’re used. Teams can feel confident picking the right tech for their projects, knowing that web components will work in whatever setting they choose.
Structured for Progressive Enhancement
It is possible to write web components in a similar way to framework components, where the element is empty until JavaScript runs, and creates the HTML elements needed to render the component. That’s a brittle approach that works against the principles of progressive enhancement, unfortunately. Web components can be strategically built to work so that all important information is accessible and functional, like form elements, even without JavaScript. If and when JavaScript becomes available, then the existing elements can be enhanced to do things that native HTML can’t do alone. Speaking of forms, consider a form
element that already has the action
and method
attributes. If there’s no JavaScript, when that form is submitted, a full page refresh is triggered. However, by wrapping that form
within a web component that asynchronously submits the form via fetch
, we can “have our cake and eat it, too.” Without JavaScript, the flow still works, but with JS, the user ends up with a slightly better experience and no full page refresh.
Caveats
Although web components can provide a certain level of functionality and progressive enhancement, there are still pitfalls and caveats for those uninitiated. Progressive enhancement strategies can assist teams in mindfully handling some of these caveats.
Encapsulation
Overall, the view in the web community that web components’ concept of “encapsulation” is a big benefit of this technology. “Encapsulation” is where the boundaries for logic and even styles of a particular web component are confined to the web component itself. The purpose of encapsulation is to prevent other styles or JavaScript from accidentally “leaking” into or “getting out” of the web component and resulting in unexpected behaviors. The encapsulation happens with the shadow DOM, and it’s meant to provide reusability to your web components. While encapsulation enhances reusability, it can pose certain challenges, particularly if a team is intending to pierce the shadow DOM and share functionality or styles.
Global styles are typically defined in the light DOM, but those styles may not penetrate a web component’s shadow DOM and be inherited as expected. To progressively enhance an app and maneuver native encapsulation, teams need to ensure that even if a component’s shadow DOM styles don’t load, the basic presentation of content with just global styling still provides readability. The use of slots can expose certain shadow DOM elements to the light DOM. Additionally, the ::part()
pseudo-element may be useful to target attributes of elements that live inside the shadow DOM from the light DOM.
Similarly, JavaScript can’t query for elements that live in the shadow DOM with getElementById
or document.querySelector
. If a team is building with progressive enhancement in mind, this means that any core functionality that relies on direct DOM manipulation within the component would need to have a fallback in the light DOM in case the web component’s JavaScript doesn’t load or initialize. The shadow DOM can complicate a lot of things, but because it is opt-in, teams don’t have to bother with it if they deem it unnecessary.
Productivity and Efficiency
Web components can be reusable and have more standardization than framework components, but they don’t natively provide the same level of state management tooling that modern JavaScript frameworks do. React has its Context API and useState
hook; Vue.js has its Reactivity API and maintains the state management library, Pinia; Angular has dependency injection; web components don’t have a built-in solution for managing state. Most of the state management for a vanilla web component would have to be handled manually. Unfortunately, the same is true for typing and event handling. React, Angular, Vue.js, and Svelte all provide a developer experience that encourages productivity with strong typing and a robust event handling tooling system. These developer experience gaps can make the initial web component development less “productive” or “efficient” than with a full framework.
It’s important to note, however, that the lack of a state management library in vanilla web components also contributes to their smaller bundle size, which is a direct benefit for progressive enhancement’s goal of fast load times and resilient core functionality. While libraries like Lit and Stencil do introduce a runtime and require a build step (which is a common workflow step for modern features like TypeScript), the resulting bundle sizes are still often significantly smaller than those of full application frameworks. By delivering targeted, reactive enhancements with a comparatively lighter JavaScript footprint, engineers can follow the progressive enhancement principle of minimizing the initial load time, and ensuring basic functionality is readily available.
As adoption of web components has started to increase, web component libraries have popped up to offer state management that feels more framework-like. Lit makes use of reactive properties, which can automatically trigger a re-render when the property changes, and has a Context Protocol to share data amongst components. Lit can also handle a web component’s observable state with signals (based on the proposal from TC39 to bring signals into the JavaScript spec). Stencil aims to smooth out the issues with event handling and props. They use their Event()
decorator to automatically set the right web component settings so that the event bubbles out of the shadow DOM, which makes it consumable to a framework, and also automatically parses and type coerces props if necessary using the @Prop
decorator.
Lit and Stencil both have strong support for TypeScript as well, and include converters for common data types, aiding in the parsing and type-coercion of web component data.
Takeaways
Web components are backed by the support of modern browsers, and are a powerful, foundational approach to web development that aligns closely with progressive enhancement principles. Using those ideals as a guide, teams can build resilient, fast web apps by depending on web components’ browser-native standardization. With the principles of progressive enhancement in mind, teams can create reusable, encapsulated UI components, faster-to-load pages, and more resilient applications without the need for heavy JavaScript frameworks and libraries. Building flexible, framework-agnostic projects is easier than ever, with targeted, robust enhancements and minimal overhead, ensuring a functional experience for a wide range of users and devices.