Steam Overlay Integration for Electron

August 15, 2026 · View on GitHub

This guide explains how to add Steam overlay support (Shift+Tab) to Electron applications using native rendering across all platforms.

Status: Working - This feature has been tested on macOS (Metal), Windows (OpenGL), and Linux (OpenGL 3.3 on Steam Deck Desktop Mode/SteamOS).

Overview

The Steam overlay integration allows Electron applications to display the Steam overlay by:

  1. Creating a native graphics window (Metal on macOS, OpenGL on Windows and Linux)
  2. Capturing Electron's content and rendering it to the native surface
  3. Allowing Steam to inject its overlay renderer

This enables full Steam overlay functionality (Shift+Tab, friends list, achievements, etc.) in Electron apps.

Platform Support

PlatformRendererStatus
macOSMetalTested
WindowsOpenGLTested
LinuxOpenGL 3.3 + X11Tested (Steam Deck Desktop Mode)

System Requirements

PlatformMinimum Version
macOS10.15+ (Catalina)
WindowsWindows 10+
LinuxX11 with OpenGL 3.3+
SteamOSSteam Deck Desktop Mode

Quick Start

1. Install Dependencies

npm install steamworks-ffi-node
# or
yarn add steamworks-ffi-node

2. Build Native Module (Optional)

The native overlay module is built automatically during installation if prebuilds aren't available for your platform.

To manually rebuild:

npm run build:native

3. Basic Integration

import { app, BrowserWindow } from "electron";
import SteamworksSDK from "steamworks-ffi-node";

const steam = SteamworksSDK.getInstance();

// Initialize Steam
if (!steam.init({ appId: 480 })) {
  console.error("Failed to initialize Steam");
  app.quit();
}

// Run callbacks periodically
setInterval(() => steam.runCallbacks(), 1000);

app.whenReady().then(() => {
  const win = new BrowserWindow({
    width: 1280,
    height: 720,
    webPreferences: {
      nodeIntegration: true,
      contextIsolation: false,
    },
  });

  // Add Steam overlay - ONE LINE!
  if (steam.isOverlayAvailable()) {
    steam.addElectronSteamOverlay(win);
  }

  win.loadFile("index.html");
});

app.on("before-quit", () => {
  steam.shutdown();
});

API Reference

addElectronSteamOverlay(browserWindow, options?)

Adds Steam overlay support to an Electron BrowserWindow.

Parameters:

  • browserWindow - The Electron BrowserWindow
  • options (optional):
    • title?: string - Window title (default: "Electron Steam App")
    • fps?: number - Frame rate (default: 60)
    • vsync?: boolean - Enable VSync (default: true)

Returns: boolean - True if overlay was successfully added

Example:

const success = steam.addElectronSteamOverlay(win, {
  title: "My Steam Game",
  fps: 60,
  vsync: true,
});

if (success) {
  console.log("Steam overlay enabled!");
} else {
  console.log("Failed to enable overlay");
}

isOverlayAvailable()

Checks if Steam overlay is available on the current system.

Returns: boolean - True if overlay can be used

Example:

if (steam.isOverlayAvailable()) {
  steam.addElectronSteamOverlay(win);
} else {
  console.log("Steam overlay not available on this platform");
}

Platform-Specific Notes

macOS (Metal)

The macOS implementation uses Metal for rendering:

  • Creates a borderless MTKView window
  • Uses a custom NSWindow subclass to prevent focus stealing
  • Syncs with Electron window position, size, minimize/restore, and focus states

Required Entitlements (entitlements.mac.plist):

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <!-- Required for Steam overlay injection -->
  <key>com.apple.security.cs.allow-dyld-environment-variables</key>
  <true/>
  <key>com.apple.security.cs.disable-library-validation</key>
  <true/>

  <!-- Standard Electron entitlements -->
  <key>com.apple.security.cs.allow-jit</key>
  <true/>
  <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
  <true/>
</dict>
</plist>

Windows (OpenGL)

The Windows implementation uses OpenGL:

  • Creates a borderless window with WS_EX_TOPMOST and WS_EX_NOACTIVATE
  • Uses WGL for OpenGL context creation
  • Click-through input handling via WM_NCHITTEST returning HTTRANSPARENT
  • DPI-aware coordinate scaling for high-DPI displays

Requirements:

  • OpenGL capable GPU
  • Windows 10 or later recommended

Linux (OpenGL)

The Linux implementation uses OpenGL 3.3 with X11:

  • Creates an X11 window with override redirect
  • Uses GLX for OpenGL context
  • Supports all major distributions (SteamOS, Ubuntu, Arch, Mint, Fedora, etc.)
  • Tested on: Steam Deck Desktop Mode (SteamOS)

