npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@drycstud.io/electron-titlebar

v2.1.1

Published

A pretty way to add Titlebar in a Electron app using ReactJS

Readme

Pretty Electron Titlebar

NPM Version (scoped) npm Prettier GitHub license

A pretty, cross-platform titlebar for Electron apps built with React. Automatically adapts to macOS (native traffic lights), Windows, and Linux — similar to VS Code, Figma, and Postman.

Features

  • Cross-platform — Native look on macOS (traffic lights), custom window controls on Windows/Linux
  • Menu bar — Full dropdown menus with cascading (nested) submenus, keyboard navigation, separators, and disabled items
  • Toolbar actions — Customizable action buttons (notifications, settings, upgrade) with badges, tooltips, dropdowns, and highlight effects
  • User profile — Avatar with status indicator, dropdown actions, sign-in/sign-out flow
  • Command palette — Searchable command palette with sections, filters, keyboard navigation, and footer actions
  • Keyboard shortcuts — Define shortcuts once in Windows notation (Ctrl+S), automatically formatted as macOS symbols (⌘S) on Mac
  • Keyboard navigation — Full Arrow key, Enter, and Escape support for menus, command palette, and toolbar dropdowns
  • Zero CSS conflicts — Styles are fully scoped; works alongside Tailwind CSS, CSS Modules, or any other styling solution
  • Super customizable — Custom title, logo, size, menus, toolbar actions, user profile, command palette, and window action handlers
  • TypeScript — Full type definitions included
  • 100% test coverage — Comprehensive tests with Vitest
  • Storybook — Interactive component documentation for all variants

Table of Contents

Architecture

Component Tree

graph TD
    A["<b>Titlebar</b><br/><i>Main entry point</i>"] --> B["<b>TitlebarContainer</b><br/><i>Styled wrapper (fixed, scoped CSS)</i>"]
    B --> C["<b>Logo</b><br/><i>App icon (Win/Linux only)</i>"]
    B --> D["<b>WindowControls</b><br/><i>Layout orchestrator</i>"]

    D -->|"macOnly"| E["<b>Menu</b><br/><i>Dropdown menu bar</i>"]
    D -->|"macOnly"| F["<b>Title</b><br/><i>Drag region (empty on Mac)</i>"]
    D -->|"macOnly"| SBm["<b>SearchBar</b><br/><i>Command palette (centered)</i>"]
    D -->|"macOnly"| TAm["<b>ToolbarActions</b><br/><i>Custom action buttons</i>"]
    D -->|"macOnly"| G["<b>UserProfile</b><br/><i>Avatar + dropdown</i>"]

    D -->|"Win / Linux"| H["<b>Menu</b><br/><i>Dropdown menu bar</i>"]
    D -->|"Win / Linux"| I["<b>Title</b><br/><i>Centered window title</i>"]
    D -->|"Win / Linux"| SBw["<b>SearchBar</b><br/><i>Command palette</i>"]
    D -->|"Win / Linux"| TAw["<b>ToolbarActions</b><br/><i>Custom action buttons</i>"]
    D -->|"Win / Linux"| J["<b>UserProfile</b><br/><i>Avatar + dropdown</i>"]
    D -->|"Win / Linux"| K["<b>ButtonContainer</b>"]

    K --> L["<b>ActionButton</b><br/><i>Minimize</i>"]
    K --> M["<b>ActionButton</b><br/><i>Maximize / Restore</i>"]
    K --> N["<b>ActionButton</b><br/><i>Close (red hover)</i>"]

    E --> O["<b>MenuItemButton</b>"]
    E --> P["<b>Dropdown</b>"]
    P --> Q["<b>DropdownItem</b> + <b>Shortcut</b>"]
    P --> R["<b>Separator</b>"]
    P --> CS["<b>SubMenuDropdown</b><br/><i>Cascading children (recursive)</i>"]

    TAm --> TAI["<b>ToolbarActionItem</b><br/><i>Icon + badge + tooltip + dropdown</i>"]

    G --> S["<b>AvatarButton</b><br/><i>Initials or image</i>"]
    G --> T["<b>User Dropdown</b>"]
    T --> U["<b>UserHeader</b><br/><i>Name + email</i>"]
    T --> V["<b>DropdownItem</b><br/><i>Custom actions</i>"]
    T --> W["<b>Sign Out</b>"]

    style A fill:#4f46e5,color:#fff,stroke:#4f46e5
    style D fill:#2563eb,color:#fff,stroke:#2563eb
    style E fill:#0d9488,color:#fff,stroke:#0d9488
    style H fill:#0d9488,color:#fff,stroke:#0d9488
    style TAm fill:#d97706,color:#fff,stroke:#d97706
    style TAw fill:#d97706,color:#fff,stroke:#d97706
    style SBm fill:#0891b2,color:#fff,stroke:#0891b2
    style SBw fill:#0891b2,color:#fff,stroke:#0891b2
    style G fill:#9333ea,color:#fff,stroke:#9333ea
    style J fill:#9333ea,color:#fff,stroke:#9333ea
    style K fill:#dc2626,color:#fff,stroke:#dc2626

