Don’t Reinvent The Wheel! Use White-Label Web Components

Creating and maintaining high quality web components is hard work. You not only implement the crucial functionality but — for UI components — you also have to make it look nice.
And while reusability is one core aspect of web components, it’s most often bound to the design language used: Simple input fields based on Material Design or Fluent UI work like the other, but look different. You don’t want to implement the component twice, so wouldn’t it be nice to separate functionality and design?

Look different, but work alike… Implement twice?

Enter Lion, a collection of white-label web components, created by ING (aka ING Diba). The components are open-sourced and available on GitHub. For now Lion concentrates on form and navigation controls, as well as overlays and localization. But apart from buttons and inputs you can also find dialogs and even a calendar. Clearly, the web component collection is a byproduct of the ING development team.

|We recommend you to read the announcement blog post and browse the live Storybook

Browsing the live storybook the first obvious thing is: the components have no default styles; or better: they use the browsers default styles. It is almost like when you forget to include your CSS into a website. But that’s the point! The web components are fully functional, for example form controls can be labelled, prefilled, disabled, and validated. But they are not opinionated regarding design. Make use of them and your only task is to make them blend in.

Lion Web Components are based on lit-html / lit-element. Each component resides in a separate npm repository and can be imported as ES module. To apply CSS directives to a component — or to add functionality — you have to subclass it. Because the components use Shadow DOM, global styles are not applied automatically.

The examples in the Lion docs are bare minimum — based on the assumption that you have a suitable development environment ready. We will get into more detail here.

We will do the following:
1. Setup a development environment and install our dependencies
2. Extend a Lion component to apply some styling.
3. Create a bundle to use the styled component elsewhere

Setup a development environment and install our dependencies

Our base component will be the basic input. Create a new folder named white-label-web-components for our projects, run

npm init

and install the component with

npm i --save @lion/input

As a result, the node_modules contain quite a lot packages, several of them in the @lion subfolder (most importantly the input subfolder), as well as lit-html and lit-element as base classes of the Lion components.

Small set of node_modules for Lion components

Furthermore bootstrap an index.html.

<!DOCTYPE html>
<html lang=”en”>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>White-Label Web Components</title>
<lion-input name="firstName" label="Input label"></lion-input>
<script type="module" src="main.js"></script>

Additionally setup a one-liner main.js looking like this:

import '@lion/input/lion-input.js';

When you open the index file using a simple web server like Pythons SimpleHTTPServer, PHP builtin server, or NodeJS lite-server, then you will see *nothing*. The paths used to load the ES module and it’s recursive dependencies cannot be resolved. We will have to setup a special dev server. You might be familiar with the LitElement dev environment, which will work perfectly. Alternatively you can use the es-dev-server by open-wc. You might have noticed “@open-wc” as a dependency of the Lion components in the node_modules. That is no coincidence as Lion and open-wc are (at least partly) backed by the same people. We will go with the lightweight option for now and use es-dev-server:

npm i —-save-dev es-dev-server

This will inflate your node_modules folder quite a bit, but thats okay. All additions have nothing to do with the Lion components. Add a script to the package.json to start the server in watch mode. Every change in the source files (directly or indirectly referenced by index.html) will cause the browser window to reload. Notice the node-resolve option that will fix the paths of referenced files on the fly:

"scripts": {
"serve": "es-dev-server --app-index index.html --node-resolve --watch --open" }

Running npm run serve will open up a new browser window showing http://localhost:8000 with our desired unstyled input. If you are interested, open the Network tab of your Dev Tools and preview the file named “LionInput.js”. Note that the path of

import { LionField } from '../../form-core/index.js'; 

differs from what you can see in the source file in the IDE:

import { LionField } from '@lion/form-core';

That’s what the es-dev-server is for.

Rewritten paths in your source code
The line in the original source file.

Extend a Lion component to apply some styling

Now that we see the lion-input in the browser, let’s create a brushed up version. First subclass LionInput, the base class of lion-input web components: Create a file pretty-input.js on the top level of your project, next to index.html and main.js.

