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
Type Description
Text Basic labeled menu option
Check Toggleable entry with checked state
Separator Visual divider between sections
Icon Entry 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)
Shortcut Accelerator String
Ctrl+S / Cmd+S CmdOrCtrl+S
Ctrl+Shift+S CmdOrCtrl+Shift+S
Alt+F4 Alt+F4
F11 F11
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;