Electron IPC Flow

sequenceDiagram
    participant Main as Main Process
    participant Preload as Preload Script
    participant Renderer as Renderer (React)

    Note over Main: setup() + getTitlebarOptions()
    Main->>Main: new BrowserWindow({ frame: false, ... })
    Main->>Main: attachToWindow(ipcMain, mainWindow)

    Note over Preload: preloadConfig()
    Preload->>Renderer: contextBridge.exposeInMainWorld('electron', electronAPI)

    Note over Renderer: <Titlebar /> mounts

    Renderer->>Main: ipc.send('minimizeWindow')
    Main->>Main: mainWindow.minimize()

    Renderer->>Main: ipc.send('maximizeRestoreWindow')
    Main->>Main: isMaximized ? restore() : maximize()
    Main->>Renderer: webContents.send('windowsIsMaximized', boolean)

    Renderer->>Main: ipc.send('closeWindow')
    Main->>Main: mainWindow.close()

Platform Layout Comparison

block-beta
    columns 1

    block:mac["macOS"]
        columns 9
        tl["🔴🟡🟢"] ml["File"] m2l["Edit"] m3l["View"] spaceMac["   ← drag region →   "]:2 searchMac["🔍"] actionsMac["🔔 ⚙️ ⚡"] upl["👤"]
    end

    block:win["Windows / Linux"]
        columns 10
        logo["⚡"] mw["File"] m2w["Edit"] m3w["View"] spaceWin["  Title  "]:2 searchWin["🔍"] actionsWin["🔔 ⚙️ ⚡"] upw["👤"] btns["— □ ✕"]
    end

    style mac fill:#1C1C1C,color:#fff,stroke:#333
    style win fill:#1C1C1C,color:#fff,stroke:#333
    style tl fill:transparent,color:#fff,stroke:none
    style spaceMac fill:transparent,color:#888,stroke:none
    style spaceWin fill:transparent,color:#888,stroke:none
    style btns fill:transparent,color:#fff,stroke:none
    style logo fill:transparent,color:#fff,stroke:none
    style searchMac fill:transparent,color:#888,stroke:none
    style searchWin fill:transparent,color:#888,stroke:none
    style actionsMac fill:transparent,color:#d97706,stroke:none
    style actionsWin fill:transparent,color:#d97706,stroke:none
    style upl fill:transparent,color:#fff,stroke:none
    style upw fill:transparent,color:#fff,stroke:none

File Structure

graph LR
    subgraph "src/"
        main["main.ts<br/><i>Barrel export</i>"]
        utils["utils/index.ts<br/><i>OS detection, formatShortcut</i>"]

        subgraph "config/"
            setup["setup.ts"]
            options["getTitlebarOptions"]
            attach["attach.ts<br/><i>IPC handlers</i>"]
            preload["preloadConfig.ts"]
        end

        subgraph "components/Titlebar/"
            titlebar["Titlebar.tsx"]
            hooks["hooks/useTitlebarActions.ts"]
            styles["styles.ts"]

            subgraph "WindowControls"
                wc["WindowControls.tsx"]
            end
            subgraph "Menu"
                menu["Menu.tsx"]
                menuTypes["types.ts"]
            end
            subgraph "ActionButton"
                ab["ActionButton.tsx"]
            end
            subgraph "SearchBar"
                sb["SearchBar.tsx"]
                sbTypes["types.ts"]
            end
            subgraph "ToolbarActions"
                ta["ToolbarActions.tsx"]
                tai["ToolbarActionItem.tsx"]
                taTypes["types.ts"]
            end
            subgraph "UserProfile"
                up["UserProfile.tsx"]
                upTypes["types.ts"]
            end
        end
    end

    main --> titlebar
    titlebar --> wc
    titlebar --> hooks
    wc --> menu
    wc --> ab
    wc --> sb
    wc --> ta
    wc --> up
    menu --> utils

    style titlebar fill:#4f46e5,color:#fff
    style main fill:#1e40af,color:#fff
    style setup fill:#0d9488,color:#fff
    style attach fill:#0d9488,color:#fff
    style preload fill:#0d9488,color:#fff
    style ta fill:#d97706,color:#fff
    style sb fill:#0891b2,color:#fff

Installation

# npm
npm install @drycstud.io/electron-titlebar

# yarn
yarn add @drycstud.io/electron-titlebar

# pnpm
pnpm add @drycstud.io/electron-titlebar

Quick Start

Integration requires changes in three files: the main process, the preload script, and the renderer (React).

1. Main Process Setup

In your Electron main process file (usually main.ts or main.js), import setup, getTitlebarOptions, and attachToWindow from the config entry point:

// main.ts
import { app, BrowserWindow, ipcMain } from 'electron';
import { setup, getTitlebarOptions, attachToWindow } from '@drycstud.io/electron-titlebar/config';

