In the previous blog article, I explained the concepts you need to know before building a pure javascript reusable component. It is now time to practice our newly acquired knowledge.
In this tutorial, we will go through everything you need to know to kickstart your widget. We will cover a wide range of topics including:
- The tools we will install
- Setting up a project
- Writing a user interface
- Sharing the library
To facilitate writing user interfaces, I decided to use a few tools. These tools do not add any dependencies to the deliverables.
- Webpack: a tool that we will use to bundle our source code in a single .js file.
- SASS: CSS with superpowers
- PostCSS, autoprefixer and cssnano: tools to add vendor prefixes and minify the css code
- Uglify-js: a javascript minifier
Because the web does not have enough todo-list implementations, our sample project will be a todo-list widget.
Note: you can find the source code of this widget here.
Setting up the project
First, make sure your installed node.js and npm on your computer. Then, create a folder to hold your project and run the following command inside it:
npm init
It will ask you a few questions. For information, you can find my answers on the image below.

Installing Webpack

Webpack will bundle all our files into a single JS file that we will later share. Along with all the dependencies I presented earlier, we will also install webpack-dev-server, which is a development server that provides live reloading. It will help us develop our widget.
npm install --save-dev webpack webpack-dev-server webpack-cli sass-loader node-sass css-loader style-loader postcss-loader uglifyjs-webpack-plugin html-webpack-plugin cssnano autoprefixer
Installing babel

Babel will transpile our ES6 code into ES5 to ensure it works with older browsers.
npm install babel-loader @babel/core @babel/preset-env --save-dev
Setting up Webpack and PostCSS
Now that we installed all the dependencies, let's add our webpack and postcss configuration.
In the root directory of your project, create a webpack.config.js file and append the following configuration to it
const path = require('path');
const HTMLWebpackPlugin = require('html-webpack-plugin');
const uglifyJsPlugin = require('uglifyjs-webpack-plugin');
const webpack = require('webpack');
const libraryName = 'todo-widget'; // TODO: Change me
const outputFile = `${libraryName}.min.js`;
module.exports = {
entry: './src/index.js',
output: {
library: libraryName,
libraryTarget: 'umd',
libraryExport: 'default',
path: path.resolve(__dirname, 'dist'),
filename: outputFile,
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: [
{
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
},
},
],
},
{
test: /\.scss$/,
use: [
'css-loader',
'postcss-loader',
'sass-loader',
],
},
{
test: /\.(png|jp(e*)g|svg)$/,
use: [{
loader: 'url-loader',
options: {
limit: 20000, // Convert images < 8kb to base64 strings
name: 'img/[hash]-[name].[ext]',
},
}],
},
],
},
plugins: [
new uglifyJsPlugin(),
new HTMLWebpackPlugin({
template: path.resolve(__dirname, 'index.html'),
}),
new webpack.HotModuleReplacementPlugin(),
],
};
This file basically tells webpack to package any css/javascript or image into a single file. You need to change the 7th line with the name of your library.
Finally, to setup PostCSS, create a file called postcss.config.js and append the following content to it:
module.exports = {
plugins: [
require('autoprefixer'),
require('cssnano'),
]
};
It tells postcss to load autoprefixer and cssnano.
Setting up a development environment
Finally, before we start coding, we will setup a development environment. To start our development server, we will need to create a new npm script. Open your package.json file, and locate the "scripts" key.
Add the following values to the list of available scripts:
"build": "webpack",
"start": "webpack-dev-server --open"
It should now look like this
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack",
"start": "webpack-dev-server --open"
},
Then, create an index.html file. This is where we will display our plugin while we're developing it.
<!doctype html>
<html>
<head>
<title>Todo Widget</title>
</head>
<body>
Hello, world!
</body>
</html>
In the webpack configuration, we specified that the entry point of our library is src/index.js
. Create the src folder and the index.js file. Then, add an alert so we can test everything is working correctly.
alert('hello, world from the widget!');
To start the dev server, run npm start
. It will automatically build our plugin and insert it in the index.html
file we created earlier. Open your browser and navigate to https://localhost:8080. The alert should display.

Coding the widget
Our widget will define two custom elements: todo-list
and list-item
. The first one holds the list and the second one is an element of the list.
Each list-item can have a "done" attribute that will strike the text if set.
Add the following code in the <body>
your index.html:
<todo-list>
<list-item done>Clean the bedroom</list-item>
<list-item>Watch a movie</list-item>
</todo-list>
Once we finish our plugin, this code will render a nice todo-list that look like this

