Cross-Platform Development

December 18, 2025 · View on GitHub

This app supports macOS, Windows, and Linux. This guide covers platform-specific patterns, utilities, and configuration.

Overview

PlatformTitle BarWindow ControlsDecorations
macOSCustom + vibrancyLeft (traffic)Hidden
WindowsCustomRightHidden
LinuxNative + toolbarNativeVisible

Design decisions:

  • macOS is the primary development target
  • No Windows code signing (configure your own certificates)
  • Linux uses native decorations for desktop environment compatibility
  • Keyboard shortcuts use mod+ prefix (Cmd on macOS, Ctrl elsewhere)

Platform Detection

Frontend (React)

Use the platform hook for UI decisions:

import { usePlatform, getPlatform } from '@/hooks/use-platform'

// In components
function MyComponent() {
  const platform = usePlatform()

  if (platform === 'macos') {
    return <MacOSSpecificUI />
  }
  // ...
}

// In callbacks or non-hook contexts
function handleAction() {
  const platform = getPlatform()
  // ...
}

Convenience hooks are also available:

import { useIsMacOS, useIsWindows, useIsLinux } from '@/hooks/use-platform'

function MyComponent() {
  const isMacOS = useIsMacOS()
  // ...
}

Backend (Rust)

Use conditional compilation for platform-specific code:

// Compile-time checks (preferred for performance)
#[cfg(target_os = "macos")]
fn macos_only() {
    // Only compiled on macOS
}

#[cfg(target_os = "windows")]
fn windows_only() {
    // Only compiled on Windows
}

#[cfg(target_os = "linux")]
fn linux_only() {
    // Only compiled on Linux
}

// Runtime checks (for dynamic behavior)
use crate::utils::platform;

if platform::is_macos() {
    // macOS runtime behavior
}

Platform-Specific Strings

Use getPlatformStrings() for platform-appropriate UI labels:

import { getPlatformStrings, formatShortcut } from '@/lib/platform-strings'
import { usePlatform } from '@/hooks/use-platform'

function FileMenu() {
  const platform = usePlatform()
  const strings = getPlatformStrings(platform)

  return (
    <MenuItem>
      {strings.revealInFileManager} {/* "Reveal in Finder" or "Show in Explorer" */}
    </MenuItem>
  )
}

// Format keyboard shortcuts for display
formatShortcut('macos', 'K') // "⌘K"
formatShortcut('windows', 'K') // "Ctrl+K"
formatShortcut('macos', 'S', ['shift', 'mod']) // "⇧⌘S"

Available strings:

PropertymacOSWindowsLinux
revealInFileManager"Reveal in Finder""Show in Explorer""Show in Files"
fileManagerName"Finder""Explorer""Files"
modifierKey"Cmd""Ctrl""Ctrl"
modifierKeySymbol"⌘""Ctrl""Ctrl"
optionKey"Option""Alt""Alt"
preferencesLabel"Preferences""Settings""Preferences"
quitLabel"Quit""Exit""Quit"
trashName"Trash""Recycle Bin""Trash"

Title Bar Architecture

The title bar system uses platform detection to render appropriate controls:

TitleBar.tsx (router)
├── macOS: MacOSWindowControls (left) + TitleBarContent
├── Windows: TitleBarContent + WindowsWindowControls (right)
└── Linux: LinuxTitleBar (toolbar only, no window controls)

Testing Other Platforms

In development, use the forcePlatform prop to preview other platform layouts:

// Only works in development builds
<TitleBar forcePlatform="windows" />
<TitleBar forcePlatform="linux" />

Custom Title Bars

If building custom title bars, use the shared components:

import {
  TitleBarLeftActions,
  TitleBarRightActions,
  TitleBarTitle,
} from '@/components/titlebar'

function CustomTitleBar() {
  return (
    <div data-tauri-drag-region className="...">
      <TitleBarLeftActions />
      <TitleBarTitle title="My App" />
      <TitleBarRightActions />
    </div>
  )
}

Path Handling

Windows uses backslashes (\) in paths, but the frontend expects forward slashes (/). Normalize paths when sending from Rust to React:

use crate::utils::platform::normalize_path_for_serialization;

#[tauri::command]
fn get_file_path() -> String {
    let path = some_path();
    // Converts "C:\Users\foo\file.txt" to "C:/Users/foo/file.txt"
    normalize_path_for_serialization(&path)
}

The frontend can then use paths consistently:

// Works on all platforms
const parts = filePath.split('/')

Tauri Configuration

Tauri v2 automatically merges platform-specific config files using JSON Merge Patch.

Config Files

src-tauri/
├── tauri.conf.json         # Base config (safe defaults)
├── tauri.macos.conf.json   # macOS overrides
├── tauri.windows.conf.json # Windows overrides
└── tauri.linux.conf.json   # Linux overrides

Key Differences

Base config (tauri.conf.json):

{
  "app": {
    "windows": [
      {
        "decorations": true,
        "transparent": false
      }
    ]
  }
}

macOS (tauri.macos.conf.json):

{
  "app": {
    "windows": [
      {
        "decorations": false,
        "transparent": true,
        "windowEffects": {
          "effects": ["hudWindow"]
        }
      }
    ]
  }
}

Windows (tauri.windows.conf.json):

{
  "app": {
    "windows": [
      {
        "decorations": false,
        "transparent": false
      }
    ]
  }
}

Linux (tauri.linux.conf.json):

{
  "app": {
    "windows": [
      {
        "decorations": true,
        "transparent": false
      }
    ]
  }
}

Important: JSON Merge Patch Behavior

JSON Merge Patch replaces arrays entirely, not element-by-element. Each platform config must include the complete windows array with all properties, not just overrides.

Building for Each Platform

Development

# Runs on current platform
npm run dev

Production Builds

Builds are platform-specific. You can only build for your current OS (cross-compilation requires additional setup):

# Build for current platform
npm run build

CI/CD Builds

The GitHub Actions release workflow builds for all platforms:

PlatformRunnerOutput
macOSmacos-latest.dmg
Windowswindows-latest.msi
Linuxubuntu-22.04.AppImage

See .github/workflows/release.yml for the full configuration.

Linux Dependencies

Linux builds require additional system libraries:

sudo apt-get update
sudo apt-get install -y \
  libwebkit2gtk-4.1-dev \
  libappindicator3-dev \
  librsvg2-dev \
  patchelf

CSS Considerations

Windows Drag Regions

Windows requires an additional CSS rule for drag regions to work with touch/pen input:

*[data-tauri-drag-region] {
  app-region: drag;
}

This is already included in the app's global styles.

GPU Acceleration

For opacity transitions on Windows title bars, use transform-gpu to fix WebKit rendering quirks:

<div className="transform-gpu transition-opacity" />

Best Practices

  1. Test with forcePlatform - Verify layouts for all platforms during development
  2. Use platform strings - Never hardcode platform-specific labels
  3. Normalize paths - Always convert Windows paths before sending to frontend
  4. Prefer conditional compilation - Use #[cfg(...)] over runtime checks in Rust when possible
  5. Keep Linux simple - Native decorations work best across desktop environments
  6. Document platform assumptions - Note any platform-specific behavior in comments

Files Reference

FilePurpose
src/hooks/use-platform.tsPlatform detection hooks
src/lib/platform-strings.tsPlatform-specific UI strings
src/components/titlebar/TitleBar.tsxPlatform-aware title bar
src-tauri/src/utils/platform.rsRust platform utilities
src-tauri/tauri.*.conf.jsonPlatform-specific configs