import { css } from 'lit-element';
import { LionInput } from '@lion/input';
export class PrettyInput extends LionInput {static get styles() {
return [
font-family:Segoe UI WestEuropean,Segoe UI,
-apple-system,BlinkMacSystemFont,Roboto,Helvetica Neue,
display: inline-block;
::slotted(input:focus) {
border: 1px solid #33C3F0;
outline: 0;
::slotted(label) {
display: block;
margin-bottom: .5rem;
font-style: italic
::slotted(input) {
height: 38px;
padding: 6px 10px;
background-color: #fff;
border: 1px solid #D1D1D1;
border-radius: 4px;
box-shadow: none;
box-sizing: border-box;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
}customElements.define('pretty-input', PrettyInput);

Within the pretty-input.js we create a the class PrettyInput as subclass of LionInput. Therefore LionInput has to be imported. The PrettyInput class is also exported as we are creating a new ES module here and maybe someone needs subclass PrettyInput itself. The most part of our class is the styles getter, a benefit that LionInput inherited from LitElement. Using the css tag function we can inject our CSS directives as string. The css tag function has to be imported as well. The styles are originally taken from Skeleton, a tiny CSS library.
The :host() selector references the custom element itself and is used for general styling like applying a nice font-face for all major OSes. As most of the parts in the elements Shadow DOM are exposed as slots, the ::slotted() selector is used to reference all parts like the input or label.
At last we define a custom element named pretty-input based on the PrettyInput class.

The pretty input is ready to be used, so next we extend our main.js :

import '@lion/input/lion-input.js';
import './pretty-input.js';

and index.html files

<!DOCTYPE html>
<html lang=”en”>
<meta charset="UTF-8">
<meta name="viewport” content=”width=device-width, initial-scale=1.0">
<title>White-Label Web Components</title>
<lion-input name="firstName" label="Input label"></lion-input>

<pretty-input name="prettyInput" label="Pretty Input" >
<div slot="help-text">This looks much nicer</div>

<script type="module" src="main.js"></script>

You can find more about styling, e.g. about reuseable styles, in the LitElement docs.

Isn’t that pretty — the second one of course.

Create a bundle to use the styled component elsewhere

Bundling is a topic for itself. Most of you will know Rollup, Webpack, Parcel, and other, but you might not have used them directly. When you start your LitElement projects using the Javascript / Typescript starter projects mentioned above, a bundler is already included.
For those of you not familiar to bundler: a bundler takes the sources of your web project, gathers all dependent scripts that are required, imported or referenced by script tags, deduplicates code, wrangels them all up and outputs a single Javascript file to be included in the production version of your code.

To show how bundling our PrettyInput component works, we install Rollup via npm:

npm install rollup

You can install Rollup globally to have it in your toolbox for upcoming projects:

npm install --global rollup

Rollup faces the same problems resolving the dependencies in node_modules but luckily there is a plugin ready. We add it to our project with:

npm install --save-dev rollup-plugin-node-resolve

At last we write a configuration script for rollup to point it to the right direction:

// rollup.config.js
import resolve from 'rollup-plugin-node-resolve';
export default {
input: 'main.js',
output: {
file: 'bundle.js',
format: 'cjs'
plugins: [
browser: true,
only: [ /^@lion\/.*$/ ]

Basically the configuration defines main.js as entry point for the bundling. The file bundle.js will be the output. The node-resolve plugin is imported like any ES module and should run in browser mode, meaning it respects potential browser fields in the package.json of the dependencies.
The only parameter of the resolve configuration is helpful when you want to only include some of the dependencies. All other dependencies will be marked as external and you have to take care of inlcuding them yourself. In our example we only include the @lion* parts, so LitElement will not end up in the bundle. When you aim at a single bundle file, remove the line strting with only.


rollup --config

on the command line and the bundle gets generated in the main directory white-label-web-components. That’s it!

The tech staff of OneBitAhead GmbH, putting the web stack to work. Here to discuss daily bits & bytes. #javascript #nodejs #webcomponents #rdbms #php