setup();

function createWindow(): void {
  const mainWindow = new BrowserWindow({
    width: 1280,
    height: 840,
    ...getTitlebarOptions(), // Applies correct frame/titlebar settings per platform
    webPreferences: {
      nodeIntegration: true,
      preload: path.join(__dirname, '../preload/index.js'),
      sandbox: false,
    },
  });

  mainWindow.on('ready-to-show', () => {
    attachToWindow(ipcMain, mainWindow); // Registers IPC handlers for window controls
    mainWindow.show();
  });
}

app.whenReady().then(createWindow);

What getTitlebarOptions() returns per platform:

| Platform | frame | titleBarStyle | trafficLightPosition | |-----------------|---------|-----------------|------------------------| | macOS | false | 'hiddenInset' | { x: 10, y: 10 } | | Windows / Linux | false | 'hidden' | — |

On macOS, hiddenInset preserves the native traffic light buttons (close, minimize, maximize) while hiding the rest of the native titlebar. On Windows and Linux, the frame is fully hidden and replaced by custom controls rendered by the Titlebar React component.

2. Preload Script Setup

In your Electron preload script (usually preload.ts or preload.js), call preloadConfig to expose the required IPC APIs to the renderer:

// preload.ts
import { preloadConfig } from '@drycstud.io/electron-titlebar/config';

preloadConfig();

This uses contextBridge.exposeInMainWorld to safely expose the Electron IPC renderer API to the browser window. It is required for the window control buttons (minimize, maximize, close) to work.

3. Renderer (React) Setup

In your React app entry point or root component, render the Titlebar component with menus:

// App.tsx
import Titlebar from '@drycstud.io/electron-titlebar';
import type { MenuItem } from '@drycstud.io/electron-titlebar';
import appLogo from './assets/logo.svg';

const menuItems: MenuItem[] = [
  {
    label: 'File',
    submenu: [
      { label: 'New File', shortcut: 'Ctrl+N', action: () => console.log('New File') },
      { label: 'Open File...', shortcut: 'Ctrl+O', action: () => console.log('Open') },
      {
        label: 'Open Recent',
        submenu: [
          { label: '~/projects/my-app', action: () => console.log('Open my-app') },
          { label: '~/projects/dashboard', action: () => console.log('Open dashboard') },
          { type: 'separator', label: '' },
          { label: 'Clear Recently Opened', action: () => console.log('Clear') },
        ],
      },
      { type: 'separator', label: '' },
      { label: 'Save', shortcut: 'Ctrl+S', action: () => console.log('Save') },
      { label: 'Save As...', shortcut: 'Ctrl+Shift+S', action: () => console.log('Save As') },
      { type: 'separator', label: '' },
      { label: 'Exit', shortcut: 'Alt+F4', action: () => globalThis.close() },
    ],
  },
  {
    label: 'Edit',
    submenu: [
      { label: 'Undo', shortcut: 'Ctrl+Z', action: () => console.log('Undo') },
      { label: 'Redo', shortcut: 'Ctrl+Shift+Z', action: () => console.log('Redo') },
      { type: 'separator', label: '' },
      { label: 'Cut', shortcut: 'Ctrl+X', action: () => console.log('Cut') },
      { label: 'Copy', shortcut: 'Ctrl+C', action: () => console.log('Copy') },
      { label: 'Paste', shortcut: 'Ctrl+V', action: () => console.log('Paste') },
    ],
  },
  {
    label: 'Help',
    submenu: [
      { label: 'Documentation', action: () => console.log('Docs') },
      { label: 'About', action: () => console.log('About') },
    ],
  },
];

export default function App() {
  return (
    <>
      <Titlebar title="My App" logo={appLogo} menuItems={menuItems} />
      {/* Your app content */}
    </>
  );
}

That's it! The titlebar will automatically:

  • Show native traffic lights + menus + centered title on macOS
  • Show logo + menus + title + minimize/maximize/close buttons on Windows and Linux
  • Format shortcuts as ⌘S on macOS and Ctrl+S on Windows/Linux

Menu System

Defining Menus

Menus are defined as an array of MenuItem objects passed to the menuItems prop. The MenuItem type is recursive — any item can have a submenu containing more MenuItem objects, enabling cascading (nested) menus to any depth.

Each menu item supports:

  • label — Display text
  • submenu — Array of MenuItem objects (nested submenu, shown as a cascading flyout)
  • action — Callback fired when the item is clicked (ignored if submenu is set)
  • shortcut — Keyboard shortcut string (e.g., 'Ctrl+Shift+N')
  • disabled — Grays out and disables the item
  • type: 'separator' — Renders a horizontal divider line
