See, the [ is a cell 0 and - s from 1 to 29 and ] at cell 30. So now to simulate loading, we will have to write = from cells 1 to 29.

To start, let’s create a class LoadingBar .

class LoadingBar { }

Now, we need to know the amount of data or size of the data that you want the progress shown.

class LoadingBar { constructor(size) {

this.size = size

}

}

we will have a start method that when called will start the loading:

class LoadingBar { constructor(size) {

this.size = size

} start() { }

}

First, we have to disable the cursor, to do that we use the write method in process.stdout Writable stream to write "\x1B[?25l" , this will remove the cursor from the terminal, to enable it we will write "\x1B[?25h" .

start() {

process.stdout.write("\x1B[?25l")

}

Next, we will write [ the opening bracket

start() {

process.stdout.write("\x1B[?25l")

process.stdout.write("[")

}

Now, we write the - the foreground symbol, now, we will loop over the size given to use to know how many of - to write:

start() {

process.stdout.write("\x1B[?25l")

process.stdout.write("[")

for (let i = 0; i < this.size; i++) {

process.stdout.write("-")

}

}

Now, the size of the data is indeterminate till runtime, so we have to track the cursor position. To do that we will create a cursor variable and set it to 0, then, increase by one when we write the - . This will be helpful when we want to render the = symbol to show loading, it will help us to know which cell position to start rendering = and where to stop which is important:

constructor(size) {

this.size = size

this.cursor = 0

} start() {

process.stdout.write("\x1B[?25l")

process.stdout.write("[")

this.cursor++

for (let i = 0; i < this.size; i++) {

process.stdout.write("-")

this.cursor++

}

}

Now, after rendering the — the size of this.size , we will write the closing bracket ] :

constructor(size) {

this.size = size

this.cursor = 0

} start() {

process.stdout.write("\x1B[?25l")

process.stdout.write("[")

this.cursor++

for (let i = 0; i < this.size; i++) {

process.stdout.write("-")

this.cursor++

}

process.stdout.write("]")

}

Now, we have to simulate the loading effect. To do that we will use the setInterval API. The setInterval API is used to execute a block of code at a specified interval of time. We know that the = have to be written over the - cells at an interval to look like loading. So, we will call the setInterval with a time interval of 100ms and a callback function.

In the callback function passed to the setInterval, we will start from the cell after the opening bracket [ and write = till the end of the - before ] .

// index.js ...

constructor(size) {

this.size = size

this.cursor = 0

} start() {

process.stdout.write("\x1B[?25l")

process.stdout.write("[")

for (let i = 0; i < this.size; i++) {

process.stdout.write("-")

}

process.stdout.write("]")

this.cursor = 1

rdl.cursorTo(process.stdout, this.cursor, 0)

setInterval(() => {

process.stdout.write("=")

this.cursor++

if (this.cursor >= this.size) {}

}, 100)

}

See, we used the cursorTo API in readline library to set the cursor to cell 1, so when we call the write method it will begin from the cell 1 to write data. We set the cursor at cell 1 because the [ is at cell 0 so we will start at cell 1 not to overwrite it.

See the callback function in the setInterval, write = then, increase the variable cursor by one. It checks if the cursor variable is greater than or equal to the size of the data, if it is true we know we are at the ] cell so we need to stop.

We need to stop the time interval setup by the setInterval API, to do that we need to get hold of the Timer instance returned by the API, so we can stop it by passing it to clearInterval API.

// index.js ...

constructor(size) {

this.size = size

this.cursor = 0

this.timer = null

} start() {

process.stdout.write("\x1B[?25l")

process.stdout.write("[")

for (let i = 0; i < this.size; i++) {

process.stdout.write("-")

}

process.stdout.write("]")

this.cursor = 1

rdl.cursorTo(process.stdout, this.cursor, 0);

this.timer = setInterval(() => {

process.stdout.write("=")

this.cursor++;

if (this.cursor >= this.size) {

clearTimeout(this.timer)

}

}, 100)

}

See, we created a timer variable then, assigned it to the value returned by setInterval call. We then called the clearInterval with the timer instance. This will make the loading effect to stop when we are the end of the cell. After that, we re-enabled the cursor by writing "\x1B[?25h" to the stdout stream.

With this our loading bar is complete.

See the full code:

// index.js

const process = require("process")

const rdl = require("readline") class LoadingBar {

constructor(size) {

this.size = size

this.cursor = 0

this.timer = null

} start() {

process.stdout.write("\x1B[?25l")

process.stdout.write("[")

for (let i = 0; i < this.size; i++) {

process.stdout.write("-")

}

process.stdout.write("]")

this.cursor = 1

rdl.cursorTo(process.stdout, this.cursor, 0);

this.timer = setInterval(() => {

process.stdout.write("=")

this.cursor++;

if (this.cursor >= this.size) {

clearTimeout(this.timer)

process.stdout.write("\x1B[?25h")

}

}, 100)

}

}

Let’s run it:

// index.js

...

const ld = new LoadingBar(50)

ld.start()

We made the size to be 50.

node .

Making it Single Bar

We used = and - to simulate loading progress, now we will use the bar instead:

█ ░

░ will serve as the foreground just as - . It will stretch out to fill the cells, then █ to overwrite it to show the progress lie = did. That will be more captivating than - and = .

Now we will touch our LoadingBar class:

class LoadingBar {

constructor(size) {

this.size = size

this.cursor = 0

this.timer = null

} start() {

process.stdout.write("\x1B[?25l")

1.➥ process.stdout.write("[")

for (let i = 0; i < this.size; i++) {

2.➥ process.stdout.write("-")

}

3.➥ process.stdout.write("]")

4.➥ this.cursor = 1

rdl.cursorTo(process.stdout, this.cursor, 0);

this.timer = setInterval(() => {

5.➥ process.stdout.write("=")

this.cursor++;

if (this.cursor >= this.size) {

clearTimeout(this.timer)

}

}, 100)

}

}

1. goes away because there will be no [ . 2. - will be changed to be ░ , the string "\u2591" does the effect. 3. Yeah, the closing bracket goes away, the decision was up to me you can leave it in your implementation then touch the code to incorporate the addition. 4. the cursor won't start from cell 0 because there is no more [ which occupies it. So the loading effect filling should start from cell 0. 5. = is now replaced by the bar █ , to display we will pass the string "\u2588" to stdout.

Now, our updated code would be this:

class LoadingBar {

constructor(size) {

this.size = size

this.cursor = 0

this.timer = null

} start() {

process.stdout.write("\x1B[?25l")

for (let i = 0; i < this.size; i++) {

process.stdout.write("\u2591")

}

rdl.cursorTo(process.stdout, this.cursor, 0);

this.timer = setInterval(() => {

process.stdout.write("\u2588")

this.cursor++;

if (this.cursor >= this.size) {

clearTimeout(this.timer)

}

}, 100)

}

}

Running it would give us this effect:

node .

Final Code 👍

Conclusion

This was just a guideline on how to build your own CLI loading bar. It is meant for you to understand the dynamics and concepts behind making the loading effect on the terminal using Nodejs. It is very simple, once you know that the terminal is divided into rows and columns, then you can manipulate the cursor to point at the cell of your choice.

If you have any questions regarding this or anything I should add, correct or remove, feel free to comment, email or DM me.

Thanks !!!

Learn more