Load HTML pages into Electron windows with loadFile and loadURL, add CSS, images and fonts with correct paths, and keep them working under a CSP.

Bishal Biswas
Self Learner
From plain page to real app screen
So far your app shows one plain HTML page. Real apps have styles, a logo, maybe a custom font and more than one page. Good news: in Electron, these work almost exactly like a website. There are just a few rules about paths and security to learn.
Paths are like giving directions. “Two doors down from here” (a relative path) works as long as you know where “here” is. “Number 1, Main Street” (an absolute path) works from anywhere, but only if that street really exists. Most problems with missing images and styles come from mixing these up.
loadFile and loadURL
A window can show a page in two ways.
loadFile: a page inside your app
// main.js
win.loadFile('index.html');
loadFile loads an HTML file from your project. A relative path like 'index.html' is looked up from your app’s folder (the one with package.json), so it works no matter where the terminal was when you started the app. You can also pass a full path:
// main.js
win.loadFile(path.join(__dirname, 'index.html'));
Both load the same file. Behind the scenes, Electron turns it into a file:// URL, so inside the page, location.href looks like this:
file:///C:/Users/asha/my-first-app/index.html
On macOS and Linux it starts with file:///Users/... or file:///home/... instead.
loadURL: a page from a web address
// main.js
win.loadURL('https://www.electronjs.org');
loadURL loads any URL, usually a website. It is handy for things like a help page, but be careful: a remote site running inside your app is a security risk if it gets any extra powers. Never give remote pages a preload with powerful functions. Read Loading Remote Content Safely before you ship an app that does this.
For your own app’s screens, prefer loadFile. Your pages then work offline and load instantly.
Organising your files
Here is a tidy layout for a plain project with styles and images:
my-first-app/
├── package.json
├── main.js
├── preload.js
├── index.html
├── settings.html
├── renderer.js
├── styles/
│ └── style.css
├── images/
│ ├── logo.svg
│ └── background.png
└── fonts/
└── Inter.woff2
Keep pages at the top, and group styles, images and fonts in their own folders.
Adding a stylesheet
Create styles/style.css:
/* styles/style.css */
body {
font-family: system-ui, sans-serif;
margin: 0;
padding: 2rem;
background: #1e1e1e;
color: #f0f0f0;
}
h1 {
color: #7cc4ff;
}
system-ui uses the operating system’s own interface font: Segoe UI on Windows, San Francisco on macOS, and your desktop’s font on Linux. It makes your app feel at home everywhere.
Link it in the <head> of index.html:
<!-- index.html -->
<head>
<meta charset="UTF-8" />
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'"
/>
<title>My First App</title>
<link rel="stylesheet" href="styles/style.css" />
</head>
The href is relative to the HTML file. Because index.html is in the project folder, styles/style.css means “the styles folder next to me”. Reload the window (Ctrl + R, or Cmd + R on macOS) and your page turns dark.
Tip: Set the window’s
backgroundColorto the same colour as your CSSbackground(here#1e1e1e). Then the window never flashes white while the page loads. See Creating Windows with BrowserWindow.
Adding images
Images work the same way:
<!-- index.html -->
<body>
<img src="images/logo.svg" alt="App logo" width="64" height="64" />
<h1>Hello from Electron!</h1>
</body>
And in CSS, paths are relative to the CSS file, not the HTML file:
/* styles/style.css */
body {
background-image: url('../images/background.png');
background-size: cover;
}
../ means “go up one folder”. From styles/style.css, ../images/background.png goes up to the project folder, then into images.
SVG, PNG, JPEG, GIF and WebP all work, because Chromium supports them.
Adding a custom font
Download a font file (.woff2 is the best format for the web), put it in fonts/, and declare it in your CSS:
/* styles/style.css */
@font-face {
font-family: 'Inter';
src: url('../fonts/Inter.woff2') format('woff2');
}
body {
font-family: 'Inter', system-ui, sans-serif;
}
Because the font file is inside your app, it works offline. That is better than loading fonts from a website.
The Content Security Policy and your assets
Your page has a Content Security Policy (CSP) from Your First Electron App:
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'"
/>
default-src 'self' means “load everything only from my own app files”. So stylesheets, images and fonts from your folders work. But a few things are blocked, and the DevTools Console tells you about each one.
Inline styles are blocked
A <style> block or a style="..." attribute in your HTML is ignored, and the Console shows:
Applying inline style violates the following Content Security Policy directive 'default-src 'self''.
Fix: move the styles into your .css file and use classes.
Images from the internet are blocked
An <img src="https://..."> does not load:
Loading the image 'https://example.com/photo.png' violates the following Content Security Policy directive: "default-src 'self'".
If your app really needs online images, allow just that kind of file from just that site, using the img-src rule:
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'; img-src 'self' https://images.example.com"
/>
Each rule (like img-src) covers one kind of file. Rules you do not list fall back to default-src. Allow as little as you can. Content Security Policy covers every rule in detail.
More than one page
Need a settings screen? Create settings.html next to index.html and load it:
// main.js
win.loadFile('settings.html');
Normal links between pages also work inside the same window:
<!-- index.html -->
<a href="settings.html">Settings</a>
Passing information to a page
loadFile accepts an options object with query and hash, which are added to the page’s URL:
// main.js
win.loadFile('index.html', { query: { page: 'settings' }, hash: 'top' });
The page’s URL becomes index.html?page=settings#top. The page can read it with URLSearchParams:
// renderer.js
const params = new URLSearchParams(location.search);
console.log(params.get('page')); // "settings"
console.log(location.hash); // "#top"
This is a simple way to open one HTML file in different modes, for example one window per document. For anything more than a few small values, use IPC (see IPC Part 2: Asking Questions with invoke and handle).
When a page fails to load
loadFile and loadURL return a Promise that is rejected if the page cannot load. Catch it so problems are easy to spot:
// main.js
win.loadFile('setings.html').catch((error) => {
console.error('Could not load page:', error.code);
});
Could not load page: ERR_FILE_NOT_FOUND
Electron also prints a warning in the terminal:
electron: Failed to load URL: file:///C:/Users/asha/my-first-app/setings.html with error: ERR_FILE_NOT_FOUND
The fix here is a typo: setings should be settings.
You can also watch every load of a window with two webContents events:
// main.js
win.webContents.on('did-finish-load', () => {
console.log('Page loaded:', win.webContents.getURL());
});
win.webContents.on('did-fail-load', (event, errorCode, errorDescription, url) => {
console.error('Failed to load', url, errorDescription);
});
These fire for every page load in that window, including when the user clicks a link or reloads.
A note for Electron Forge projects
In a Forge project with Vite (see Starting a Project with Electron Forge), the page is served by Vite during development, so a few things differ:
- CSS is usually imported from JavaScript:
import './index.css';insrc/renderer.js. Vite adds it to the page for you. - The main process loads
MAIN_WINDOW_VITE_DEV_SERVER_URLwithloadURLin development, and the builtindex.htmlwithloadFilewhen packaged.
Relative paths and the CSP work the same way as in this lesson.
Common mistakes
Starting a path with a slash
<!-- index.html: don't do this -->
<img src="/images/logo.svg" alt="Logo" />
On a website, /images/logo.svg means “from the site’s root”. But your page is a file:// URL, so the root is the root of the disk: file:///C:/images/logo.svg on Windows, or file:///images/logo.svg on macOS and Linux. The image is not there, so nothing shows. Use images/logo.svg with no leading slash.
Making CSS paths relative to the HTML file
In styles/style.css, url('images/bg.png') looks for styles/images/bg.png. CSS paths start from the CSS file’s own folder, so write url('../images/bg.png').
Using inline styles and wondering why they are ignored
With the safe CSP, <style> blocks and style="..." attributes are blocked. Move them into your stylesheet.
Wrong letter case in a file name
Logo.SVG and logo.svg are the same file on Windows and usually on macOS, but different files on Linux. If an image works on your computer but not on a friend’s Linux machine, check the capital letters.
Quick recap
win.loadFile('index.html')loads a page from your app folder.win.loadURL(url)loads a web address; be careful with remote content.- Pages from
loadFilehavefile://URLs. - Link CSS with
<link rel="stylesheet" href="styles/style.css">and images with<img src="images/logo.svg">. - HTML paths are relative to the HTML file. CSS paths are relative to the CSS file. Never start a path with
/. - Keep fonts and images inside your app so they work offline.
- The CSP
default-src 'self'allows your own files but blocks inline styles and remote images. Loosen it rule by rule, likeimg-src. loadFiletakesqueryandhashoptions. Read them in the page withURLSearchParams.- Catch the Promise from
loadFile, or usedid-fail-load, to see loading errors likeERR_FILE_NOT_FOUND.
// main.js: load pages safely and report problems
const { app, BrowserWindow } = require('electron');
const path = require('path');
// index.html head (paths are relative to the HTML file, no leading slash):
// <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
// <link rel="stylesheet" href="styles/style.css">
// index.html body:
// <img src="images/logo.svg" alt="App logo">
// <script src="renderer.js"></script>
// styles/style.css (paths are relative to the CSS file):
// body { background-image: url('../images/background.png'); }
function createWindow(page = 'home') {
const win = new BrowserWindow({
width: 900,
height: 650,
backgroundColor: '#1e1e1e', // matches the CSS background: no white flash
webPreferences: { preload: path.join(__dirname, 'preload.js') },
});
// --- Log every successful or failed load in this window
win.webContents.on('did-finish-load', () => {
console.log('Loaded:', win.webContents.getURL());
});
win.webContents.on('did-fail-load', (event, errorCode, errorDescription, url) => {
console.error('Failed:', url, errorDescription);
});
// --- Same HTML file, different mode: the page reads ?page=... with URLSearchParams
win.loadFile('index.html', { query: { page } }).catch((error) => {
console.error('Could not load index.html:', error.code); // e.g. ERR_FILE_NOT_FOUND
});
return win;
}
app.whenReady().then(() => {
createWindow('home');
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow('home');
});
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit();
});
// renderer.js: const page = new URLSearchParams(location.search).get('page');
Was this explanation helpful?
Your feedback helps us improve the content for everyone.
Thank you for your feedback!
We appreciate you helping us improve.
What could we improve? (Optional)
Read aloud
Ready