const menuItems: MenuItem[] = [
  {
    label: 'File',
    submenu: [
      { label: 'New File', shortcut: 'Ctrl+N', action: () => createFile() },
      {
        label: 'Open Recent',
        submenu: [
          { label: '~/projects/my-app', action: () => openProject('my-app') },
          { label: '~/projects/dashboard', action: () => openProject('dashboard') },
          { type: 'separator', label: '' },
          { label: 'Clear Recently Opened', action: () => clearRecent() },
        ],
      },
      { type: 'separator', label: '' },
      { label: 'Save', shortcut: 'Ctrl+S', action: () => save() },
    ],
  },
  {
    label: 'Disabled Menu',
    disabled: true,
  },
];

Cascading (Nested) Submenus

Any MenuItem can contain a submenu array, and those children can also have their own submenu, enabling multi-level cascading menus just like native desktop applications.

{
  label: 'View',
  submenu: [
    {
      label: 'Appearance',
      submenu: [
        { label: 'Zoom In', shortcut: 'Ctrl+=', action: () => zoomIn() },
        { label: 'Zoom Out', shortcut: 'Ctrl+-', action: () => zoomOut() },
        { type: 'separator', label: '' },
        {
          label: 'Color Theme',
          submenu: [
            { label: 'Dark+ (default)', action: () => setTheme('dark+') },
            { label: 'Light+', action: () => setTheme('light+') },
            { label: 'Monokai', action: () => setTheme('monokai') },
          ],
        },
      ],
    },
    { label: 'Toggle Full Screen', shortcut: 'F11', action: () => toggleFullScreen() },
  ],
}

Cascading submenu behavior:

  • Items with children display a chevron indicator (▸) instead of a keyboard shortcut
  • Child menus open to the right by default, flipping to the left if there isn't enough viewport space
  • Hover delay (200ms) prevents accidental opening/closing when moving the mouse across items
  • Clicking a leaf item closes all menus (including all parent levels)
  • Keyboard: ArrowRight opens a child submenu, ArrowLeft closes it and returns to the parent
  • Works at any nesting depth (2–3 levels recommended for good UX)

Keyboard Shortcuts

Shortcuts are defined as strings using Windows-style notation. The titlebar automatically converts them to macOS symbols at render time — no need to provide separate strings per OS.

Conversion table:

