mcpField
C
customizing-tauri-windows

customizing-tauri-windows

github.com/dchukDesign & UI36221 installsUpdated 40w ago
Repository

About customizing-tauri-windows

A window customization guide for Tauri v2 developers, covering custom titlebars, transparent windows, and window menus through configuration, JavaScript APIs, or Rust APIs.

Key features

  1. 01Configure window size and decorations
  2. 02Implement custom titlebars and drag regions
  3. 03Set transparent macOS titlebars
  4. 04Create menus with JavaScript or Rust
  5. 05Handle menu events and update items
  6. 06Configure menu keyboard shortcuts

Use cases

  1. 01Design a custom titlebar for a Tauri app
  2. 02Add minimize and maximize controls to a window
  3. 03Build app menus and handle menu events
  4. 04Update menu item state or text

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/dchuk/claude-code-tauri-skills --skill customizing-tauri-windows

After installing, type/customizing-tauri-windowsin Claude Code to confirm it loaded.

Details

From the project README

Tauri Window Customization

Covers window customization in Tauri v2: custom titlebars, transparent windows, and window menus.

Configuration Methods

  • tauri.conf.json - Static configuration at build time
  • JavaScript Window API - Runtime modifications from frontend
  • Rust Window struct - Runtime modifications from backend

Window Configuration (tauri.conf.json)

{
  "app": {
    "windows": [{
      "title": "My App",
      "width": 800,
      "height": 600,
      "decorations": true,
      "transparent": false,
      "alwaysOnTop": false,
      "center": true
    }]
  }
}

Custom Titlebar Implementation

Step 1: Disable Decorations
{ "app": { "windows": [{ "decorations": false }] } }
Step 2: Configure Permissions (src-tauri/capabilities/default.json)
{
  "identifier": "main-capability",
  "windows": ["main"],
  "permissions": [
    "core:window:default",
    "core:window:allow-start-dragging",
    "core:window:allow-close",
    "core:window:allow-minimize",
    "core:window:allow-toggle-maximize"
  ]
}
Step 3: HTML Structure
<div class="titlebar">
  <div class="titlebar-drag" data-tauri-drag-region>
    <span class="title">My Application</span>
  </div>
  <div class="titlebar-controls">
    <button id="titlebar-minimize">-</button>
    <button id="titlebar-maximize">[]</button>
    <button id="titlebar-close">x</button>
  </div>
</div>
<main class="content"><!-- App content --></main>
Step 4: CSS Styling
.titlebar {
  height: 30px;
  background: #329ea3;
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  display: grid;
  grid-template-columns: 1fr auto;
  user-select: none;
}

.titlebar-drag {
  display: flex;
  align-items: center;
  padding-left: 12px;
}

.titlebar-controls { display: flex; }

.titlebar-controls button {
  width: 46px;
  height: 30px;
  border: none;
  background: transparent;
  color: white;
  cursor: pointer;
}

.titlebar-controls button:hover { background: rgba(255,255,255,0.1); }
.titlebar-controls button#titlebar-close:hover { background: #e81123; }
.content { margin-top: 30px; padding: 16px; }
Step 5: JavaScript Controls
import { getCurrentWindow } from '@tauri-apps/api/window';

const appWindow = getCurrentWindow();

document.getElementById('titlebar-minimize')
  ?.addEventListener('click', () => appWindow.minimize());
document.getElementById('titlebar-maximize')
  ?.addEventListener('click', () => appWindow.toggleMaximize());
document.getElementById('titlebar-close')
  ?.addEventListener('click', () => appWindow.close());
Drag Region Behavior

The data-tauri-drag-region attribute applies only to its element, not children. This preserves button interactivity. Add the attribute to each draggable child if needed.

Manual Drag with Double-Click Maximize
document.getElementById('titlebar')?.addEventListener('mousedown', (e) => {
  if (e.buttons === 1 && e.target === e.currentTarget) {
    e.detail === 2 ? appWindow.toggleMaximize() : appWindow.startDragging();
  }
});

macOS Transparent Titlebar

Cargo.toml
[target."cfg(target_os = \"macos\")".dependencies]
cocoa = "0.26"
Rust Implementation
use tauri::{TitleBarStyle, WebviewUrl, WebviewWindowBuilder};

