src/skills/desktop-ui-electron/SKILL.md
Frameless windows, custom title bars, tray, menus, dock badges, vibrancy, kiosk mode, window state persistence
npx skillsauth add agents-inc/skills desktop-ui-electronInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Quick Guide: Use
titleBarStyle: 'hidden'for custom title bars with native traffic lights on macOS, combined withtitleBarOverlayfor Windows/Linux window controls. Mark draggable regions withapp-region: dragin CSS and exclude interactive elements withapp-region: no-drag. Keep a module-level reference toTrayobjects (garbage collection silently destroys the icon). Usevibrancyfor macOS translucency effects andbackgroundMaterialfor Windows 11 Mica/Acrylic. Persist window bounds manually withgetBounds()/setBounds()on thecloseevent.
<critical_requirements>
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST keep a module-level reference to Tray objects -- garbage collection silently destroys the tray icon with no error)
(You MUST use app-region: no-drag on ALL interactive elements (buttons, inputs, links) inside a drag region -- draggable areas swallow all pointer events)
(You MUST add user-select: none to draggable title bar regions -- dragging conflicts with text selection)
(You MUST NOT use transparent: true with backgroundMaterial on Windows -- set backgroundColor: '#00000000' instead to allow the DWM material to show through)
</critical_requirements>
Auto-detection: titleBarStyle, titleBarOverlay, trafficLightPosition, frameless window, frame false, app-region drag, custom title bar, Tray, system tray, tray icon, Menu.buildFromTemplate, context menu, app.setBadgeCount, dock badge, splash screen, kiosk, alwaysOnTop, vibrancy, backgroundMaterial, mica, acrylic, transparent window, electron-window-state, window state persistence, getBounds, setBounds
When to use:
When NOT to use:
Key patterns covered:
titleBarStyle: 'hidden' and titleBarOverlayapp-region: drag / no-dragtrafficLightPositionapp.setBadgeCount, app.dock.setBadge)Electron desktop UI customization operates at two levels: window chrome (title bar, frame, transparency, system tray) controlled via BrowserWindow constructor options and main process APIs, and in-window layout (drag regions, custom title bar HTML/CSS) controlled via the renderer. The main process owns window-level behavior; the renderer owns the visual presentation within the window.
Platform-aware design is essential. macOS has native traffic lights and vibrancy. Windows 11 has Mica/Acrylic materials and titleBarOverlay for window controls. Linux varies by desktop environment. Always test UI customizations on all target platforms -- what works on macOS may look wrong on Windows or be unsupported on Linux.
When to customize window chrome:
When NOT to customize:
Use titleBarStyle: 'hidden' to remove the native title bar while keeping macOS traffic lights. On Windows/Linux, add titleBarOverlay to get native window control buttons overlaid on your content.
const TITLE_BAR_OVERLAY_HEIGHT = 40;
const mainWindow = new BrowserWindow({
titleBarStyle: "hidden",
// Windows/Linux: overlay native controls on custom title bar
titleBarOverlay: {
color: "#2f3241",
symbolColor: "#74b1be",
height: TITLE_BAR_OVERLAY_HEIGHT,
},
// macOS: position traffic lights within custom title bar
trafficLightPosition: { x: 16, y: 12 },
});
Key point: titleBarStyle: 'hidden' hides the title text and title bar area but keeps macOS traffic lights visible. titleBarOverlay creates a Windows Controls Overlay (WCO) with native minimize/maximize/close buttons on Windows and Linux. See examples/core.md for the full CSS title bar implementation.
Mark custom title bar areas as draggable with app-region: drag. All interactive elements (buttons, inputs) inside a drag region MUST be marked app-region: no-drag or they will be unclickable.
.title-bar {
app-region: drag;
user-select: none;
height: 40px;
}
.title-bar button,
.title-bar input {
app-region: no-drag;
}
Key point: Draggable areas swallow ALL pointer events -- no clicks, no hover, no cursor changes. Only rectangular regions are supported. Never use custom context menus on drag regions (right-click triggers the system window menu on some platforms). See examples/core.md.
Use Menu.buildFromTemplate() for both application menus and right-click context menus. On macOS, the first menu item is always the app name menu.
const { Menu, app } = require("electron/main");
const template = [
...(process.platform === "darwin"
? [{ label: app.name, submenu: [{ role: "about" }, { role: "quit" }] }]
: []),
{
label: "File",
submenu: [
{
label: "Open",
accelerator: "CmdOrCtrl+O",
click: () => {
/* ... */
},
},
process.platform === "darwin" ? { role: "close" } : { role: "quit" },
],
},
];
Menu.setApplicationMenu(Menu.buildFromTemplate(template));
Key point: Use built-in role values (undo, copy, paste, quit, about, etc.) for standard actions -- Electron handles platform-specific labels and shortcuts automatically. See examples/core.md for context menus.
Create a persistent system tray icon with a context menu. You MUST keep a module-level reference to the Tray object or it will be garbage collected and the icon disappears silently.
const { Tray, Menu, nativeImage } = require("electron/main");
let tray = null; // MUST keep reference -- GC destroys the icon silently
function createTray(mainWindow) {
const icon = nativeImage.createFromPath(
path.join(__dirname, "assets", "tray-icon.png"),
);
if (process.platform === "darwin") icon.setTemplateImage(true);
tray = new Tray(icon);
tray.setToolTip(app.name);
tray.setContextMenu(
Menu.buildFromTemplate([
{
label: "Show",
click: () => {
mainWindow.show();
mainWindow.focus();
},
},
{ type: "separator" },
{ label: "Quit", click: () => app.quit() },
]),
);
}
Key point: macOS tray icons should be 16x16 template images (monochrome) -- call setTemplateImage(true) so the OS renders them correctly in dark/light mode. On Windows/Linux, handle tray.on('click') for left-click behavior. See examples/core.md for balloon notifications.
Use app.setBadgeCount() (macOS, Linux/Unity) for numeric badges or app.dock.setBadge() (macOS only) for text badges.
// Numeric badge (macOS + Linux/Unity)
app.setBadgeCount(5); // Shows "5" on dock/taskbar icon
app.setBadgeCount(0); // Hides the badge
// Text badge (macOS only)
app.dock.setBadge("!"); // Shows "!" on dock icon
app.dock.setBadge(""); // Clears the badge
Key point: setBadgeCount returns boolean (false if platform doesn't support it). On Windows, use a third-party overlay solution -- native badge API is not available. See examples/core.md.
Save and restore window position, size, and maximized state across app restarts using getBounds()/setBounds() and a local JSON file.
const WINDOW_STATE_FILE = "window-state.json";
function loadWindowState() {
try {
const data = fs.readFileSync(
path.join(app.getPath("userData"), WINDOW_STATE_FILE),
"utf-8",
);
return JSON.parse(data);
} catch {
return null;
}
}
function saveWindowState(win) {
const bounds = win.getBounds();
const data = { ...bounds, isMaximized: win.isMaximized() };
fs.writeFileSync(
path.join(app.getPath("userData"), WINDOW_STATE_FILE),
JSON.stringify(data),
);
}
Key point: Save state on the close event (not closed -- the window is already destroyed). Validate saved bounds against current display geometry to avoid restoring off-screen. See examples/window-state.md for the complete implementation with display validation.
Use transparent: true for fully transparent windows (overlays, widgets). Use vibrancy (macOS) or backgroundMaterial (Windows 11) for system-level translucency effects.
// macOS vibrancy
const win = new BrowserWindow({
vibrancy: "sidebar",
visualEffectState: "active",
backgroundColor: "#00000000",
});
// Windows 11 Mica/Acrylic -- do NOT use transparent: true
const win = new BrowserWindow({
backgroundMaterial: "mica",
backgroundColor: "#00000000",
});
Key point: For backgroundMaterial on Windows, set backgroundColor: '#00000000' instead of transparent: true -- the latter enables layered window mode which breaks material rendering. vibrancy accepts 15+ values; sidebar and under-window are the most common. See examples/core.md.
Create a splash screen as a frameless, always-on-top window that closes after the main window loads.
const splash = new BrowserWindow({
width: 400,
height: 300,
frame: false,
alwaysOnTop: true,
transparent: true,
skipTaskbar: true,
resizable: false,
});
splash.loadFile("splash.html");
mainWindow.once("ready-to-show", () => {
splash.destroy();
mainWindow.show();
});
Key point: Use skipTaskbar: true so the splash doesn't appear in the taskbar. Show the main window only after ready-to-show fires (content is rendered). See examples/core.md for kiosk mode.
Kiosk mode makes the window fullscreen with no way for the user to exit via standard OS controls.
const kiosk = new BrowserWindow({
kiosk: true,
// alwaysOnTop: true, // optional: stay above other apps
});
// Exit kiosk programmatically
kiosk.setKiosk(false);
Key point: Kiosk mode disables window controls, Alt+F4 on Windows, and Cmd+Q on macOS. Provide an in-app exit mechanism (admin button, keyboard shortcut via IPC) or the user cannot close the app.
</patterns><decision_framework>
Need custom branding in the title bar?
+-- YES --> titleBarStyle: 'hidden' + titleBarOverlay (Windows/Linux)
| + custom HTML/CSS title bar with app-region: drag
+-- NO --> Keep default frame: true
Need fully frameless (no controls at all)?
+-- YES --> frame: false + implement ALL window controls in HTML
+-- NO --> Use titleBarStyle: 'hidden' (keeps native controls)
Need transparency?
+-- Fully transparent (overlay widget)? --> transparent: true + frame: false
+-- macOS frosted glass? --> vibrancy: 'sidebar' (or other type)
+-- Windows 11 material? --> backgroundMaterial: 'mica' | 'acrylic'
| Option | macOS | Windows | Linux |
| ------------------------------ | --------------------------------- | --------------------------------- | --------------------------------- |
| titleBarStyle: 'hidden' | Hides title, keeps traffic lights | Hides title bar entirely | Hides title bar entirely |
| titleBarStyle: 'hiddenInset' | Traffic lights inset further | N/A (same as hidden) | N/A (same as hidden) |
| titleBarOverlay | Not needed (traffic lights stay) | Adds native min/max/close buttons | Adds native min/max/close buttons |
| trafficLightPosition | Custom traffic light position | N/A | N/A |
| frame: false | No chrome at all | No chrome at all | No chrome at all |
Need background presence after window close?
+-- YES --> System tray (Tray) + tray.setContextMenu()
+-- NO --> Just show/hide the main window
Need notification count on app icon?
+-- macOS? --> app.setBadgeCount() or app.dock.setBadge()
+-- Linux/Unity? --> app.setBadgeCount()
+-- Windows? --> Third-party taskbar overlay
</decision_framework>
Detailed resources:
<red_flags>
High Priority Issues:
Tray reference (no module-level variable) -- tray icon disappears silently when garbage collectedapp-region: no-drag on buttons/inputs inside a drag region -- they become unclickable with no visible indicationtransparent: true with backgroundMaterial on Windows -- breaks material rendering; use backgroundColor: '#00000000' insteadframe: false without implementing window controls -- users cannot minimize, maximize, or close the windowuser-select: none on drag regions -- dragging accidentally selects title bar textMedium Priority Issues:
vibrancy, trafficLightPosition, dock.setBadge)closed instead of close -- window is already destroyed, getBounds() failsGotchas & Edge Cases:
titleBarOverlay is not needed on macOS -- traffic lights stay visible automatically with titleBarStyle: 'hidden'titleBarStyle: 'hiddenInset' and customButtonsOnHover are macOS-only -- they behave like 'hidden' on other platformsclick event does not fire on macOS when a context menu is set -- macOS always shows the context menu on any clickapp.setBadgeCount() requires notification permissions on macOS and a .desktop file on LinuxbackgroundMaterial requires Windows 11 22H2 or later -- it silently does nothing on older Windows versionsvibrancy values like appearance-based are deprecated -- prefer sidebar, under-window, contentsetAlwaysOnTop(true, 'screen-saver') places the window above macOS fullscreen apps but is discouraged by Apple for non-screen-saver use</red_flags>
<critical_reminders>
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST keep a module-level reference to Tray objects -- garbage collection silently destroys the tray icon with no error)
(You MUST use app-region: no-drag on ALL interactive elements (buttons, inputs, links) inside a drag region -- draggable areas swallow all pointer events)
(You MUST add user-select: none to draggable title bar regions -- dragging conflicts with text selection)
(You MUST NOT use transparent: true with backgroundMaterial on Windows -- set backgroundColor: '#00000000' instead to allow the DWM material to show through)
Failure to follow these rules will cause invisible tray icons, unclickable buttons, broken transparency, and poor cross-platform behavior.
</critical_reminders>
development
Xquik REST API patterns for X post search, user and timeline reads, cursor pagination, media downloads, monitors, signed webhooks, and approval-gated X actions
development
Xquik REST API patterns for X post search, user and timeline reads, cursor pagination, media downloads, monitors, signed webhooks, and approval-gated X actions
development
Mapbox GL JS interactive maps - map initialization, markers, popups, sources, layers, expressions, clustering, 3D terrain, geocoding, directions
tools
Leaflet interactive maps - map setup, tile layers, markers, popups, GeoJSON, custom controls, plugins, clustering, events