| Input | Windows/Linux | macOS | |-------|---------------|-------| | Ctrl+N | Ctrl+N | ⌘N | | Ctrl+Shift+N | Ctrl+Shift+N | ⇧⌘N | | Ctrl+Alt+S | Ctrl+Alt+S | ⌥⌘S | | Alt+F4 | Alt+F4 | ⌥F4 | | Ctrl+Shift+Alt+N | Ctrl+Shift+Alt+N | ⌥⇧⌘N | | F11 | F11 | F11 | | Ctrl+` | Ctrl+` | ⌘` | | Ctrl+, | Ctrl+, | ⌘, |

Modifier mapping (macOS):

| Input | Symbol | Name | |-------|--------|------| | Ctrl | ⌘ | Command | | Alt | ⌥ | Option | | Shift | ⇧ | Shift | | Control | ⌃ | Control | | Meta / Cmd | ⌘ | Command |

Modifiers are sorted in standard macOS order: ⌃ ⌥ ⇧ ⌘

Keyboard Navigation

When a dropdown menu is open, the following keyboard controls are available:

| Key | Action | |-----|--------| | Arrow Down | Focus next actionable item (skips separators and disabled items) | | Arrow Up | Focus previous actionable item | | Arrow Right | Open a child submenu if the focused item has one, otherwise move to the next top-level menu | | Arrow Left | Close the current child submenu and return to parent, or move to the previous top-level menu | | Enter | Activate the focused item, or open its child submenu if it has one | | Escape | Close the current dropdown |

Hover switching is also supported — hovering over another top-level menu label while one is open will switch to it instantly.

Responsive Overflow

When the window is too narrow to display all menu items, the menu automatically collapses items one by one into an overflow button (). Items restore individually as the window grows back.

  • Items collapse right-to-left (rightmost items overflow first)
  • The overflow dropdown groups hidden items under their original menu labels
  • Keyboard navigation seamlessly transitions between visible menus and the overflow dropdown
  • Dropdown menus scroll with a styled scrollbar when they exceed the viewport height

This behavior is fully automatic — no configuration needed.

Toolbar Actions

The titlebar supports a customizable toolbar actions area positioned between the search bar and the user profile. Use it for notifications, settings, upgrade indicators, or any custom action buttons.

Defining Actions

Pass an array of TitlebarAction objects to the actions prop. Each action renders as an icon button that can have a badge, tooltip, dropdown, or highlight effect.

import Titlebar from '@drycstud.io/electron-titlebar';
import type { TitlebarAction } from '@drycstud.io/electron-titlebar';
import { FiBell, FiSettings, FiZap, FiCheckCircle, FiPackage } from 'react-icons/fi';

const actions: TitlebarAction[] = [
  {
    id: 'notifications',
    icon: <FiBell />,
    tooltip: 'Notifications',
    badge: 3,
    badgeVariant: 'attention',
    dropdown: [
      { label: 'Mark all as read', icon: <FiCheckCircle />, action: () => {} },
      { label: '', type: 'separator' },
      { label: 'New deployment completed', icon: <FiPackage />, action: () => {} },
    ],
  },
  {
    id: 'settings',
    icon: <FiSettings />,
    tooltip: 'Settings',
    onClick: () => openSettings(),
  },
  {
    id: 'upgrade',
    icon: <FiZap />,
    label: 'Update v2.1.0',
    variant: 'filled',
    tooltip: 'Update Available',
    badgeVariant: 'success',
    onClick: () => downloadAndUpdate(),
    dropdown: [
      { label: 'Download & Update Now', icon: <FiRefreshCw />, action: () => {} },
      { label: 'Download Only', icon: <FiDownload />, action: () => {} },
      { label: '', type: 'separator' },
      { label: 'Release Notes', icon: <FiPackage />, action: () => {} },
    ],
  },
];

<Titlebar title="My App" actions={actions} />

Action features:

| Feature | Prop | Description | |---------|------|-------------| | Icon | icon | Any React node (e.g., react-icons). Required. | | Label | label | Text label displayed next to the icon (used with variant: 'filled'). | | Variant | variant | 'icon' (default) for compact icon buttons, 'filled' for medium colored split buttons. | | Tooltip | tooltip | Text shown on hover. Also used as aria-label. | | Count badge | badge: number | Rounded pill showing the count (capped at 99+). | | Dot badge | badge: true | Small colored dot indicator. | | Badge color | badgeVariant | 'default' (blue), 'attention' (red), or 'success' (green). Also colors filled buttons. | | Highlight | highlight: true | Pulsing glow animation for "needs attention" states (icon variant only). | | Click | onClick | Callback fired on click (when no dropdown, or on the main part of a split button). | | Dropdown | dropdown | Array of items shown in a dropdown menu. | | Disabled | disabled: true | Grays out the button and blocks interaction. |

Dropdown keyboard navigation:

| Key | Action | |-----|--------| | Arrow Down | Focus next actionable item (skips separators and disabled) | | Arrow Up | Focus previous actionable item | | Enter | Activate the focused item | | Escape | Close the dropdown |

Split Button (Filled Variant)

When variant: 'filled' is set, the action renders as a medium-sized colored button with a text label. If a dropdown array is also provided, a chevron divider appears on the right side, creating a split button:

  • Main area — fires onClick (e.g., "Download & Update Now")
  • Chevron — opens the dropdown with alternative options (e.g., "Download Only", "Release Notes")

The button color is controlled by badgeVariant ('default' = blue, 'attention' = red, 'success' = green).

const updateAction: TitlebarAction = {
  id: 'update',
  icon: <FiZap />,
  label: 'Update v2.1.0',
  variant: 'filled',
  badgeVariant: 'success',
  onClick: () => downloadAndUpdateNow(),
  dropdown: [
    { label: 'Download & Update Now', icon: <FiRefreshCw />, action: () => {} },
    { label: 'Download Only', icon: <FiDownload />, action: () => {} },
    { label: '', type: 'separator' },
    { label: 'Release Notes', icon: <FiPackage />, action: () => {} },
  ],
};

Without a dropdown, the filled variant renders as a single solid button (no chevron).

Custom Dropdown Content

For advanced dropdowns (e.g., a rich notification panel), use renderDropdown instead of the dropdown array. This gives you full control over the dropdown content:

const notificationsAction: TitlebarAction = {
  id: 'notifications',
  icon: <FiBell />,
  tooltip: 'Notifications',
  badge: 3,
  badgeVariant: 'attention',
  dropdownWidth: 360,
  renderDropdown: (close) => (
    <NotificationPanelRoot>
      <NotificationHeader>
        <NotificationTitle>Notifications</NotificationTitle>
        <NotificationHeaderActions>
          <NotificationHeaderButton onClick={() => markAllRead()}>
            Mark all read
          </NotificationHeaderButton>
        </NotificationHeaderActions>
      </NotificationHeader>
      <NotificationList>
        <NotificationItem>
          <NotificationIcon style={{ color: '#22C55E' }}>
            <FiCheck />
          </NotificationIcon>
          <NotificationContent>
            <NotificationItemTitle>Deploy succeeded</NotificationItemTitle>
            <NotificationDescription>Production v2.1.0 is live</NotificationDescription>
            <NotificationMeta>2 minutes ago</NotificationMeta>
          </NotificationContent>
        </NotificationItem>
      </NotificationList>
      <NotificationFooter>
        <NotificationFooterButton onClick={close}>Dismiss all</NotificationFooterButton>
      </NotificationFooter>
    </NotificationPanelRoot>
  ),
};

| Prop | Type | Description | |------|------|-------------| | renderDropdown | (close: () => void) => ReactNode | Custom dropdown content. Receives a close callback to dismiss the dropdown. | | dropdownWidth | number \| string | Width of the dropdown container (e.g., 360 or '360px'). |

When both renderDropdown and dropdown are provided, renderDropdown takes precedence.

Notification Panel Components

The library exports a set of pre-styled building blocks for notification panels. Import them from the main entry point:

import {
  NotificationPanelRoot,
  NotificationHeader, NotificationTitle, NotificationHeaderActions, NotificationHeaderButton,
  NotificationList, NotificationItem, NotificationDot, NotificationIcon,
  NotificationContent, NotificationItemTitle, NotificationDescription, NotificationMeta,
  NotificationBadge, NotificationSeparator,
  NotificationFooter, NotificationFooterButton,
  NotificationEmpty, NotificationEmptyText,
  NotificationGroup, NotificationGroupLabel,
} from '@drycstud.io/electron-titlebar';

These are headless-ish styled components — compose them freely to build any notification UI.

Custom Render Actions

For fully custom content in the actions area, use the renderActions escape hatch:

<Titlebar
  title="My App"
  renderActions={() => (
    <button style={{ padding: '4px 10px', borderRadius: '4px', background: '#4F46E5', color: '#fff', fontSize: '11px' }}>
      Upgrade to Pro
    </button>
  )}
/>

You can combine actions and renderActions — both will render side by side.

User Profile

The titlebar includes a user profile section with avatar, status indicator, and dropdown actions.

<Titlebar
  title="My App"
  user={{ name: 'Jane Smith', email: '[email protected]', status: 'online' }}
  userActions={[
    { label: 'My Account', action: () => {} },
    { label: 'Settings', action: () => {} },
    { type: 'separator', label: 'sep' },
    { label: 'Switch Workspace', action: () => {} },
  ]}
  onSignOut={() => console.log('Sign out')}
/>

When no user is provided but onSignIn is set, a "Sign In" button appears instead.

Command Palette

The titlebar supports a searchable command palette (similar to VS Code's Ctrl+K or Postman's search). Configure it via the commandPalette prop:

<Titlebar
  title="My App"
  commandPalette={{
    placeholder: 'Search commands...',
    shortcut: 'Ctrl+K',
    sections: [{ id: 'actions', title: 'Quick Actions', items: [...] }],
    onQueryChange: (query) => filterResults(query),
  }}
/>

API Reference

Titlebar Component

import Titlebar from '@drycstud.io/electron-titlebar';

Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | title | string \| null | 'Pretty Titlebar' | Window title text displayed in the center. Pass null to hide. | | logo | string | Built-in logo | Path or URL to the logo image (Windows/Linux only). | | size | 'default' \| 'small' | 'default' | Height of the titlebar. 'default' = 38px, 'small' = 32px. | | platform | 'windows' \| 'macos' \| 'linux' | Auto-detected | Override platform detection. Useful for testing or Storybook. | | menuItems | MenuItem[] | undefined | Array of menu definitions for the menu bar. | | user | UserInfo \| null | undefined | User info for the profile avatar. Pass null to show sign-in button. | | userActions | UserProfileAction[] | undefined | Custom actions in the user profile dropdown. | | onSignIn | () => void | undefined | Shows a "Sign In" button when no user is set. | | onSignOut | () => void | undefined | Adds a "Sign Out" item to the user dropdown. | | commandPalette | CommandPaletteConfig | undefined | Configuration for the searchable command palette. | | actions | TitlebarAction[] | undefined | Array of toolbar action buttons (notifications, settings, etc.). | | renderActions | () => ReactNode | undefined | Escape hatch for fully custom JSX in the actions area. | | onMinus | () => void | Built-in minimize | Custom handler for the minimize button. | | onMinimizeMaximaze | () => void | Built-in max/restore | Custom handler for the maximize/restore button. | | onClose | () => void | Built-in close | Custom handler for the close button. |

Types

All types are exported from the main entry point:

import type {
  TitlebarProps,
  Platform,
  MenuItem,
  SubMenuItem,
  MenuItemAction,
  TitlebarAction,
  TitlebarActionDropdownItem,
  ToolbarActionsProps,
  UserInfo,
  UserStatus,
  UserProfileAction,
  CommandPaletteConfig,
  CommandPaletteSection,
  CommandPaletteItem,
  FilterChip,
  CommandPaletteFooterAction,
} from '@drycstud.io/electron-titlebar';
type Platform = 'windows' | 'macos' | 'linux';

type MenuItemAction = () => void;

type MenuItem = {
  label: string;
  action?: MenuItemAction;
  submenu?: MenuItem[];   // recursive — enables cascading submenus
  disabled?: boolean;
  type?: 'separator';
  shortcut?: string;
};

// SubMenuItem is a deprecated alias for MenuItem (kept for backward compatibility)
type SubMenuItem = MenuItem;

type TitlebarAction = {
  id: string;
  icon: ReactNode;
  label?: string;                 // text label (used with variant: 'filled')
  variant?: 'icon' | 'filled';   // 'icon' (default) or 'filled' (colored split button)
  tooltip?: string;
  badge?: number | boolean;       // number = count badge, true = dot indicator
  badgeVariant?: 'default' | 'attention' | 'success';
  highlight?: boolean;            // pulsing glow effect (icon variant only)
  onClick?: () => void;
  dropdown?: TitlebarActionDropdownItem[];
  renderDropdown?: (close: () => void) => ReactNode; // custom dropdown content
  dropdownWidth?: number | string;                    // custom dropdown width
  disabled?: boolean;
};

type TitlebarActionDropdownItem =
  | { label: string; icon?: ReactNode; action: () => void; disabled?: boolean }
  | { label: string; type: 'separator' };

type UserInfo = {
  name: string;
  email?: string;
  avatar?: string;
  status?: 'online' | 'away' | 'busy' | 'offline';
};

type UserProfileAction =
  | { label: string; action: () => void }
  | { label: string; type: 'separator' };

type TitlebarProps = {
  title?: string | null;
  logo?: string;
  size?: 'default' | 'small';
  platform?: Platform;
  menuItems?: MenuItem[];
  user?: UserInfo | null;
  userActions?: UserProfileAction[];
  onSignIn?: () => void;
  onSignOut?: () => void;
  commandPalette?: CommandPaletteConfig;
  actions?: TitlebarAction[];
  renderActions?: () => ReactNode;
  onMinus?: () => void;
  onMinimizeMaximaze?: () => void;
  onClose?: () => void;
};

Config Functions

All config functions are imported from the /config entry point:

import {
  setup,
  getTitlebarOptions,
  attachToWindow,
  preloadConfig,
} from '@drycstud.io/electron-titlebar/config';

setup()

Validates the Electron environment. Call this at the top level of your main process file, before creating any windows.

setup();

getTitlebarOptions()

Returns the correct BrowserWindow constructor options for the current platform. Spread the result into your BrowserWindow options.

const options = getTitlebarOptions();
// macOS:         { frame: false, titleBarStyle: 'hiddenInset', trafficLightPosition: { x: 10, y: 10 } }
// Windows/Linux: { frame: false, titleBarStyle: 'hidden' }

attachToWindow(ipcMain, mainWindow)

Registers IPC handlers on the given BrowserWindow for the titlebar's window control buttons. Call this after the window is ready.

| Parameter | Type | Description | |-------------|-----------------|-------------| | ipcMain | IpcMain | Electron's ipcMain instance | | mainWindow | BrowserWindow | The BrowserWindow instance to control |

Registered IPC channels:

| Channel | Direction | Description | |---------|-----------|-------------| | minimizeWindow | Renderer → Main | Minimizes the window | | maximizeRestoreWindow | Renderer → Main | Toggles between maximize and restore | | closeWindow | Renderer → Main | Closes the window | | windowsIsMaximized | Renderer → Main | Returns whether the window is currently maximized |

preloadConfig()

Sets up the contextBridge to expose Electron's IPC renderer API to the browser window. Call this in your preload script.

preloadConfig();

Utility Functions

formatShortcut(shortcut, platform)

Converts a keyboard shortcut string to the platform-specific format. You can use this outside the titlebar for your own UI:

import { formatShortcut } from '@drycstud.io/electron-titlebar';

formatShortcut('Ctrl+Shift+N', 'macos');   // '⇧⌘N'
formatShortcut('Ctrl+Shift+N', 'windows'); // 'Ctrl+Shift+N' (unchanged)
formatShortcut('Alt+F4', 'macos');         // '⌥F4'
formatShortcut('Ctrl+S', 'macos');         // '⌘S'

| Parameter | Type | Description | |-----------|------|-------------| | shortcut | string | Shortcut in Windows notation (e.g., 'Ctrl+Shift+N') | | platform | Platform | Target platform ('windows', 'macos', or 'linux') | | Returns | string | Formatted shortcut string |

Platform Behavior

The titlebar automatically adapts its appearance and behavior based on the detected operating system:

| Feature | macOS | Windows | Linux | |---------|-------|---------|-------| | Window controls | Native traffic lights | Custom buttons | Custom buttons | | App logo | Hidden | Shown | Shown | | Menu position | After traffic lights area | After logo | After logo | | Search bar | Centered (absolute) | Inline (flex) | Inline (flex) | | Toolbar actions | Between search and profile | Between search and profile | Between search and profile | | User profile | Right side | Before window controls | Before window controls | | Shortcut format | ⌘⇧N (symbols) | Ctrl+Shift+N (text) | Ctrl+Shift+N (text) | | Title position | Centered | Centered | Centered | | Titlebar left padding | 70px (traffic lights) | 0 | 0 |

macOS

  • Uses Electron's titleBarStyle: 'hiddenInset' to display native traffic light buttons (close, minimize, maximize) on the left
  • The custom titlebar renders the menu bar and centered title
  • Logo and custom window control buttons are hidden (the native controls handle everything)
  • The titlebar container has left padding (70px) to avoid overlapping the traffic lights

Windows

  • Uses frame: false with titleBarStyle: 'hidden' for a fully custom titlebar
  • Renders logo on the left, menus, title in the center, and minimize/maximize/close buttons on the right
  • Buttons use hover effects (subtle highlight on hover, red on close hover — matching the Windows standard)

Linux

  • Same behavior as Windows (custom buttons on the right)

Overriding Platform Detection

You can force a specific platform style using the platform prop:

<Titlebar title="My App" platform="macos" />

This is particularly useful for:

  • Storybook — Preview different platform styles without running on that OS
  • Testing — Ensure correct rendering for each platform in unit tests
  • Debugging — Verify layout without switching machines

CSS Framework Compatibility

The titlebar is designed to never conflict with your app's CSS framework:

  • All CSS resets (margin, padding, box-sizing, button styles) are scoped inside the titlebar container using & * and & button selectors
  • Button styles use doubled CSS specificity (&&) to override any external resets
  • No global *, html, body, or button resets are injected into the document
  • The only global change is a paddingTop on <html> (via react-helmet-async) to reserve space for the fixed titlebar

This means the titlebar works cleanly with:

  • Tailwind CSS (including Preflight)
  • CSS Modules
  • Styled Components / Emotion
  • Vanilla CSS
  • Any other CSS-in-JS or utility-class framework

Custom Window Action Handlers

If you need custom behavior for the window control buttons (e.g., showing a confirmation dialog before closing), pass handler functions via props:

import Titlebar from '@drycstud.io/electron-titlebar';

export default function App() {
  const handleClose = () => {
    const confirmed = globalThis.confirm('Are you sure you want to quit?');
    if (confirmed) {
      globalThis.electron.ipcRenderer.send('closeWindow');
    }
  };

  return (
    <Titlebar
      title="My App"
      onClose={handleClose}
      onMinus={() => {
        console.log('Minimizing...');
        globalThis.electron.ipcRenderer.send('minimizeWindow');
      }}
      onMinimizeMaximaze={() => {
        globalThis.electron.ipcRenderer.send('maximizeRestoreWindow');
      }}
    />
  );
}

When a custom handler is provided, the titlebar will call your function instead of the built-in IPC action. This gives you full control over each button's behavior.

Note: On macOS, custom handlers are not used since the native traffic light buttons handle window actions directly through the OS.

Storybook

The library includes interactive Storybook documentation for all components. To run it locally:

# From the monorepo root
yarn storybook

# Or from the library directory
cd libs/electron-titlebar
yarn storybook

Available Stories

| Component | Stories | Description | |-----------|---------|-------------| | Titlebar | Default, MacOS, Linux, SmallSize, WithMenuWindows, WithMenuMacOS, SmallWithMenu, CustomHandlers, NoTitle, LongTitle, WithUserProfileWindows, WithUserProfileMacOS, LoggedOutState, WithToolbarActionsWindows, WithToolbarActionsMacOS | All platform variants, sizes, menus, user profiles, toolbar actions | | ToolbarActions | NotificationsBell, SettingsButton, UpgradeHighlight, FullToolbar, HighBadgeCount, DisabledAction, DefaultBadgeVariant, CustomRenderActions, MixedActionsAndCustom, UpdateSplitButton, UpdateSplitButtonAttention, FilledButtonNoDropdown, FilledButtonDisabled, FullToolbarWithSplitButton | Badge variants, dropdowns, highlight effects, filled split buttons, custom render | | ActionButton | Minimize, Maximize, Restore, Close | Each window control button with its icon and variant | | Menu | WindowsShortcuts, MacOSShortcuts, DisabledItems, DisabledTopLevel, SingleMenu, NoShortcuts | Shortcut formatting per platform, disabled states, variations | | WindowControls | WindowsDefault, WindowsMaximized, WindowsNoMenu, MacOnly, MacOnlyNoMenu | Layout modes for each platform with/without menus |

Documentation Pages

  • Getting Started / Introduction — Overview, quick start, platform comparison
  • Getting Started / API Reference — Full props, types, config, and utility reference
  • Guides / Keyboard Shortcuts — Shortcut conversion table, formatShortcut usage, navigation keys

To build static Storybook docs:

yarn storybook:build

Examples

A complete working example is available in the repository:

  • with-electron-vite — Full Electron + Vite + React integration with complete menu system (File, Edit, View, Terminal, Window, Help)

To run the example locally:

git clone https://github.com/drycstudio/drystud.io.git
cd drystud.io
yarn install
yarn build          # Build the titlebar library first
cd examples/with-electron-vite
yarn dev            # Start the Electron app

Peer Dependencies

Make sure these are installed in your project:

| Package | Version | |---------------|----------------------| | electron | >=31.0.0 | | react | ^18.3.1 \|\| ^19.0.0 | | react-dom | ^18.3.1 \|\| ^19.0.0 | | react-icons | ^5.2.1 |

License

MIT