Here's a list of all the files we will create for our widget.
.
├── src
│ ├── css
│ │ ├── item.scss
│ │ └── list.scss
│ ├── lib
│ │ ├── list-item.js
│ │ └── todo-list.js
│ └── index.js
├── README.md
├── index.html
├── package-lock.json
├── package.json
├── postcss.config.js
└── webpack.config.js
The src/lib
folder will contain the source code of our widget. As you can see, it has two files in it, one for each custom element. The src/index.js
file will register our custom elements so we can use them in the HTML page. As we saw earlier, it is the entrypoint of our library.
In the css
folder, we will add all our styles. In this tutorial, I decided to create one for each custom-element.
The todo-list element
Let's start with todo-list.js
. At the top of the file, I like to define the initial structure of our web component. To do this, I create a <template>
element.
import styles from '../css/list.scss'
const template = document.createElement('template');
template.innerHTML = `
<style>${styles.toString()}</style>
<ul>
<slot>
<p>You have nothing to do, yay!</p>
</slot>
</ul>
<input id="new-item"/>
<button>Add</button>
`;
As you can see, I imported the styles from the lib/css/list.scss
file. Then, I used the toString()
method inside a <script>
element to append them into the DOM. Why did I import the styles as a module? Because we will later create a Shadow DOM and we have to define them inside it. If we directly imported them using import '../css/list.scss'
, they would be defined outside of our shadow DOM and may also clash with the ones already defined in the page.
Our custom-element is simple. It has an unordered list, an input and a button. Inside the list, you will notice a <slot>
element. In a shadow DOM, the content of a slot will automatically be replaced with the children from the Light DOM.
<todo-list>
#shadow-root
<slot></slot> <!-- the slot will contain the two list-items -->
<list-item done>Clean the bedroom</list-item>
<list-item>Watch a movie</list-item>
</todo-list>
It's time to implement the element. Our class is simple. In the constructor, it initializes the structure of the element by rendering the template we created earlier. The connectedCallback
sets the "onclick" event on the button and the addItem method adds the new item once we click the Add button.
class TodoList extends HTMLElement {
constructor() {
super();
// Add a shadow DOM
const shadowDOM = this.attachShadow({ mode: 'open' });
// Render the template in the shadow dom
shadowDOM.appendChild(template.content.cloneNode(true));
// Method binding
this.addItem = this.addItem.bind(this);
}
// Called when the element is added to the DOM
connectedCallback() {
const button = this.shadowRoot.querySelector('button');
button.onclick = this.addItem;
}
addItem() {
// Get the input
const input = this.shadowRoot.querySelector('#new-item');
// Create a new list item
const item = document.createElement('list-item');
item.innerHTML = input.value;
// Add it to the light DOM
this.appendChild(item);
}
}
export default TodoList;
To showcase how styling works, you can append the following content to src/css/list.scss
ul {
background-color: #ffc7c7;
}
The list-item element
Again, I first defined the styles and template for this element, then implemented it. As it is similar to the one we just defined, I'll just give you the code.
import styles from '../css/item.scss';
const template = document.createElement('template');
template.innerHTML = `
<style>${styles.toString()}</style>
<li>
<span>Empty</span>
<button>Remove</button>
</li>
`;
class ListItem extends HTMLElement {
constructor() {
super();
// Add a shadow DOM
const shadowDOM = this.attachShadow({ mode: 'open' });
// Render the template
shadowDOM.appendChild(template.content.cloneNode(true));
// Bindings
this.onRemove = this.onRemove.bind(this);
this.renderChanges = this.renderChanges.bind(this);
this.onDoneChange = this.onDoneChange.bind(this);
}
// any attribute specified in the following array will automatically
// trigger attributeChangedCallback when you modify it.
static get observedAttributes() {
return ['done'];
}
renderChanges() {
const text = this.shadowRoot.querySelector('li > span');
// done attribute
if (this.hasAttribute('done')) {
text.style.textDecoration = 'line-through';
} else {
text.style.textDecoration = 'none';
}
}
// Mark: - Component lifecycle
attributeChangedCallback(attr, oldVal, newVal) {
this.renderChanges();
}
connectedCallback() {
const shadowDOM = this.shadowRoot;
// init list item text and button
const text = shadowDOM.querySelector('li > span');
text.innerHTML = this.innerHTML;
text.onclick = this.onDoneChange;
// remove button action
const button = shadowDOM.querySelector('li > button');
button.onclick = this.onRemove;
this.renderChanges();
}
// Mark: - Actions
onRemove() {
this.remove();
}
onDoneChange() {
this.toggleAttribute('done');
}
}
export default ListItem;
Registering our custom elements
Finally, open src/index.js
and append the following code to it:
import TodoList from './lib/todo-list';
import ListItem from './lib/list-item';
customElements.define('list-item', ListItem);
customElements.define('todo-list', TodoList);
It will register our two custom elements. Navigate to https://localhost:8080/ and you should see it working.
Building production-ready code
To build your code, all you have to do is running the following command:
npm run build
It will create a dist/ folder containing your final javascript file. 🎉
Distributing the library
There are three ways you can distribute your library:
- as a file
Probably the easiest way to share your widget. In the dist/ folder, grab your .js file and share it.
- as a NPM package
You can publish your package on NPM in 3 easy steps.
- Create an account on npmjs.com
- Login to npm in your terminal by running the following command:
npm login
- Run
npm publish
You should see something like this.

- As a link (hosted on a CDN)
To achieve this, we will use unpkg. Unpkg is a free CDN that hosts your npm packages. Once published, you can get a link to your package using their service.
All you have to do is adding the following lines to your package.json:
"unpkg": "dist/plugin-name.min.js",
"files": [
"dist"
],
The first line tells unpkg the file to serve, and the last 3 lines tell npm to add the dist folder in the package. (if you ignored it in the .gitignore file)
The link to your file will look like this
https://unpkg.com/:package@:version/dist/:package.min.js
in our case, it would be https://unpkg.com/todo-widget@1.0.2/dist/todo-widget.min.js
Finally, the following HTML initiates our plugin and downloads the script from the CDN:
<!doctype html>
<html>
<head>
<title>Todo Widget</title>
</head>
<body>
<todo-list>
<list-item done>Clean the bedroom</list-item>
<list-item>Watch a movie</list-item>
</todo-list>
<script type="text/javascript" src="https://unpkg.com/todo-widget@1.0.2/dist/todo-widget.min.js"></script></body>
</html>
That's it! I hope you enjoyed this article. If you want to be notified when we post something new, click on the notification bell located at the bottom right end corner of your screen. By the way, the bell is a widget I built using the knowledge I shared in this tutorial 😉