# How to Implement Custom Titlebars in Tauri Applications: A Complete Guide

> Learn to implement custom titlebars in Tauri apps. Control window state, enable dragging, and ensure macOS compatibility with this comprehensive guide.

- Repository: [lencx/ChatGPT](https://github.com/lencx/ChatGPT)
- Tags: how-to-guide
- Published: 2026-03-06

---

**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.tsx`](https://github.com/lencx/ChatGPT/blob/main/src/components/WinTitlebar.tsx) render visual controls and expose click handlers for window operations.
- **Platform Logic**: The [`src/view/Titlebar.tsx`](https://github.com/lencx/ChatGPT/blob/main/src/view/Titlebar.tsx) component handles macOS-specific adjustments when the native titlebar is hidden.
- **Tauri Window API**: The frontend uses `@tauri-apps/api/window` to call methods like `toggleMaximize()`, `minimize()`, and `close()` 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`](https://github.com/lencx/ChatGPT/blob/main/src-tauri/src/main.rs), the ChatGPT app creates its main window with the label `"core"`:

```rust
// 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`](https://github.com/lencx/ChatGPT/blob/main/src/components/WinTitlebar.tsx) file implements the core controls. It imports the Tauri `Window` API and binds button clicks to window operations:

```typescript
// 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:

```tsx
<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`](https://github.com/lencx/ChatGPT/blob/main/src/view/Titlebar.tsx) component demonstrates handling macOS-specific layouts when hiding the native titlebar. It conditionally applies padding and visibility classes:

```tsx
// 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:

```tsx
// 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:

```tsx
// 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`](https://github.com/lencx/ChatGPT/blob/main/src-tauri/src/core/cmd.rs), the `window_pin` command handles these settings:

```rust
// 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:

```typescript
// 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.rs`](https://github.com/lencx/ChatGPT/blob/main/src-tauri/src/main.rs) matches the label used in `Window.getByLabel()` on the frontend.
- **Window API**: Use `@tauri-apps/api/window` methods like `toggleMaximize()`, `minimize()`, and `close()` to control native window state.
- **Drag regions**: Add `data-tauri-drag-region` to 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.rs`](https://github.com/lencx/ChatGPT/blob/main/src-tauri/src/core/cmd.rs) to 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`](https://github.com/lencx/ChatGPT/blob/main/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`](https://github.com/lencx/ChatGPT/blob/main/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`](https://github.com/lencx/ChatGPT/blob/main/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.