pub fn run() {
    tauri::Builder::default()
        .setup(|app| {
            let win_builder = WebviewWindowBuilder::new(app, "main", WebviewUrl::default())
                .title("Transparent Titlebar Window")
                .inner_size(800.0, 600.0);

            #[cfg(target_os = "macos")]
            let win_builder = win_builder.title_bar_style(TitleBarStyle::Transparent);

            let window = win_builder.build().unwrap();

            #[cfg(target_os = "macos")]
            {
                use cocoa::appkit::{NSColor, NSWindow};
                use cocoa::base::{id, nil};
                let ns_window = window.ns_window().unwrap() as id;
                unsafe {
                    let bg_color = NSColor::colorWithRed_green_blue_alpha_(
                        nil, 50.0/255.0, 158.0/255.0, 163.5/255.0, 1.0
                    );
                    ns_window.setBackgroundColor_(bg_color);
                }
            }
            Ok(())
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application")
}

Note: Custom titlebars on macOS lose native features like window snapping. Transparent titlebar preserves these.

Window Menus

Menu Item Types
TypeDescription
TextBasic labeled menu option
CheckToggleable entry with checked state
SeparatorVisual divider between sections
IconEntry with custom icon (Tauri 2.8.0+)
Creating Menus (JavaScript/TypeScript)
import { Menu, MenuItem, Submenu, PredefinedMenuItem, CheckMenuItem } from '@tauri-apps/api/menu';

const fileSubmenu = await Submenu.new({
  text: 'File',
  items: [
    await MenuItem.new({
      id: 'new', text: 'New', accelerator: 'CmdOrCtrl+N',
      action: () => console.log('New')
    }),
    await MenuItem.new({
      id: 'open', text: 'Open', accelerator: 'CmdOrCtrl+O',
      action: () => console.log('Open')
    }),
    await MenuItem.new({
      id: 'save', text: 'Save', accelerator: 'CmdOrCtrl+S',
      action: () => console.log('Save')
    }),
    { type: 'Separator' },
    await MenuItem.new({
      id: 'quit', text: 'Quit', accelerator: 'CmdOrCtrl+Q',
      action: () => console.log('Quit')
    })
  ]
});

const editSubmenu = await Submenu.new({
  text: 'Edit',
  items: [
    await PredefinedMenuItem.new({ item: 'Undo' }),
    await PredefinedMenuItem.new({ item: 'Redo' }),
    await PredefinedMenuItem.new({ item: 'Separator' }),
    await PredefinedMenuItem.new({ item: 'Cut' }),
    await PredefinedMenuItem.new({ item: 'Copy' }),
    await PredefinedMenuItem.new({ item: 'Paste' })
  ]
});

const viewSubmenu = await Submenu.new({
  text: 'View',
  items: [
    await CheckMenuItem.new({
      id: 'sidebar', text: 'Show Sidebar', checked: true,
      action: async (item) => console.log('Sidebar:', await item.isChecked())
    })
  ]
});

const menu = await Menu.new({ items: [fileSubmenu, editSubmenu, viewSubmenu] });
await menu.setAsAppMenu();
Creating Menus (Rust)
use tauri::menu::{MenuBuilder, SubmenuBuilder};

let file_menu = SubmenuBuilder::new(app, "File")
    .text("new", "New")
    .text("open", "Open")
    .text("save", "Save")
    .separator()
    .text("quit", "Quit")
    .build()?;

let edit_menu = SubmenuBuilder::new(app, "Edit")
    .undo()
    .redo()
    .separator()
    .cut()
    .copy()
    .paste()
    .build()?;

let menu = MenuBuilder::new(app)
    .items(&[&file_menu, &edit_menu])
    .build()?;

app.set_menu(menu)?;

macOS Note: All menu items must be grouped under submenus. Top-level items are ignored.

Handling Menu Events (Rust)
app.on_menu_event(|_app_handle, event| {
    match event.id().0.as_str() {
        "new" => println!("New file"),
        "open" => println!("Open file"),
        "save" => println!("Save file"),
        "quit" => std::process::exit(0),
        _ => {}
    }
});
Dynamic Menu Updates

JavaScript:

const statusItem = await menu.get('status');
if (statusItem) await statusItem.setText('Status: Ready');

Rust:

menu.get("status").unwrap().as_menuitem_unchecked().set_text("Status: Ready")?;

Keyboard Shortcuts (Accelerators)

ShortcutAccelerator String
Ctrl+S / Cmd+SCmdOrCtrl+S
Ctrl+Shift+SCmdOrCtrl+Shift+S
Alt+F4Alt+F4
F11F11

Complete Example

main.rs
use tauri::menu::{MenuBuilder, SubmenuBuilder};

pub fn run() {
    tauri::Builder::default()
        .setup(|app| {
            let file_menu = SubmenuBuilder::new(app, "File")
                .text("new", "New")
                .text("open", "Open")
                .separator()
                .text("quit", "Quit")
                .build()?;

            let edit_menu = SubmenuBuilder::new(app, "Edit")
                .undo().redo().separator().cut().copy().paste()
                .build()?;

            let menu = MenuBuilder::new(app)
                .items(&[&file_menu, &edit_menu])
                .build()?;
            app.set_menu(menu)?;
            Ok(())
        })
        .on_menu_event(|_app, event| {
            match event.id().0.as_str() {
                "quit" => std::process::exit(0),
                id => println!("Menu event: {}", id),
            }
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application")
}
React Component
import { useEffect } from 'react';
import { getCurrentWindow } from '@tauri-apps/api/window';

function App() {
  useEffect(() => {
    const appWindow = getCurrentWindow();
    const minimize = () => appWindow.minimize();
    const maximize = () => appWindow.toggleMaximize();
    const close = () => appWindow.close();

    document.getElementById('titlebar-minimize')?.addEventListener('click', minimize);
    document.getElementById('titlebar-maximize')?.addEventListener('click', maximize);
    document.getElementById('titlebar-close')?.addEventListener('click', close);

    return () => {
      document.getElementById('titlebar-minimize')?.removeEventListener('click', minimize);
      document.getElementById('titlebar-maximize')?.removeEventListener('click', maximize);
      document.getElementById('titlebar-close')?.removeEventListener('click', close);
    };
  }, []);

  return (
    <>
      <div className="titlebar">
        <div className="titlebar-drag" data-tauri-drag-region>
          <span>My Tauri App</span>
        </div>
        <div className="titlebar-controls">
          <button id="titlebar-minimize">-</button>
          <button id="titlebar-maximize">[]</button>
          <button id="titlebar-close">x</button>
        </div>
      </div>
      <main className="content">
        <h1>Welcome to Tauri</h1>
      </main>
    </>
  );
}

export default App;

Comments

Markdown supported