Creating a Custom Web Component Library
A real-life roadmap for building a design system — Pt II
In case you missed it, be sure to check out Part I: Translating design principles into scalable code. Here we’ll take a look at how we built a custom web component library that powers our content management application and supports React, Angular, and framework-free projects.
The Challenge 🍝
The JW Player enterprise dashboard is a robust product that many of our users rely on for their daily workflows. It provides a GUI for our APIs, allows customers to manage their tools and content throughout every stage of our platform — video players, ad schedules, live stream events, etc — it operates conditionally based on user entitlements, and handles sensitive billing and account data.
UI execution for all these complex workflows hasn’t always been lean. Inputs, buttons, checkboxes, toggles, loading spinners, tables, modals… nearly everything has been rewritten, restyled, and functionally wired-up from scratch.
Engineers dreaded UI-heavy tickets and focused more on backend work, it was common to find visual bugs in production, and our QA team’s job got incrementally harder.
Our design and front-end contributors determined that the best way to tackle our growing nightmare of spaghetti-UI was to implement a library of reusable components.
The Solution 💫
We built ours around these golden rules:
- A UI component is always product-agnostic
- A UI component is always framework-agnostic
- A UI component is reusable
Our enterprise dashboard runs on Angular and a range of other projects are built in React, so our first goal became creating a system that could support both. The first iteration began with Storybook.
A Tale of 2 Storybooks 📕
Storybook is a platform that allows you to develop components in isolated environments without the need for a complex dev stack, database connections, or the need to dig through browser dev tools to force an element into a specific state.
A component can be developed, debugged, tested, and documented with a range of “stories” that demonstrate its functionality and limitations. This format is very helpful for demoing interaction design and things like
Users can then use the Storybook interface to toggle props on/off and enter test data with “knobs”. There’s also an “actions” tab that logs events (click, focus, etc.), and a
README.md for detailing usage guidelines.
The heaviest burden of this first iteration became the requirement to develop and maintain two concurrent builds of Storybook: one for Angular and one for React. The React side experienced some neglect as time-sensitive Angular dashboard tickets took precedence, and it was a challenge to keep releases coordinated.
As the number of components grew, it became difficult to categorize them in a clear and searchable way. Keeping them hidden behind collapsed categories meant that engineers often couldn’t find what they needed.
We went back to the drawing board to assess how we could support all projects — with better docs — and without the duplicated effort and cognitive load of developing in more than one framework.
The Case for Web Components 🕸
We were 2/3 on our golden rules — we had achieved reusable and product-agnostic — but to be truly framework agnostic, we also had to support the projects that use no framework at all. Web components to the rescue!
Web components allow developers to write more semantic (and therefore more accessible) HTML in a reusable way that is easy to maintain and compatible across all browsers. They’re ideal for UI components that can be used in framework-free environments.
Building with Stencil
Our research on web component implementation led us to Stencil, a system that combines the best concepts of the most popular front-end frameworks into a compile-time rather than runtime tool. It takes TypeScript, JSX, a tiny virtual DOM layer, efficient one-way data binding, an asynchronous rendering pipeline (similar to React Fiber), and lazy-loading out of the box, and it generates 100% standards-based web components that run in any browser supporting the Custom Elements v1 spec.
Bindings make it possible to build your component once as a web-component, then Stencil wraps it appropriately and emits libraries in the framework of your choice (in our case, Angular & React). Then you can build and deploy it as necessary for different use-cases.
Using Stencil to build our new Web UI Components (WUI) removed the need to develop for multiple frameworks and also gave us the flexibility to support projects written in vanilla JS.
Using WUI 🔧
Like Hook, WUI is consumable by CDN or our internal npm registry, so it’s easily accessible for all teams in our organization.
Using WUI in a vanilla project is as easy as adding the following CDN-hosted scripts to your document:
<script type="module" src="https://wui.jwplayer.com/v/[version]/dist/wui/wui.esm.js"></script> <script nomodule src="https://wui.jwplayer.com/v/[version]/dist/wui/wui.js"></script>
…and then referencing the components in HTML:
<wui-button type="tertiary" label="Play" icon="play" />
Alternatively, a React developer would pull in this package via npm:
Here’s where Typescript shines. It helps prevent type coercion and bugs on the component-development side, but it also provides excellent autocomplete support in IDEs like VS Code on the UI-development side.
Strictly typing our components means that their names and props are always consistent and enforced, and therefore less prone to breakage. It also reduces strain on developers who use WUI by removing the need to memorize props and their potential values, meaning the components are rarely misused.
Here’s an example of Typescript autocomplete in action for a play icon component in a React context:
Creating an Intuitive API 🧠
WUI is built by engineers, for engineers. To be intuitive, consistent, and performant, its API must stay as close to the existing DOM API as possible. For example,
wui-check-input accepts the props
checked—both valid HTML attributes—which can be passed directly to our inner
input element without extra configuration or translation.
This concept is also useful when handling events; we don’t need to add custom
@Event properties to our components when there’s already an HTML event that we can rely on. A
wui-input is constructed by wrapping a standard HTML
input element and adding our styles and markup around it. Then, when a user types in the
input element, it emits the
change events. Because these events bubble up, we don't need to account for them separately.
Custom Props & Children
To discourage developers from passing incompatible child elements to any given component, we restrict the contents with strictly-typed props and construct the resulting HTML accordingly. Here’s an example of how we use a
string label prop to guard against rogue child elements and remove the guesswork of populating a
For components that require children that can’t be strictly typed, we can still ensure that the contents end up in the right place with the right styles by placing them in a
<slot></slot>, seen here with a
wui-modal body & footer:
Now our new web components and their updated APIs needed a home for their documentation. Our front-end lead, Pieter Janssen, took WUI to the next level with an imaginative take on a custom docs site called Atlas.
Introducing Atlas 🗺
Atlas became everything we couldn’t pull off in Storybook at the time: a single, highly dynamic hub that showcases all of our available components more visually, with interactive documentation and changelogs for each release. It’s built with React & SCSS and is tested with Jest and Puppeteer.
Running Stencil’s component server alongside the Atlas server results in a super-fast, hot-reloading environment where we can develop components and Atlas UI (documentation) in one fell swoop.
The browse page alphabetically lists dynamic (functional!) previews of each component. Because these aren’t static images, they can’t get stale — ensuring that they’ll always reflect the accurate design of each component.
The dynamic preview panes and real-time code samples are made possible by our use of MDX (markdown that renders JSX), which allows us to weave interactive components straight into the documentation itself.
Let’s take a look at an
wui-alert page with a preview of several use-cases. We can toggle the built-in code editor, see how they’re built, and even update the code to see immediate results in the previews above. This section also features an action log and a theme toggler to view how the components adjust to dark and light backgrounds.
The true beauty of theses docs lie in their autogeneration: the Props, Events & Methods markdown categories are automatically populated from the component’s prop type definitions. This ensures that our docs always match the code and that we don’t spend time rewriting them from scratch.
Atlas also features a playground mode, where you can freely edit a code example with any valid JSX and then share the link with anyone who has access to Atlas:
Because the code remains editable and the output is fully interactive, this feature has become incredibly helpful for sharing quick samples and debugging faulty implementation.
Its easy-to-scan design coupled with flexible docs has made Atlas a tool that’s as beautiful as it is functional. It has become indispensable to JW Player engineers who work on front-end projects.
The Payoff 🎉
At the time of writing this, WUI is at
v7.3.0 and steadily growing. The Stencil-based web components approach has proven to be a great improvement on past iterations and has integrated seamlessly into our major codebases.
The combination of a consumable design library like Hook and a highly flexible component library like WUI has saved countless developer hours, allowed us to remove duplicated code, made our UI more beautiful and consistent, improved our QA testing process, and has encouraged our teams to forge ahead on UI-heavy projects with confidence.
Next Up… 🧚♀️
In part 3, we’ll investigate how we use Hook, WUI, and other internal tools to improve design handoff and forge a stronger partnership between designers and engineers.