How to Implement Custom Titlebars in Tauri Applications: A Complete Guide
You can implement custom titlebars in Tauri applications by using the @tauri-apps/api/window module to control window state, adding data-tauri-drag-region attributes for dragging, and conditionally rendering platform-specific layouts for macOS compatibility.
The lencx/ChatGPT repository demonstrates a production-ready approach to implementing custom titlebars in Tauri applications. By replacing native OS decorations with React components while preserving full window-control functionality, you can create seamless cross-platform desktop experiences that match your brand's design language.
Architecture of Custom Titlebars in Tauri
A custom titlebar implementation requires coordination between the Rust backend and the JavaScript frontend. The lencx/ChatGPT project structures this across four distinct layers:
- UI Components: React components in
src/components/WinTitlebar.tsxrender visual controls and expose click handlers for window operations. - Platform Logic: The
src/view/Titlebar.tsxcomponent handles macOS-specific adjustments when the native titlebar is hidden. - Tauri Window API: The frontend uses
@tauri-apps/api/windowto call methods liketoggleMaximize(),minimize(), andclose()on the native window. - Drag Region: HTML attributes enable window dragging without native decorations.
Step-by-Step Implementation
Label the Core Window
First, ensure your Tauri window has a consistent label that the frontend can reference. In src-tauri/src/main.rs, the ChatGPT app creates its main window with the label "core":
// src-tauri/src/main.rs
tauri::Builder::default()
.setup(|app| {
let main_window = tauri::WindowBuilder::new(
app,
"core", // This label is used by the frontend
tauri::WindowUrl::App("index.html".into())
)
.title("ChatGPT")
.build()?;
Ok(())
})
Create the React Titlebar Component
The src/components/WinTitlebar.tsx file implements the core controls. It imports the Tauri Window API and binds button clicks to window operations:
// src/components/WinTitlebar.tsx
import { useEffect, useState } from 'react';
import { Window } from '@tauri-apps/api/window';
export default function WinTitlebar() {
const [isMaximized, setIsMaximized] = useState(false);
const win = Window.getByLabel('core'); // References the window label
useEffect(() => {
// Sync initial maximize state
win?.isMaximized().then(setIsMaximized);
}, [win]);
const toggleMaximize = async () => {
await win?.toggleMaximize();
setIsMaximized(await win?.isMaximized() ?? false);
};
return (
<div data-tauri-drag-region className="titlebar-container">
<button onClick={() => win?.minimize()}>Minimize</button>
<button onClick={toggleMaximize}>
{isMaximized ? 'Restore' : 'Maximize'}
</button>
<button onClick={() => win?.close()}>Close</button>
</div>
);
}
Handle Window Controls and State
The implementation uses specific Window API methods to manipulate the native window:
win?.isMaximized(): Returns a promise resolving to the current maximized state.win?.toggleMaximize(): Switches between maximized and restored states.win?.minimize(): Minimizes the window to the taskbar.win?.close(): Terminates the application.
The component maintains local state to toggle between maximize and restore icons based on the isMaximized boolean.
Enable Drag Functionality
To allow users to drag the window by the custom titlebar, add the data-tauri-drag-region attribute to the container element:
<div data-tauri-drag-region className="flex items-center">
{/* Window controls */}
</div>
Tauri automatically treats elements with this attribute as draggable regions, allowing window movement without native decorations. Ensure interactive elements like buttons do not have this attribute to prevent drag interference with clicks.
Platform-Specific Adjustments for macOS
The src/view/Titlebar.tsx component demonstrates handling macOS-specific layouts when hiding the native titlebar. It conditionally applies padding and visibility classes:
// src/view/Titlebar.tsx
<div className={`flex items-center ${info.isMac && isTitlebarHidden ? 'pl-[80px]' : ''}`}>
{/* Navigation and controls */}
<div className="hidden group-hover:flex">
{/* Additional controls visible on hover */}
</div>
</div>
This ensures proper spacing for the traffic light buttons on macOS while maintaining a consistent cross-platform design.
Complete Working Example
Here is a minimal, production-ready implementation combining all patterns from the lencx/ChatGPT repository:
// MinimalCustomTitlebar.tsx
import { useEffect, useState } from 'react';
import { Window } from '@tauri-apps/api/window';
import WindowClose from '~icons/WindowClose';
import WindowMaximize from '~icons/WindowMaximize';
import WindowRestore from '~icons/WindowRestore';
import WindowMinimize from '~icons/WindowMinimize';
export default function MinimalCustomTitlebar() {
const [isMax, setIsMax] = useState(false);
const win = Window.getByLabel('core'); // matches label in src-tauri/src/main.rs
// Sync initial maximize state
useEffect(() => {
(async () => setIsMax(await win?.isMaximized() ?? false))();
}, []);
const toggle = async () => {
await win?.toggleMaximize();
setIsMax(!isMax);
};
return (
<div data-tauri-drag-region className="flex items-center gap-1">
<WindowMinimize size={20} onClick={() => win?.minimize()} />
{isMax
? <WindowRestore size={20} onClick={toggle} />
: <WindowMaximize size={20} onClick={toggle} />}
<WindowClose size={20} onClick={() => win?.close()} />
</div>
);
}
Integrate the component into your application layout:
// App.tsx (simplified)
import MinimalCustomTitlebar from './MinimalCustomTitlebar';
export default function App() {
return (
<div className="h-screen flex flex-col">
<MinimalCustomTitlebar />
{/* Your main content goes here */}
</div>
);
}
Configuration and State Persistence
The lencx/ChatGPT application persists user preferences such as "always on top" using Tauri commands. In src-tauri/src/core/cmd.rs, the window_pin command handles these settings:
// src-tauri/src/core/cmd.rs
#[tauri::command]
async fn window_pin(pin: bool, window: tauri::Window) -> Result<(), String> {
window.set_always_on_top(pin)
.map_err(|e| e.to_string())?;
Ok(())
}
The frontend invokes this command to sync UI state with the native window:
// From src/view/Titlebar.tsx
const togglePin = async () => {
const newPin = !isPinned;
setIsPinned(newPin);
await invoke('window_pin', { pin: newPin });
};
Summary
Implementing custom titlebars in Tauri applications requires coordination between the Rust backend and the JavaScript frontend. Key takeaways include:
- Label consistency: Ensure your window label in
src-tauri/src/main.rsmatches the label used inWindow.getByLabel()on the frontend. - Window API: Use
@tauri-apps/api/windowmethods liketoggleMaximize(),minimize(), andclose()to control native window state. - Drag regions: Add
data-tauri-drag-regionto container elements to enable window dragging without native decorations. - Platform handling: Conditionally render padding and controls for macOS when hiding the native titlebar to accommodate traffic light buttons.
- State persistence: Use Tauri commands in
src-tauri/src/core/cmd.rsto persist window preferences like "always on top".
Frequently Asked Questions
How do I make the custom titlebar draggable in Tauri?
Add the data-tauri-drag-region attribute to the container element of your titlebar. Tauri automatically recognizes this attribute and allows users to drag the window by clicking and dragging that element. Ensure that interactive elements like buttons do not have this attribute to prevent interference with click events.
Can I hide the native titlebar on macOS while keeping traffic light buttons?
Yes, you can hide the native titlebar on macOS while accommodating the traffic light buttons by conditionally applying padding to your custom titlebar. In the src/view/Titlebar.tsx implementation, the component checks for macOS (info.isMac) and hidden titlebar state (isTitlebarHidden) to add left padding (pl-[80px]), ensuring the custom controls do not overlap with the native traffic lights.
How do I sync the maximize/restore button state with the window state?
Query the initial maximized state using win?.isMaximized() in a useEffect hook when the component mounts, then update local state after each toggleMaximize() call. The WinTitlebar.tsx component demonstrates this pattern by maintaining an isMaximized boolean that syncs with the native window state through the Tauri Window API.
What is the correct way to reference the Tauri window from React?
Use Window.getByLabel('core') from @tauri-apps/api/window, ensuring the label string matches the window label defined in src-tauri/src/main.rs. The ChatGPT application creates its main window with the label "core" in the Rust setup code, then references this same label in the React components to manipulate the native window instance.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →