Building a Multi-Step Loading Screen with Electron
In this article, I'll walk you through how I built an Electron application with a multi-step loading screen. We'll cover everything from setting up the main Electron process to designing the splash screen and login page.
Why Electron?
Electron allows us to build cross-platform desktop apps with web technologies like HTML, CSS, and JavaScript. It provides the tools to create powerful user experiences, such as loading screens and transitions, while keeping the code simple.
Setting up the Main Process
The heart of any Electron app lies in its main.js
. Here's how we set up the main process to create a splash screen, load the main window, and simulate a multi-step loading process:
// Modules to control application life and create native browser window
const { app, BrowserWindow } = require('electron')
const path = require('node:path')
const EventEmitter = require('events')
const loadingEvents = new EventEmitter()
let mainWindow = null
let splashWindow = null
function createWindow () {
// Create the browser window.
mainWindow = new BrowserWindow({
width: 800,
height: 600,
show: false,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
nodeIntegration: false,
contextIsolation: true
}
})
// Add splash load file
splashWindow = new BrowserWindow({
width: 600,
height: 500,
frame: false,
alwaysOnTop: true,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
nodeIntegration: false,
contextIsolation: true
}
});
loadingEvents.on('finishedProgressBar', async () => {
try {
splashWindow.close();
await mainWindow.loadFile('src/pages/login-google.html');
mainWindow.center();
mainWindow.maximize();
mainWindow.show();
} catch (error) {
console.error('Error loading main window:', error);
}
})
loadingEvents.on('progressBar', async data => {
try {
splashWindow.webContents.send('progressBar', data);
} catch (error) {
console.error('Error sending progressBar event:', error);
}
setTimeout(() => loadingEvents.emit('finishedProgressBar'), 1000)
})
// Load splash screen file
splashWindow.loadFile('src/pages/splash.html').then(() => {
splashWindow.center();
setTimeout(() => {
loadingEvents.emit('progressBar', { percentage: 100, step: 2 });
}, 5000);
}).catch(error => {
console.error('Error loading splash window:', error);
});
// Clean up ressources when splash window is closed
splashWindow.on('closed', () => {
splashWindow.removeAllListeners()
splashWindow = null
})
// Open the DevTools.
// mainWindow.webContents.openDevTools()
}
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.whenReady().then(() => {
createWindow()
app.on('activate', function () {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
// if (splashWindow) {
// splashWindow.on('closed', () => {
// splashWindow.removeAllListeners()
// splashWindow = null
// })
// }
})
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
// Error handling for uncaught exceptions
process.on('uncaughtException', (error) => {
console.error('Uncaught exception:', error);
});
process.on('unhandledRejection', (reason) => {
console.error('Unhandled rejection:', reason);
});
// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and require them here.
This script does the following:
Creates a
splash window
that serves as a loading screen.Emits events (
progressBar
andfinishedProgressBar
) to manage the loading steps.Switches to the main window (
login-google.html
) after loading is complete.
Designing the Splash Screen
The splash screen shows a progress bar and loading animations. Here's the splash.html
file:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
.bg-primary {
background-color: rgb(83 105 248 / 1);
}
</style>
</head>
<body class="flex items-center justify-center h-screen">
<div aria-label="Loading..." role="status" class="space-x-2">
<div class="m-auto mb-[5rem] flex justify-center items-center">
<div class="absolute animate-spin rounded-full h-32 w-32 border-t-4 border-b-4 border-purple-500"></div>
<img src="https://www.svgrepo.com/show/509001/avatar-thinking-9.svg" class="rounded-full h-28 w-28">
</div>
<span class="text-4xl font-medium text-gray-700 flex text-center">Chargement des
dépendances</span>
<div class="mt-[2rem]">
<p class="text-gray-400 mb-3 dark:text-gray-400 text-center" id="step-block">Step 1 / 3</p>
<div class="w-full h-3 bg-black/10 rounded-full">
<div id="progress-bar-loading"
class="bg-primary h-3 rounded-s-full w-0 animate-strip"
style="background-image:linear-gradient(45deg,hsla(0,0%,100%,.15) 25%,transparent 0,transparent 50%,hsla(0,0%,100%,.15) 0,hsla(0,0%,100%,.15) 75%,transparent 0,transparent);background-size:1rem 1rem"></div>
</div>
</div>
</div>
<script>
// Fallback for when IPC messages are not received
document.addEventListener('DOMContentLoaded', () => {
const progressBarLoading = document.getElementById('progress-bar-loading');
const stepBlock = document.getElementById('step-block');
// Default loading state
progressBarLoading.style.width = '33%';
stepBlock.innerHTML = 'Step 1 / 3';
});
ipc.on('progressBar', (event, message) => {
const percentage = message.percentage
const step = message.step
// Update DOM element progressBar
const progressBarLoading = document.getElementById('progress-bar-loading')
progressBarLoading.classList.remove('rounded-s-full')
progressBarLoading.classList.add('rounded-full')
progressBarLoading.style.width = percentage + '%'
// Update HTML text to step-block
const stepBlock = document.getElementById('step-block')
stepBlock.innerHTML = `Step ${step} / 3`
})
</script>
</body>
</html>
Creating the Login Page
After the splash screen finishes, we transition to a login page (login-google.html
). Here's the code:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta name="description" content="Login with Google to access fully functional accounts starting at $19/month.">
<title>Login with Google</title>
<link rel="stylesheet" href="../../dist/source/style.css">
</head>
<body>
<main class="p-6">
<div class="2xl:px-32 mx-auto">
<!-- Title -->
<div class="max-w-lg mx-auto text-center">
<h2 class="sm:text-2xl text-xl font-semibold mb-2">Login with Google</h2>
<p class="text-gray-600">Fully functional accounts are starting from $19/month only</p>
</div>
<!-- Form -->
<div class="mt-10 max-w-4xl flex items-center m-auto">
<div class="card">
<div class="p-6">
<div class="flex items-center justify-between gap-5">
<div>
<h6 class="text-base mb-2">Form</h6>
</div>
</div>
<hr class="mb-5">
<p class="text-gray-600">
It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English. Many desktop publishing packages and web page editors now use Lorem Ipsum as their default model text, and a search for 'lorem ipsum' will uncover many web sites still in their infancy. Various versions have evolved over the years, sometimes by accident, sometimes on purpose (injected humour and the like).
</p>
<div class="mt-14 text-center">
<form action="/auth/google" method="POST">
<button type="submit" class="btn font-medium px-8 bg-primary/90 text-white hover:bg-primary" aria-busy="false">
<span id="login-text">Login</span>
<span id="loading-spinner" class="hidden animate-spin ml-2"></span>
</button>
</form>
</div>
</div>
</div>
</div>
</div>
</main>
<script>
const loginButton = document.querySelector('button');
loginButton.addEventListener('click', () => {
const spinner = document.getElementById('loading-spinner');
const text = document.getElementById('login-text');
spinner.classList.remove('hidden');
text.textContent = 'Logging in...';
});
</script>
</body>
</html>
Final Thoughts
This project demonstrates how to implement a multi-step loading screen in an Electron app. By combining BrowserWindow
, EventEmitter
, and IPC, we ensure smooth transitions between screens.
Top comments (0)