Build an Image Library Using Web Components

Banner for a MediaJam post


Every front-end developer knows about at least one of the popular frameworks like React, Vue, Angular, and Next. They probably know about a few more as well. These frameworks are great for most applications, but there are a few cases you might not want to use them.

You might need to build an app around performance and don't want the overhead of a framework or you want to build a framework-agnostic component library. The second case is usually the most common. That's why we're going to build a simple image library with web components to upload and display images we host in Cloudinary to show off how this can work.

What are Web Components

Web components are a set of APIs that let you create custom HTML tags to use in your web apps. It's basically a way for you to build the same functionality you can in a framework, without one. It works with the shadow DOM so you can build hidden DOM trees and only show them when you want. This lets you build things in the background before they get rendered.

It uses HTML template elements to define the components and you can still use ES modules in your implementation. You don't need a library to work with it because you can build web components with pure HTML and JavaScript, but it does help to have at least a simple library to get things running.

Initialize the Lit app

We do need to set up the folder for the project. So create a new folder called image-library and run the following command.

1$ npm i

This will take you through a few questions in the terminal that you can feel free to hit "Enter" all the way through. Now we need to install the Lit library to work with web components and build this little app.

1$ npm i lit

Just to see how much smaller this library is compared to the frameworks we're working to replace, take a look in the node_modules and see how small it is. This is a huge reason that web components are still used and considered. That and they can be used in any of the frameworks if you need to!

You'll also need a free Cloudinary account to host all of your pictures for this app. So if you don't have one, go sign up here. You'll need the cloud name and upload preset values from your console so we can upload images. Go ahead and make a .env file at the root of your project. It'll look something like this:


Set up the tsconfig

We're going to be using TypeScript throughout this app, so we want to have the configs properly set. Create a new file called tsconfig.json at the root of the project and add the following code.

2 "compilerOptions": {
3 "experimentalDecorators": true
4 }

This should be all you need for the app to run, but feel free to update this with your favorite rules. Now that everything is set up, let's continue with the upload component.

Make the upload component

While there is an SDK that makes this component for us, we can also write a custom upload widget using a web component to have more control over styles and handling requests. In the image-library folder, add a new folder called components. In that folder, add a new file called upload-widget.ts.

This is where we'll define the web component. In this file, add the following code.

1import { LitElement, html, css } from "lit";
2import { customElement } from "lit/decorators.js";
4async function uploadReq(e: any) {
5 const uploadApi = `${process.env.CLOUDINARY_CLOUD_NAME}/image/upload`;
7 const dataUrl =;
9 const formData = new FormData();
11 formData.append("file", dataUrl);
12 formData.append("upload_preset", process.env.CLOUDINARY_UPLOAD_PRESET);
14 await fetch(uploadApi, {
15 method: "POST",
16 body: formData,
17 });
21export class UploadWidget extends LitElement {
22 static styles = css`
23 button {
24 font-size: 18px;
25 padding: 2px 5px;
26 }
27 `;
29 render() {
30 return html`
31 <form onSubmit=${(e: any) => uploadReq(e)}>
32 <label htmlFor="imageUploader">Upload an image here</label>
33 <input name="imageUploader" type="file" />
34 <button type="submit">Add image</button>
35 </form>
36 `;
37 }

Let's go through what's happening in the code here. First, we import a few packages. Next, we define a function called uploadReq. This will take an uploaded file and send it to our Cloudinary account. You'll need the credentials for your account to make the POST request work and you can get those from the Cloudinary dashboard.

After the uploadReq function, we start defining the component. We use the @customElement decorator so that our app will know that this is a custom HTML element called upload-widget. Then we define some styles for the widget. Lastly, we build the form that will be rendered when this web component is added to the screen. This form lets users upload any image file they want and it will trigger a POST request to Cloudinary.

Create the image library

Now we need to get all of the images from Cloudinary that we want to show in our library. You'll need a couple more credentials, so go back to your dashboard and get an API key and the corresponding API secret and add those to your .env file. So your file should look like this:

1# .env

Now add a new file to the components folder called library-display.ts. Add the following code to the file:

1// library-display.ts
2import { LitElement, html, css } from "lit";
3import { customElement } from "lit/decorators.js";
5interface Image {
6 title: string;
7 url: string;
11export class LibraryDisplay extends LitElement {
12 static styles = css`
13 .container {
14 display: flex;
15 justify-content: space-between;
16 width: 100%;
17 }
18 .image-card {
19 padding: 8px;
20 width: 250px;
21 }
22 `;
24 images: Image[] = [
25 {
26 title: "dogs",
27 url: `${process.env.CLOUDINARY_CLOUD_NAME}/image/upload/v1606580778/3dogs.jpg`,
28 },
29 {
30 title: "dogs",
31 url: `${process.env.CLOUDINARY_CLOUD_NAME}/image/upload/v1606580778/3dogs.jpg`,
32 },
33 ];
35 async fetchImages() {
36 const results = await fetch(
37 `${process.env.CLOUDINARY_CLOUD_NAME}/resources/image`,
38 {
39 headers: {
40 Authorization: `Basic ${Buffer.from(
41 process.env.CLOUDINARY_API_KEY +
42 ":" +
44 ).toString("base64")}`,
45 },
46 }
47 ).then((r) => r.json());
49 const { resources } = results;
51 const allImgs: Image[] = => ({
52 url: resource.secure_url,
53 title: resource.public_id,
54 }));
56 this.images = allImgs;
57 }
59 render() {
60 this.fetchImages();
61 return html`
62 <div class="container">
63 ${this.images.length > 0
64 ?
65 (image) =>
66 html`
67 <img
68 class="image-card"
69 src="${image.url}"
70 alt="${image.title}"
71 />
72 `
73 )
74 : html` <div>No images</div> `}
75 </div>
76 `;
77 }

We start this component off with some imports and a type definition. Then we create a new custom library-display component. Next we add some styles and then we seed a few images. Make sure you update the image files to something in your own Cloudinary account!

Then we define the fetchImages method which will retrieve all of the images we upload to Cloudinary and update the images array we have in the component. Finally, we call the render function which is where we call fetchImages, and then create all of the HTML elements to display the images. That's all for this component. Now we need to update the index.html file to use these custom web components.

Use the web components

Open the index.html file, delete any existing code and add the following:

1<!-- index.html -->
3 <head>
4 <title>This library though</title>
5 <script type="module" src="./upload-widget.ts"></script>
6 <script type="module" src="./library-display.ts"></script>
7 </head>
8 <body>
9 <upload-widget></upload-widget>
10 <library-display></library-display>
11 </body>

We import the components in the <head> and then use them in the <body>. That's all we need to display these to users! So you've written a couple of custom web components to handle this whole library for you.

finished app

Finished code

You can check out the complete code for this project in the image-library folder of this repo. You can also check it out in this Code Sandbox.


Now that you've seen what it's like to work with web components using Lit, maybe you can convince your team to stray away from those big frameworks. Depending on the type of application, it can be easier to make up your own internal framework as you build.


Software Team Lead

Milecia is a senior software engineer, international tech speaker, and mad scientist that works with hardware and software. She will try to make anything with JavaScript first.