# How Fluxer Implements Zoom Factor Control in Its Desktop Application

> Discover how Fluxer implements zoom factor control in its desktop app via IPC channels and a preload bridge for seamless window management and UI scaling.

- Repository: [Fluxer/fluxer](https://github.com/fluxerapp/fluxer)
- Tags: internals
- Published: 2026-03-17

---

**Fluxer synchronizes zoom actions between the Electron main process and renderer through dedicated IPC channels, using a preload bridge to safely expose native window controls while the UI layer manages visual scaling via an AccessibilityStore.**

Fluxer is an Electron-based desktop application that requires precise control over content scaling for accessibility and usability. The implementation separates the native window zoom factor from the UI rendering layer, creating a clean IPC-based architecture that handles zoom commands initiated from the application menu. This article examines how the `fluxerapp/fluxer` repository implements bidirectional zoom control across the main and renderer processes.

## Architecture Overview

The zoom control system in Fluxer relies on a five-step communication flow between the **main process** and **renderer process**:

1. **Menu commands** defined in [`Menu.tsx`](https://github.com/fluxerapp/fluxer/blob/main/Menu.tsx) trigger IPC events (`zoom-in`, `zoom-out`, `zoom-reset`)
2. The **preload script** ([`preload/index.tsx`](https://github.com/fluxerapp/fluxer/blob/main/preload/index.tsx)) exposes a type-safe API that forwards these events to the renderer
3. The **renderer** ([`App.tsx`](https://github.com/fluxerapp/fluxer/blob/main/App.tsx)) registers callbacks that adjust UI scaling through `AccessibilityStore`
4. Optional direct control via `set-zoom-factor` and `get-zoom-factor` IPC handlers that manipulate the native `BrowserWindow`
5. Type definitions ensure contract consistency across the boundary

This design cleanly separates native window management from React UI state while maintaining synchronous feedback for menu interactions.

## Menu Command Definitions

The zoom interface originates in the main process menu configuration. In [`fluxer_desktop/src/main/Menu.tsx`](https://github.com/fluxerapp/fluxer/blob/main/fluxer_desktop/src/main/Menu.tsx), handlers broadcast IPC events to the renderer when users select Zoom In, Zoom Out, or Reset from the application menu.

```tsx
// fluxer_desktop/src/main/Menu.tsx
const zoomInHandler = () => {
  const mainWindow = getMainWindow();
  if (mainWindow) {
    mainWindow.webContents.send('zoom-in');
  }
};

```

Each menu entry follows this pattern, emitting distinct channels (`zoom-in`, `zoom-out`, `zoom-reset`) that the renderer subscribes to through the preload bridge.

## Preload Script Bridge

The preload script acts as a secure intermediary, exposing only approved APIs to the renderer context. In [`fluxer_desktop/src/preload/index.tsx`](https://github.com/fluxerapp/fluxer/blob/main/fluxer_desktop/src/preload/index.tsx), the bridge provides methods to query and modify zoom factors, plus subscription functions for menu-triggered events.

```tsx
// fluxer_desktop/src/preload/index.tsx
setZoomFactor: (factor: number): void => 
  ipcRenderer.send('set-zoom-factor', factor),

getZoomFactor: (): Promise<number> => 
  ipcRenderer.invoke('get-zoom-factor'),

onZoomIn: (cb: () => void) => {
  const handler = () => cb();
  ipcRenderer.on('zoom-in', handler);
  return () => ipcRenderer.removeListener('zoom-in', handler);
},

```

This implementation ensures that the renderer cannot directly access Node.js or Electron main-process modules, adhering to Electron security best practices while enabling responsive zoom controls.

## Renderer Integration

The React layer consumes these APIs through a global `electronApi` object injected by the preload script. In [`fluxer_app/src/App.tsx`](https://github.com/fluxerapp/fluxer/blob/main/fluxer_app/src/App.tsx), the application registers effect hooks that translate IPC events into UI state changes via `AccessibilityStore`.

```tsx
// fluxer_app/src/App.tsx
useEffect(() => {
  const unsubZoomIn = electronApi.onZoomIn?.(() => 
    AccessibilityStore.adjustZoom(0.1));
  
  const unsubZoomOut = electronApi.onZoomOut?.(() => 
    AccessibilityStore.adjustZoom(-0.1));
  
  const unsubReset = electronApi.onZoomReset?.(() => 
    AccessibilityStore.setZoom(1));

  return () => {
    unsubZoomIn?.();
    unsubZoomOut?.();
    unsubReset?.();
  };
}, []);

```

The `AccessibilityStore` manages the visual zoom level through CSS transforms or scaling, providing immediate feedback while optionally synchronizing with the native zoom factor via `setZoomFactor`.

## Main Process IPC Handlers

For persistence and native window consistency, Fluxer implements request-response handlers in [`fluxer_desktop/src/main/IpcHandlers.tsx`](https://github.com/fluxerapp/fluxer/blob/main/fluxer_desktop/src/main/IpcHandlers.tsx). These directly manipulate the `BrowserWindow` zoom factor using Electron's `webContents` API.

```ts
// fluxer_desktop/src/main/IpcHandlers.tsx
ipcMain.on('set-zoom-factor', (event, factor: number) => {
  const win = BrowserWindow.fromWebContents(event.sender);
  if (win && factor > 0) {
    win.webContents.setZoomFactor(factor);
  }
});

ipcMain.handle('get-zoom-factor', (event): number => {
  const win = BrowserWindow.fromWebContents(event.sender);
  return win?.webContents.getZoomFactor() ?? 1;
});

```

The `set-zoom-factor` channel validates input (rejecting factors ≤ 0) while `get-zoom-factor` returns the current value or defaults to 1.0 (100%).

## Type Safety Across the Boundary

Type definitions in [`fluxer_desktop/src/common/Types.tsx`](https://github.com/fluxerapp/fluxer/blob/main/fluxer_desktop/src/common/Types.tsx) and [`fluxer_app/src/types/ElectronTypes.tsx`](https://github.com/fluxerapp/fluxer/blob/main/fluxer_app/src/types/ElectronTypes.tsx) enforce the contract between processes:

```tsx
interface ElectronAPI {
  setZoomFactor: (factor: number) => void;
  getZoomFactor: () => Promise<number>;
  onZoomIn: (cb: () => void) => () => void;
  onZoomOut: (cb: () => void) => () => void;
  onZoomReset: (cb: () => void) => () => void;
}

```

These interfaces prevent runtime errors by ensuring that both main and renderer processes agree on method signatures and parameter types.

## Practical Usage Examples

### Programmatically Setting Zoom Factor

To set a specific zoom level from the renderer and persist it to the native window:

```tsx
// In a React component or store action
await electronApi.setZoomFactor(1.25);  // 125%
const current = await electronApi.getZoomFactor();
console.log(`Native zoom: ${current * 100}%`);

```

### Subscribing to Menu Commands

Components can respond to application menu zoom actions independently of the global store:

```tsx
useEffect(() => {
  const unsubscribe = electronApi.onZoomIn(() => {
    console.log('Zoom in triggered from menu');
    // Custom zoom logic here
  });
  return unsubscribe;
}, []);

```

### Handler Registration in Main Process

When extending the zoom system, register handlers in the main process before the application loads:

```ts
// In IpcHandlers.tsx or equivalent initialization file
ipcMain.on('set-zoom-factor', (event, factor: number) => {
  const win = BrowserWindow.fromWebContents(event.sender);
  if (win && factor > 0) {
    win.webContents.setZoomFactor(factor);
  }
});

```

## Summary

- **IPC Channels** (`zoom-in`, `zoom-out`, `zoom-reset`) drive the zoom flow from main process menu items to the renderer
- **Preload Bridge** ([`preload/index.tsx`](https://github.com/fluxerapp/fluxer/blob/main/preload/index.tsx)) exposes a secure, type-safe API without granting full Node.js access to the renderer
- **AccessibilityStore** manages UI scaling immediately, while optional `setZoomFactor` calls persist the state to the native `BrowserWindow`
- **Main Process Handlers** validate zoom factors and directly manipulate `webContents.setZoomFactor` for Electron-native persistence
- **Type Definitions** ensure compile-time safety across the main/renderer boundary

## Frequently Asked Questions

### How does Fluxer persist zoom settings between sessions?

While the source analysis focuses on runtime zoom control, the architecture supports persistence through the `get-zoom-factor` IPC handler. The renderer can query the current native zoom level via `electronApi.getZoomFactor()` and store it in user preferences (e.g., via localStorage or a settings file), then restore it on application launch using `setZoomFactor` after window creation.

### What is the difference between AccessibilityStore scaling and the native zoom factor?

**AccessibilityStore** handles immediate UI rendering changes through CSS transforms or scaling properties, providing responsive feedback. The **native zoom factor** set via `webContents.setZoomFactor()` operates at the Chromium level, affecting the entire page including browser-internal UI elements. Fluxer uses the former for immediate visual feedback and optionally synchronizes with the latter for persistence.

### Can the zoom factor be constrained to minimum and maximum values?

Yes. The IPC handler in [`fluxer_desktop/src/main/IpcHandlers.tsx`](https://github.com/fluxerapp/fluxer/blob/main/fluxer_desktop/src/main/IpcHandlers.tsx) already validates that `factor > 0`, but you can extend this logic to enforce specific bounds (e.g., 0.5 to 3.0) by adding range checks before calling `win.webContents.setZoomFactor(factor)` or by validating inputs in the `AccessibilityStore` before invoking the IPC method.

### Why does Fluxer use separate channels for zoom events instead of a single channel with parameters?

The separate IPC channels (`zoom-in`, `zoom-out`, `zoom-reset`) map directly to discrete menu actions, making the code self-documenting and easier to debug. This approach also allows the renderer to subscribe to specific actions without parsing message contents, keeping the event handlers lightweight and the preload API explicit.