Requirements:

  • X11 display server (Wayland not yet supported)
  • OpenGL 3.3+ capable driver

Electron Packaging

⚠️ The native overlay module (steam-overlay.node) must be outside the .asar archive — require() cannot load native addons from inside .asar. The recommended approach for games is to unpack the entire library with a single rule.

⚠️ Since koffi 3.x, its native binary ships as a separate platform-specific package (@koromix/koffi-<platform>-<arch>, an optionalDependency of koffi — npm installs only the one matching your OS/arch). It is a sibling of steamworks-ffi-node in node_modules, not nested inside it, so a rule that only unpacks steamworks-ffi-node/** will miss it, leaving koffi's native binary trapped inside .asar and causing Error: Cannot find the native Koffi module; did you bundle it correctly? at runtime. Unpack koffi and @koromix explicitly, as shown below.

electron-builder

Add to your package.json (or electron-builder.json):

{
  "build": {
    "asarUnpack": [
      "node_modules/steamworks-ffi-node/**",
      "node_modules/koffi/**",
      "node_modules/@koromix/**",
      "steamworks_sdk/redistributable_bin/**"
    ],
    "mac": {
      "hardenedRuntime": true,
      "gatekeeperAssess": false,
      "entitlements": "buildconfig/entitlements.mac.plist",
      "entitlementsInherit": "buildconfig/entitlements.mac.plist"
    }
  }
}

electron-forge

// forge.config.js
module.exports = {
  packagerConfig: {
    asar: {
      unpack: "*(steamworks_sdk/**|**/steamworks-ffi-node/**|**/koffi/**|**/@koromix/**)"
    }
  }
};

macOS universal (x64 + arm64) builds

koffi 3.x ships its native binary as a separate per-architecture package (npm only installs the one matching your build machine). If you're packaging a macOS universal binary, run this once before electron-builder/electron-forge so both architectures' koffi binaries are present to bundle:

npx steamworks-fetch-universal-koffi

Complete Example

import { app, BrowserWindow } from "electron";
import SteamworksSDK from "steamworks-ffi-node";

const steam = SteamworksSDK.getInstance();
let mainWindow: BrowserWindow | null = null;

// Initialize Steam before app ready
const STEAM_APP_ID = 480; // Replace with your Steam App ID

if (!steam.init({ appId: STEAM_APP_ID })) {
  console.error("Failed to initialize Steam - is Steam running?");
  app.quit();
}

// Run Steam callbacks periodically
setInterval(() => {
  steam.runCallbacks();
}, 1000);

// Create window when app is ready
app.whenReady().then(() => {
  mainWindow = new BrowserWindow({
    width: 1280,
    height: 720,
    title: "My Steam Game",
    webPreferences: {
      nodeIntegration: true,
      contextIsolation: false,
    },
  });

  // Check if Steam overlay is available
  if (steam.isOverlayAvailable()) {
    // Wait for content to load before enabling overlay
    mainWindow.webContents.once("did-finish-load", () => {
      setTimeout(() => {
        const success = steam.addElectronSteamOverlay(mainWindow!, {
          title: "My Steam Game",
          fps: 60,
          vsync: true,
        });

        if (success) {
          console.log("Steam overlay enabled! Press Shift+Tab to open.");
        } else {
          console.error("Failed to enable Steam overlay");
        }
      }, 500); // Small delay to ensure window is fully rendered
    });
  } else {
    console.warn("Steam overlay not available on this system");
  }

  mainWindow.loadFile("index.html");

  mainWindow.on("closed", () => {
    mainWindow = null;
  });
});

// Quit when all windows are closed
app.on("window-all-closed", () => {
  if (process.platform !== "darwin") {
    app.quit();
  }
});

// Clean up Steam on quit
app.on("before-quit", () => {
  console.log("Shutting down Steam...");
  steam.shutdown();
});

// macOS: Re-create window when dock icon is clicked
app.on("activate", () => {
  if (mainWindow === null) {
    // Re-create window logic here
  }
});

Testing

1. Add to Steam

  1. Open Steam
  2. Click "Games" → "Add a Non-Steam Game to My Library"
  3. Browse to your Electron app
  4. Add it to Steam

2. Restart Steam (optional, in some cases it could be an issue on macOS)

CRITICAL: After adding your app, you must restart Steam for the overlay to work!

3. Launch from Steam

Launch your app from Steam (not directly). Press Shift+Tab to open the overlay.

Troubleshooting

Overlay doesn't appear

  1. Restart Steam - This is the #1 issue!
  2. Check Steam Settings → In-Game → "Enable Steam Overlay" is checked
  3. Verify you're launching from Steam, not directly
  4. Check console for errors
  5. Clean steam download cache

macOS-specific issues

  • Check entitlements: codesign -d --entitlements :- YourApp.app
  • Verify code signing: codesign -vv YourApp.app
  • Install Xcode Command Line Tools: xcode-select --install

Windows-specific issues

  • Ensure OpenGL is available
  • Check Windows Event Viewer for graphics errors
  • Update graphics drivers
  • For high-DPI displays, coordinates are automatically scaled

Linux-specific issues

  • Verify X11 is running (Wayland not supported)
  • Check OpenGL version: glxinfo | grep "OpenGL version"
  • Install required libraries: libX11, libGL, libGLX

Build fails

  1. Install build tools for your platform:
    • macOS: xcode-select --install
    • Windows: Visual Studio Build Tools
    • Linux: build-essential, libx11-dev, libgl1-mesa-dev
  2. Install node-gyp: npm install -g node-gyp
  3. Rebuild: npm run build:native

App crashes

  1. Check logs for crash information
  2. Verify native module is loaded correctly
  3. Ensure proper cleanup with steam.shutdown() on quit

Performance issues / halved framerate

Since v0.10.5, frame delivery uses beginFrameSubscription which fires in sync with Electron's compositor — no forced GPU→CPU stalls. If you're on an older version, update first.

If you still see performance issues:

  1. Lower the overlay capture FPS if the overlay background doesn't need to match the game rate: steam.addElectronSteamOverlay(win, { fps: 30 })
  2. Reduce window size
  3. Check that runCallbacks() isn't being called at an extremely high rate (once per second is enough)

How It Works

Architecture

┌─────────────────────────────────────────────────────────────┐
│                     Electron Window                         │
│  ┌───────────────────────────────────────────────────────┐  │
│  │                    Web Content                        │  │
│  │               (HTML/CSS/JavaScript)                   │  │
│  └───────────────────────────────────────────────────────┘  │
│                           │                                 │
│               beginFrameSubscription()                      │
│                           ▼                                 │
│  ┌───────────────────────────────────────────────────────┐  │
│  │                Frame Buffer (BGRA)                    │  │
│  └───────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────┘

                       N-API Bridge

┌─────────────────────────────────────────────────────────────┐
│                   Native Overlay Window                     │
│  ┌───────────────────────────────────────────────────────┐  │
│  │         Metal (macOS) / OpenGL (Win/Linux)            │  │
│  │                                                       │  │
│  │   ┌─────────────────────────────────────────────────┐ │  │
│  │   │         Texture with Electron Content           │ │  │
│  │   └─────────────────────────────────────────────────┘ │  │
│  │                         +                             │  │
│  │   ┌─────────────────────────────────────────────────┐ │  │
│  │   │              Steam Overlay Layer                │ │  │
│  │   │          (Injected by Steam client)             │ │  │
│  │   └─────────────────────────────────────────────────┘ │  │
│  └───────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────┘

Frame Pipeline

  1. Electron Rendering: Your Electron window renders to an offscreen buffer (OSR mode)
  2. Frame Delivery: beginFrameSubscription delivers each frame in sync with the compositor — no forced GPU→CPU stalls
  3. Buffer Transfer: Frame data is passed to native module via N-API
  4. Texture Upload: Frame is uploaded to GPU texture
  5. Native Rendering: Native window displays the texture
  6. Steam Injection: Steam injects overlay renderer into native window
  7. Overlay Rendering: Steam overlay renders on top of your content

Performance

  • CPU Usage: ~1-3% additional CPU for frame copying (OSR mode)
  • Memory: ~50-100 MB for buffers and textures
  • Latency: One compositor frame of delay (<16ms at 60 FPS)
  • GPU: Minimal GPU usage (texture upload + blit)
  • Game FPS: No impact in OSR mode — frames are delivered by beginFrameSubscription in sync with the compositor without forced GPU→CPU readback stalls

Best Practices

  1. Always check availability: Use isOverlayAvailable() before enabling
  2. Handle failures gracefully: The app should work without overlay
  3. Wait for content load: Enable overlay after did-finish-load event
  4. Test thoroughly: Test with Steam overlay enabled and disabled
  5. Monitor performance: Profile your app with overlay active
  6. Proper cleanup: Always call steam.shutdown() on quit

Known Limitations

  • Linux Wayland support not available (X11 only)
  • High DPI scaling may require additional handling on some platforms
  • VR/AR applications may need special consideration

License

MIT - See LICENSE file for details

Support