A panel above the menu bar: NSPanel, window levels and when it may take focus
The window recipe a notch app uses, line by line, with the three problems that shaped it
TL;DR
- A window that sits in the notch area needs four decisions: a panel subclass with a
.nonactivatingPanelstyle, a window level above the menu bar, a collection behavior that follows the user across Spaces and full screen apps, and a rule for when it may become the key window. - NotchStar’s recipe is 10 lines of setup in
NotchPanel.swift. The window level isNSWindow.Level.mainMenu.rawValue + 3, which is 27 on macOS 27.0.1 (measured on 4 November 2026, method below). - Three bugs shaped the design: a shadow that showed as a black block, a strip wider than the notch that covered menu bar items, and a fading window that caught the pointer. Each fix is a rule in the code today.
The problem: a window that must be there and must not be noticed
A notch app has two jobs that pull in opposite directions. The window has to be on screen all the time, flush against the top edge, at the exact size of the hardware notch. And it must never behave like a window. It should not take focus from the document being typed in, it should not appear in the window cycle, and it should not cover the menu bar items on either side of the notch.
A normal NSWindow fails all of that. It activates its app when clicked, it sits at level 0, it belongs to one Space, and it has a title bar. SwiftUI’s WindowGroup and MenuBarExtra do not fit either, because neither gives control over the window level or the exact frame. The workable tool is AppKit: an NSPanel subclass hosting a SwiftUI view.
This post walks through the real panel in NotchStar, a native macOS app (Swift, SwiftUI and AppKit) whose notch holds ten small tools. The source is Sources/NotchStar/Notch/NotchPanel.swift and NotchController.swift. Everything quoted is from the repository, and anything that is not verified says so.
The recipe
The whole panel class is short. This is the real one:
final class NotchPanel: NSPanel {
var allowsKey = false
override var canBecomeKey: Bool { allowsKey }
override var canBecomeMain: Bool { false }
init() {
super.init(contentRect: .zero, styleMask: [.borderless, .nonactivatingPanel],
backing: .buffered, defer: false)
isFloatingPanel = true
level = NSWindow.Level(rawValue: NSWindow.Level.mainMenu.rawValue + 3)
backgroundColor = .clear
isOpaque = false
hasShadow = false
collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary, .stationary, .ignoresCycle]
setAccessibilityIdentifier("notch")
}
}
Each line answers one question.
| Line | What it does | What happens without it |
|---|---|---|
.borderless | No title bar, no frame, no traffic lights | A window with chrome on top of the notch |
.nonactivatingPanel | Clicking the panel does not make the app the active app | Every click in the notch would pull focus away from the app being used |
isFloatingPanel = true | The panel floats above normal windows | Normal windows can cover it |
level = mainMenu + 3 | Places it above the menu bar | The menu bar draws over it |
backgroundColor = .clear, isOpaque = false | The window itself is transparent, so the black notch shape is drawn by SwiftUI | A rectangular opaque window |
hasShadow = false | No system shadow; the app draws its own | A system shadow around a rectangle |
canJoinAllSpaces, fullScreenAuxiliary | Appears on every Space and beside full screen apps | The notch exists on one desktop only |
stationary, ignoresCycle | Stays put during Mission Control and is skipped by the window cycle | It animates away or shows up in the cycle |
canBecomeMain = false | Never the main window | It could take the main role from the user’s document |
The app is also an agent app: LSUIElement is true in project.yml, so there is no Dock icon. The panel is the main visible surface, plus two siblings that use the same recipe.
Window levels: what 27 means
Window levels are plain integers. The panel’s level is expressed relative to a named one, so the number is easy to lose. The following script prints the named levels on the Mac used for this post:
import AppKit
for (n, l) in [("normal", NSWindow.Level.normal), ("floating", .floating),
("statusBar", .statusBar), ("mainMenu", .mainMenu),
("popUpMenu", .popUpMenu), ("screenSaver", .screenSaver)] {
print(n, l.rawValue)
}
print("notch", NSWindow.Level.mainMenu.rawValue + 3)
Output on 4 November 2026, macOS 27.0.1:
normal 0
floating 3
statusBar 25
mainMenu 24
popUpMenu 101
screenSaver 1000
notch 27
So the panel sits three above the main menu level and two above the status bar level, and well below pop up menus at 101. The code does not record why the offset is 3 and not 1. A reasonable reading is that it is above the menu bar and its status items with a little room, but that is an inference from the numbers, not a note in the source.
The panel does not use anything near .popUpMenu, which is 101 on this Mac. How a menu opened from a status item stacks against it was not tested.
When the panel may take focus
The hard rule for a nonactivating panel is that it can be clicked without activating the app, and it can still receive keystrokes if it becomes the key window. Becoming key is what a text field needs. Quick Notes, the Clipboard search field, the Teleprompter script editor and the Disk Find box all need typing, and the other tools do not.
The tool protocol carries that as a single property:
/// True when the tool needs typed input; the panel then becomes key (still without activating the app).
var wantsKeyboard: Bool { get }
The controller applies it whenever the tool or the open state changes:
private func updateKeyboard() {
let wants = model.state == .open && model.selectedTool?.wantsKeyboard == true
panel.allowsKey = wants
if wants { panel.makeKey() } else if panel.isKeyWindow { panel.resignKey() }
}
Two details matter. First, canBecomeKey returns allowsKey, so the panel can only become key when the controller has said yes. allowsKey is set before makeKey() is called, so canBecomeKey already answers yes when the call is made. Second, closing the notch always resigns key, so a closed notch never holds keyboard focus. There is a test for exactly that:
func testKeyboardOnlyWhenOpenOnATextTool() {
let c = makeController()
c.model.select("home")
c.send(.hoverEntered)
XCTAssertFalse(c.panel.allowsKey)
c.model.select("note")
XCTAssertTrue(c.panel.allowsKey)
// ... clipboard true, system false ...
c.send(.hoverExited)
XCTAssertFalse(c.panel.allowsKey, "closed notch must never take keyboard focus")
}
Keyboard shortcuts in a text field need one more thing. An agent app has no Edit menu, and Command A, C, V, X and Z are routed through the menu. The 1.0.4 changelog (11 October 2026) records the fix: Command A, C, V, X and Z did nothing in Quick Note, Clipboard search, the Teleprompter script or the license key box until the app installed a hidden Edit menu, which NotchStarApp.swift does with NSApp.mainMenu = Self.editMenu().
Frames: one panel, two sizes
The panel is one window that resizes. The frame comes from the screen, never from a constant:
let size = model.state == .open ? NotchModel.openSize : model.metrics.closedSize
let m = model.state == .open ? NotchModel.shadowMargin : 0
let w = size.width + 2 * m, h = size.height + m
panel.setFrame(CGRect(x: screen.frame.midX - w / 2, y: screen.frame.maxY - h, width: w, height: h), display: true)
The closed size comes from NotchGeometry.metrics: the screen width minus the left and right auxiliary areas gives the notch width, and safeAreaInsets.top gives its height. On a Mac with no notch the fallback is a 190 by 24 point pill. The open size is 660 by 296 points. The panel gets a 30 point transparent margin on the left, right and bottom only while open, so its shadow is not clipped. That makes the open window 720 by 326 points, a derived number from the three constants.
The screen is chosen as the first one with a nonzero top safe area inset, else the main screen:
NSScreen.screens.first(where: { $0.safeAreaInsets.top > 0 }) ?? NSScreen.main
A test pins the closed geometry: the panel must be no wider than the notch (within 1 point of rounding), flush to the top edge and centered on the screen.
closed open
+---------------+ +----------------------------------+
| notch size | | 660 x 296 shape |
+---------------+ | (window is 720 x 326: 30 pt of |
window == notch | transparent margin, three sides)|
+----------------------------------+
Wrong turns that became rules
The source holds three problems that are written down in comments, commit messages or tests. Each is a design rule now.
The shadow showed as a black block. The open notch draws its own drop shadow, a copy of the open shape with a blur. While the shape was still growing, the shadow was already at full size, and it showed as a black rectangle under the notch. The fix is in the model and the controller: shadowVisible is false whenever the state changes, and only becomes true 400 ms after opening, when the spring has settled. The view comment says it plainly: “it may only be visible once the real notch has finished growing”. The commit that introduced the rule is 969c58b (7 October 2026), with regression tests.
A wide strip covered menu bar items. The screenshot strip hangs under the closed notch and can be wider than the notch. Placed in the notch panel, it would have made a closed window wider than the hardware notch, and a window above the menu bar swallows clicks on whatever it covers. The strip got its own panel instead, and a test guards the original, and its comment says the closed panel must be no wider than the notch itself or it would swallow clicks on menu bar items. The controller comment says the same thing: “so the strip can be wider than the notch without covering the menu bar items beside it.”
A fading window caught the pointer. When the notch opens, the strip should disappear. The strip normally fades out when its last screenshot is used. When the notch opens, the code removes it at once, and the comment explains why: “A fading window would sit over the opening notch and catch the pointer for the next 0.4 s.” A 0.4 second window that is invisible but still hit tested is a good example of why a floating panel above everything needs its own lifecycle.
The common thread is that an overlay at level 27 wins every hit test it covers. The cure is to keep each window as small as its job, and to take it off screen with orderOut instead of making it transparent.
Events without Accessibility permission
The panel is not the only way the app watches the pointer. The controller installs global mouse monitors for clicks and drags outside its own windows:
// Global *mouse* monitors need no Accessibility permission (only key events do).
monitor([.leftMouseDown, .rightMouseDown]) { $0.mouseDown() }
monitor(.leftMouseDragged) { $0.dragMoved() }
monitor(.leftMouseUp) { $0.dragEnded() }
The comment in the source records the rule the app relies on: global mouse monitors need no Accessibility permission, and only key events do. That is why this app does not ask for Accessibility access. A global monitor reports events that go to other apps. The monitors serve two features: closing the notch when a click lands outside it (only in click mode), and opening the shelf when a file drag nears the notch, which uses the drag pasteboard’s change count to tell a new drag from an old one.
Numbers, with the method
| Fact | Value | Source |
|---|---|---|
| Panel level | 27 | NSWindow.Level.mainMenu.rawValue + 3, printed on macOS 27.0.1 on 4 November 2026 |
| Closed size, notched Mac | screen width minus both auxiliary areas, by the top safe area height | NotchGeometry.metrics |
| Closed size, no notch | 190 by 24 points | NotchGeometry.pillSize |
| Open shape | 660 by 296 points | NotchModel.openSize |
| Open window | 720 by 326 points | shape plus a 30 point margin on three sides, derived |
| Shadow delay | 400 ms | shadowTask in NotchController.send |
| Close shrink delay | 420 ms | closeTask in NotchController.send |
| Panels using the recipe | 3 | NotchPanel, ScreenshotStripPanel, ActivityPanel |
No performance claims are made here. Idle CPU, memory and energy for the app have not been measured.
What is still bad
- Three panel classes carry the same ten lines. A shared base class or a small factory would stop them drifting apart.
- The offset 3 is unexplained in the source. Nothing breaks if it is wrong, but nothing documents it either.
- The panel was verified through unit tests that drive the real controller and through UI tests that launch the real app, and the owner reports that all features work on their Mac. Multiple displays, a Mac without a notch, full screen apps and Spaces have not been reported, so
fullScreenAuxiliaryandcanJoinAllSpacesare set from the documentation and not confirmed there. - Choosing “the first screen with a top inset” assumes at most one notched display.
Try it yourself
- Create an app with
LSUIElementset totrueso it has no Dock icon. - Subclass
NSPanelwith the style mask[.borderless, .nonactivatingPanel], then copy the table above: floating, a level above.mainMenu, clear background, the collection behavior. - Override
canBecomeKeyto return a Bool you control, andcanBecomeMainto return false. - Host a SwiftUI view with
NSHostingViewas the content view. - Position it with
setFrame, computing the origin fromscreen.frame.maxYminus the height, so it hangs from the top edge. - Print
NSWindow.Level.mainMenu.rawValueon your Mac and choose a level with a reason you can write down. - Click it while a text editor is active. The editor should keep its caret.
Cheat sheet
- Style mask includes
.nonactivatingPanel -
isFloatingPanelis true and the level is above.mainMenu - The window is transparent and draws its own shape
-
canBecomeKeyis false until a text tool asks for it, and closing always resigns key -
collectionBehaviorhas.canJoinAllSpacesand.fullScreenAuxiliary - A closed window is never wider than the notch, so it cannot cover menu bar items
- A window that must go away is taken off screen, not faded while it still takes clicks
- Global mouse monitors are used instead of key monitors, so no Accessibility prompt
- A hidden Edit menu exists so Command A, C, V, X and Z work in text fields
Close
NotchStar is the app these panels belong to. If you want to try it, it is at notchstar.com.
NotchStar is a native Mac notch app, $4 once, with a 7 day free trial. Try it, or read the ten tools.