Day for Electron developers
Electron renders a web interface using Chromium and Node.js. Day compiles Rust code into an app that uses native toolkit controls. Moving between them changes UI composition, process communication, dependencies, and packaging; this guide compares those areas with code examples.
Electron terms in Day
| Electron | Day |
|---|---|
BrowserWindow | day::launch for the main window, open_window for more |
| main process / renderer / IPC | one process; a platform call is a function call |
preload.js + contextBridge | not needed; the app is one process and calls platform APIs directly |
| HTML + CSS (+ React/Vue) | pieces + signals + modifiers |
Menu.buildFromTemplate | app_menu — native menus in both, declared in Rust |
new Notification({...}) | day-part-local-notify |
electron-store, localStorage | day::prefs (guide) |
Node fs | day-part-fs, or std::fs — it’s ordinary Rust |
fetch / Node net | day-part-http — each platform’s networking stack |
<webview> / WebContentsView | day-piece-webview — the system webview |
| native modules (N-API, node-gyp) | parts and pieces, compiled with the app |
nodeIntegration / contextIsolation hardening | not applicable; the UI is compiled Rust driving native widgets, so there is nothing to isolate |
electron-builder / Forge | day pack -p <target> |
autoUpdater (Squirrel) | none yet; updates ship through stores and installers |
| Playwright / WebdriverIO | dayscript, the same script on every target |
The same counter in both
<!-- index.html, loaded by a BrowserWindow -->
<div style="display:flex; flex-direction:column;
gap:12px; padding:16px">
<span id="count">0 clicks</span>
<button id="inc">+</button>
</div>
<script>
let count = 0;
const label = document.getElementById('count');
document.getElementById('inc').onclick = () => {
count += 1;
label.textContent = `${count} clicks`;
};
</script>use day::prelude::*;
fn counter() -> impl Piece {
let count = Signal::new(0i64);
column((
label(move || format!("{} clicks", count.get())),
button("+").action(move || count.update(|c| *c += 1)),
))
.spacing(12.0)
.padding(16.0)
}The parts match (a container, a text node, a click handler). On the Day side, label is a native
text widget, button a native button, and the closure inside label is a binding: when count
changes, Day re-runs that one closure and calls one native setter. There is no document tree to
restyle or reconcile. If your Electron app is React inside the renderer, one more thing changes: Day
never re-runs your component function, so hooks rules and dependency arrays have no equivalent.
One process
In Electron a capability lives in the main process, so the renderer reaches it through
ipcMain.handle, a preload bridge, and an invoke. In Day the capability is a crate and you
call it.
// main.js
ipcMain.handle('open-notes', async () => {
const { canceled, filePaths } = await dialog.showOpenDialog({
filters: [{ name: 'Text', extensions: ['txt', 'md'] }],
});
if (canceled) return null;
return fs.promises.readFile(filePaths[0], 'utf8');
});
// preload.js
contextBridge.exposeInMainWorld('api', {
openNotes: () => ipcRenderer.invoke('open-notes'),
});
// renderer.js
const text = await window.api.openNotes();
if (text !== null) editor.value = text;// anywhere in the app — same process, same language
button("Open…").action(move || {
day::task(async move {
if let Some(file) = open_file()
.filter("Text", &["txt", "md"])
.await
{
if let Ok(text) = file.read_to_string() {
editor.set(text);
}
}
});
})The three-file round trip on the left is the cost of the process boundary: every capability needs a
handler, a bridge entry, and a serializable payload. On the right the dialog is open_file(), the
battery is day_part_battery::status(), the clipboard is day_part_clipboard::get_text(). All are
plain functions, typed end to end. When a capability needs platform code Day doesn’t ship (your
N-API module case), you write a part: the platform halves live in one crate and calls
into it stay ordinary Rust.
Native form controls
Form controls bind two ways, and each one is the platform’s control, which supplies accessibility, keyboard traversal, right-to-left mirroring, and the user’s text size on its own:
let name = Signal::new(String::new());
let volume = Signal::new(40.0);
let subscribed = Signal::new(false);
let size = Signal::new(0usize);
column((
text_field(name).placeholder("Your name"),
slider(volume).range(0.0..=100.0),
toggle(subscribed),
picker(["Small", "Medium", "Large"], size).segmented(),
))
.spacing(12.0)
For long lists, list hands your rows to the platform’s recycling
list widget (NSTableView-family, RecyclerView), so ten thousand rows build only the visible
cells. Styling is modifier-based (.padding, .background, .corner_radius, semantic
Font::Title), and Styling lists what you can restyle and what stays native:
you adjust spacing, color, and typography, and the control chrome stays the platform’s. If the
design brief is a custom skin on every pixel, a web renderer serves that better. For an app
that should look and behave like standard platform controls, Day keeps the controls native.
Windows, menus, and the chrome
Electron’s menus are already native, and Menu.buildFromTemplate is the part of Electron
closest to Day’s model. Day gives windows and toolbars the same treatment: windows are native
windows you open directly, the Settings window is a one-call convention, and toolbars live in
the title-bar chrome.
const win = new BrowserWindow({
width: 520, height: 420,
webPreferences: { preload: PRELOAD },
});
win.loadFile('settings.html');
Menu.setApplicationMenu(Menu.buildFromTemplate([
{ role: 'appMenu' },
{ label: 'File', submenu: [
{ label: 'New Note', accelerator: 'CmdOrCtrl+N',
click: newNote },
]},
]));day::open_window(
"settings",
WindowOptions { title: "Settings".into(),
size: Size::new(520.0, 420.0),
..Default::default() },
WindowKind::Preferences,
settings_page,
);
app_menu((
sub_menu("File", (
menu_item("New Note").key("n").action(new_note),
menu_role(MenuRole::Quit),
)),
));register_preferences_with goes further than the sample: registering a Settings page once puts
the standard Settings… item (⌘, on macOS) in the right menu on every desktop, and
platforms without windows (the same code on an iPhone) present a fullscreen cover instead.
The desktop guide walks all three subsystems.
Binary size and process model
A Day app links the toolkit the OS already has, so the binary stays small: the Day Showcase (26
pages exercising every widget, chart, and part in this documentation) packs to a 5.7 MB notarized
.dmg. Remote content goes through day-piece-webview, which hosts the
system web view (WKWebView, WebView2, WebKitGTK).
Reusing web code
Two paths keep existing web code useful. web_view(url_signal) embeds existing web screens in the
system web view while you migrate page by page. And the whole app also runs as a website: day build -p web-dom compiles the same crate to WebAssembly driving DOM elements. For a team that keeps
writing TypeScript, day-lite runs JS/TS miniapps on top of Day’s native
pieces; it hosts miniapps and is not a way to write the app itself.
Package configuration
// package.json
{
"name": "field-notes",
"version": "1.2.0",
"main": "main.js",
"devDependencies": {
"electron": "^43.0.0",
"electron-builder": "^26.0.0"
}
}
// plus electron-builder.yml: appId, mac.category,
// win.target, linux.target, files, asar…# Cargo.toml — the package
[package]
name = "field-notes"
version = "1.2.0"
edition = "2024"
[dependencies]
day = { git = "https://github.com/daybrite/day.git" }
# Day.toml — the app manifest
[app]
id = "dev.example.fieldnotes"
title = "Field Notes"
build = 34
targets = ["macos-appkit", "windows-xaml", "linux-gtk",
"ios-uikit", "android-mdc", "web-dom"]Day.toml plays electron-builder.yml’s role (identity, targets, signing, permissions).
Per-platform values are overrides in the same file ([app.macos-appkit]), and the same file
covers the mobile targets.
Project structure
field-notes/
├── package.json
├── electron-builder.yml
├── main.js # the Node side
├── preload.js # the bridge
├── src/ # the web app
│ ├── index.html
│ ├── renderer.js
│ └── styles.css
└── node_modules/field-notes/
├── Cargo.toml
├── Day.toml
├── src/
│ ├── lib.rs # the app: pieces, signals, routes
│ └── main.rs # desktop entry point
├── resource/ # assets/ images/ vectors/ fonts/ icons/ locales/
├── dayscript/ # UI test flows
├── platform/ # thin mobile hosts (no app logic)
└── build/day/ # everything generatedThe main, preload, and renderer files become one src/ directory, because the app is one
process. Resources are staged natively per platform and referenced through generated constants
(image(res::images::logo)), so a renamed file is a compile error. Localization uses Fluent
catalogs compiled to typed functions (Localization).
Building and shipping
| Task | Electron | Day |
|---|---|---|
| run in dev | electron . | day launch -p macos-appkit |
| apply changes | reload the window | day relaunch --all-running |
| macOS artifact | electron-builder → .dmg, notarize via config | day pack -p macos-appkit → notarized, stapled .dmg |
| Windows artifact | NSIS / Squirrel | day pack -p windows-xaml → .msix + NSIS installer |
| Linux artifact | AppImage / deb / snap | day pack -p linux-gtk → .flatpak + .appimage |
| mobile | — | day pack -p ios-uikit / -p android-mdc → .ipa, .apk + .aab |
| web | separate web build of the renderer code | day build -p web-dom — the same crate |
| auto-update | autoUpdater + an update server | none yet; stores and installers carry updates |
| UI tests | Playwright per OS | one dayscript, every target, in CI with screenshots |
| supply chain | your Chromium/Node rebuild cadence | reproducible builds with SBOM + buildinfo sidecars per artifact (details) |
Maturity differs by target, and Platform support lists what each target has
today: every (OS, toolkit) pair carries a support tier, from
Tier 1 (thoroughly tested, with shipping apps on it) down to
Tier 4 (development combinations nobody ships).
What you give up
- HTML, CSS, and the npm UI ecosystem are replaced by a small widget vocabulary styled through
modifiers; custom visuals are drawn with
canvasor a piece. - DevTools have no equivalent; debugging is Rust tooling plus dayscript assertions and screenshots.
- Hot reload becomes an incremental compile and relaunch, seconds on desktop, with a script to put you back on the screen you were editing.
- Electron’s
autoUpdaterhas no Day equivalent yet; updates travel through the stores and installersday packproduces. - Day has no tray or global-shortcut API yet. Menus carry per-item accelerators, but there is no general key-event surface.
- Your app will look like a Mac app on macOS and a GTK app on GNOME. If your brand requires one rendering on every OS, Electron’s single renderer serves it better.
- The team writes Rust instead of JavaScript. Budget ramp-up time; the compiler catches at build time much of what you currently catch in DevTools.
Where to go next
- Getting started — install, scaffold, first launch.
- API tour — examples of common UI components and patterns.
- Menus, toolbars, and windows — the desktop-app chrome in one guide.
- Why Day compares the approaches and says when a web renderer fits better.