A menu bar app is a strange shape of software. There is no main window, no document, no place for a user to go and look around. There is an icon in the top right corner and one panel that appears for a few seconds at a time. Everything you want the app to be has to fit in that panel.
We build several of these, and the lessons that mattered were not about frameworks. They were about where the window shows up, whether it can accept a keystroke, how big it is, and when it is allowed to disappear.
Open where the person is already looking
The obvious behavior is to drop the panel under the menu bar icon, because that is where the click happened. It is also wrong most of the time. Someone triggering a menu bar app by keyboard shortcut is looking at the middle of their screen, in another app, at the cursor. Making them travel to the top right corner and back is a small cost paid on every single use.
Opening at the cursor sounds trivial and has a few sharp edges. The mouse location comes back in a screen coordinate space whose origin is not where SwiftUI thinks it is, so a naive conversion puts your panel on the wrong half of the display. With multiple monitors you have to find which screen actually contains the point rather than assuming the main one. And you want the visible frame of that screen, not its full frame, so the panel does not tuck itself under the menu bar or behind the Dock. Then clamp: if the panel would run off an edge, slide it back in rather than letting it hang.
It is maybe forty lines of code and it changes how the app feels more than anything else on this list. Voice opens its panel right under the cursor in whatever app you are in, and the reason is that a dictation tool you have to go find is a dictation tool you stop using.
Take the keyboard without taking the focus
This is the problem that costs people a weekend. You want a panel that can accept typing, arrow keys, and Escape. You do not want the app behind it to lose focus, because the whole point is that the user is in the middle of working somewhere else and your panel is a brief visitor.
Two behaviors have to be separated. A window becoming key means it receives keyboard events. An application activating means it comes to the front and the previous app's window dims and gives up its insertion point. The default AppKit window ties those together, and a borderless window refuses to become key at all, which is why a text field in a custom panel can silently ignore every keystroke while looking perfectly normal.
The combination that works is a panel with the non-activating style, an app that runs as an accessory with no Dock icon, and an explicit override so the window is allowed to become key. Two more settings are worth knowing. One lets the panel become key only when a view in it actually needs input, so a click on a button does not pull focus for no reason. The other lets the window join all Spaces and float over full-screen apps, without which your panel is invisible to anyone who works full screen, which is a lot of people.
Size the panel to the content, not the content to the panel
A menu bar panel has no business being a fixed rectangle. It shows a short transcript or a long one, an empty state or a list, and a window sized for the largest case looks broken in the smallest.
Measure the content and resize the window to it. The practical problems are all about motion:
- Pick an anchor and keep it. If the panel grows and the window is positioned by its origin, the whole thing appears to jump. Decide which corner is nailed down and recompute the origin on every resize.
- Set a maximum and scroll past it. Content that can be arbitrarily long needs a ceiling, or one paste turns your panel into a full-height wall.
- Do not animate every change. Animating a resize that happens while text streams in produces a panel that visibly breathes. Animate deliberate state changes, snap the incremental ones.
- Reserve space for states you know are coming. If a row of buttons appears when a task finishes, laying that out from the start avoids a layout jolt at the exact moment the user is reading.
Dismiss on click-away, except when you must not
Closing when the user clicks somewhere else is correct default behavior. A menu bar panel is transient by nature and nothing is more annoying than an overlay you have to explicitly dismiss.
The exceptions are the ones that matter. Never disappear while work is in progress, and never disappear while holding something the user has not seen yet or has not decided about. If a panel is recording, or is showing a result the user is about to accept or discard, a stray click on the desktop should not throw that away silently. Voice keeps its transcript in a review step until you copy or discard it, precisely because the alternative is losing something you just said.
Our rule ended up simple. Click-away closes a panel that has nothing to lose. A panel with unreviewed state either stays open, or closes and keeps the state so reopening restores it. What it must never do is close and quietly drop what the user made.
Almost no chrome
A menu bar app should look closer to a system menu than to an application window. Not because minimalism is fashionable, but because chrome implies a place you can settle into, and this is not one. Title bars, toolbars, sidebars, and tab strips all make a promise the panel cannot keep in the four seconds it is on screen.
What earns its space: the content, the one or two actions that follow from it, and the keyboard hint next to those actions so the user learns to stop clicking. Everything else (settings, history, model choices, anything with more than a handful of options) belongs in a real settings window that opens when asked. Keeping those apart is what lets the panel stay small enough to read at a glance.
The takeaway
The hard parts of a menu bar app are not in the feature list. They are in the window: where it opens, whether it can hear the keyboard, how it grows, and when it is allowed to vanish. Get those four right and a small app feels like part of the system. Get them wrong and no amount of features makes it feel like anything but a floating box that keeps getting in the way.
Filed Under
Written by
Isaac Juracich
Full-stack engineer building production software for businesses that need it done right. Based in La Crosse, WI.
More about Isaac