#Start with the shape of the problem
The first thing to accept is that Apple gives you nothing to build on. There's no Dynamic Island framework, no notch API beyond a couple of geometry helpers, and no live-activity system on macOS at all. What you're really building is an illusion: a small, always-on-top window that sits under the physical camera housing and pretends to be part of it. Get the position, the shape, and the motion right and people read it as a system feature. Get any one of them wrong and it reads as a floating box.
What follows is the architecture NotchBay actually ships, not theory, and it is the same architecture behind every app in the Dynamic Island for Mac category. Four pieces carry it: the window and its three NSWindow properties, finding the notch, the animatable shape, and hit-testing. None is huge on its own; the difficulty is that all four have to be right at the same time.
#The panel: a borderless, transparent NSPanel
The container is a single NSPanel, not an NSWindow, because a panel can float above your other windows without stealing focus. Make it borderless and transparent, kill the shadow, and park it at a high window level so it stays above ordinary app windows:
// AppKit host for the island let panel = NSPanel( contentRect: rect, styleMask: [.borderless, .nonactivatingPanel], backing: .buffered, defer: false) panel.isOpaque = false panel.backgroundColor = .clear panel.hasShadow = false panel.level = .statusBar // above normal windows panel.collectionBehavior = [.canJoinAllSpaces, .stationary] panel.ignoresMouseEvents = true // pass clicks through by default
The .nonactivatingPanel flag is the one that saves you: without it, tapping the island yanks keyboard focus out of whatever you were doing. Everything visible, the resting pill, the expanded card, a music waveform, is a SwiftUI view hosted inside this panel with an NSHostingView. Keep the panel itself dumb and do all layout and animation in SwiftUI.
#level, ignoresMouseEvents and collectionBehavior
NSPanel is a subclass of NSWindow, so the three lines that decide how the island behaves are plain NSWindow properties, and they are worth understanding before anything gets drawn. Each one fixes a specific way an always-on-top window otherwise ruins the machine it runs on.
level sets the stacking order against every other window on screen. Ordinary app windows sit at .normal, and the island has to stay visible over them, so it goes to .statusBar, the band the menu bar it is pretending to belong to already occupies. Too low and a maximised window buries the island. Too high, above the alert and modal levels, and it draws on top of system dialogs, which is the worse failure of the two because the user cannot see what they are dismissing.
ignoresMouseEvents is the click-through switch, and it is the property that decides whether the app is polite. Set to true, the window is transparent to the mouse: clicks land on the menu bar and on whatever window is underneath, exactly as though the panel were not there. That is the correct resting state, because the island covers a strip of screen it does not own. Flip it to false only while a real control is on screen, then flip it straight back. It is a whole-window switch, not a per-pixel one, which is why the hit-testing further down exists.
collectionBehavior decides how the window relates to Spaces and Mission Control. .canJoinAllSpaces keeps the island on every desktop, which is what people expect, since the hardware notch is on every desktop too. .stationary stops it sliding sideways with the Space-switch animation and stops Mission Control gathering it up with the real windows. Without both flags the island vanishes the first time someone swipes to another desktop.
Specs at a glance
| Property | Value on the island | What it buys |
|---|---|---|
| level | .statusBar | Draws above ordinary app windows |
| ignoresMouseEvents | true when idle | Clicks fall through to the menu bar and the window behind |
| collectionBehavior | [.canJoinAllSpaces, .stationary] | On every Space, and it does not slide with the switch |
| styleMask | [.borderless, .nonactivatingPanel] | No title bar, never takes key focus |
| isOpaque, backgroundColor, hasShadow | false, .clear, false | Nothing visible but the SwiftUI content |
| Panel frame | 220×38 pt (16-inch), 185×32 pt (14-inch) | Matches the cutout, read from NSScreen at launch |
Every value is the configuration in the code above. The two frame sizes are what NSScreen reports on those two models, not a spec-sheet number.
#Finding the notch with NSScreen
You can't hardcode the notch position; it moves with screen size and scaled resolution. macOS exposes it through NSScreen. safeAreaInsets.top is the notch height, and auxiliaryTopLeftArea and auxiliaryTopRightArea are the two menu-bar slivers on either side of the camera, so the cutout width is the screen width minus both:
let screen = NSScreen.main! let notchHeight = screen.safeAreaInsets.top // 38 pt on a 16-inch if let l = screen.auxiliaryTopLeftArea, let r = screen.auxiliaryTopRightArea { let notchWidth = screen.frame.width - l.width - r.width // center a panel of this width at the top of the screen }
If safeAreaInsets.top comes back 0, that Mac has no notch, so either bail or fall back to a simulated pill near the menu bar. On real hardware this returns 220×38 points on the 16-inch MacBook Pro and 185×32 on the 14-inch; the full set across models is in the notch size breakdown, and the geometry the island is drawn to is written up on the design spec page. Recompute all of it whenever the screen parameters change (listen for didChangeScreenParametersNotification), because plugging in a display or changing resolution shifts everything.
#The shape, and the spring that stretches it
A plain rounded rectangle looks close but reads as wrong, because the real cutout isn't one. Its top corners are tighter than its bottom corners (roughly 4 pt versus 8 pt), and Apple uses continuous, squircle-style curvature rather than a plain arc. In SwiftUI the cheapest honest approximation is a continuous corner style:
RoundedRectangle(cornerRadius: 12, style: .continuous)
for the expanded states, with hand-tuned asymmetric corners on the resting pill so its top hugs the hardware. The .continuous style is doing real work; the default .circular corners are the giveaway that separates a convincing island from a floating badge.
Motion is the other half. The island's whole appeal is that it stretches: a compact pill snaps open into a wide card when a call arrives, then settles back. Drive that with SwiftUI springs, not linear curves, because the overshoot-and-settle is what sells it as physical:
withAnimation(.spring(response: 0.35, dampingFraction: 0.72)) {
state = .expanded
}Animate width, height, and corner radius together and let the spring carry them. Expect to spend more time tuning response and dampingFraction than on any other pair of numbers: a hair too bouncy and it feels like a toy, a hair too stiff and it feels dead.
#Click-through, staying light, and permissions
Here's what the quick tutorials skip. Four things will eat your week:
- Per-pixel hit-testing. ignoresMouseEvents is all-or-nothing, and the moment a button is on screen you need the window live and its empty regions still dead. Override hitTest on the content view so only actual controls return a view; every transparent pixel returns nil and the click falls through to whatever's behind. Miss this and a 220-point-wide invisible rectangle eats the menu bar.
- Notch geometry per model. The 14-inch and 16-inch cutouts differ, the Air's differs again, and scaled resolutions move all of them. Anything you hardcode will be a point off on someone's machine, and at this size a point is visible. Derive it from NSScreen, every launch.
- Staying light. This runs all day. It has to idle at effectively zero CPU when nothing's happening and wake only for real events. Poll in a tight loop and you'll show up in everyone's battery complaints; prefer notifications and observers over timers.
- Permissions. The island itself needs none, but the features people want do. Now-playing info comes from MediaRemote-style plumbing Apple keeps semi-private; call controls that press another app's mute button need Accessibility; anything touching the mic triggers its own prompt. Each capability is a separate permission and a separate review headache.
#Or skip the build and use NotchBay
Building one is a genuinely fun AppKit-and-SwiftUI project, and if that's why you're here, go do it. But if you just want the feature on your Mac today, the honest answer is that a polished island is weeks of tuning, not an afternoon, and several good apps already exist:
- boring.notch is free and open source, so it doubles as a working reference: media controls, a file shelf, a HUD replacement.
- NotchNook and Alcove are the polished commercial options, refined widgets and files on one, animation and notifications on the other. Both are paid; check their sites for current pricing, which moves around.
- DynamicLake Pro covers a wide spread, from media and files to Bluetooth and live activities.
- NotchBay, this site's own app, is the work-focused pick, led by meeting controls (one-tap Join, Zoom and Meet from the notch) and a clipboard tray that holds the last 60 clips and OCRs screenshots. It's $9 one-time with a 14-day money-back guarantee, and it needs macOS 26 Tahoe and a notched MacBook. It is newer than NotchNook and Alcove, its call controls need Accessibility permission, and its Meet controls drive Chromium browsers, not Safari.
Before deciding to build or buy, there is a plain walkthrough of how to get one installed in five minutes. That will tell you fast whether building it is worth your weekend.
#Frequently asked questions
Do these properties work on a plain NSWindow, or is NSPanel required?
level, ignoresMouseEvents and collectionBehavior are all NSWindow properties, so a plain NSWindow takes them and behaves the same. NSPanel is chosen for one thing the base class cannot do: the .nonactivatingPanel style mask, which stops a click on the island from activating the app and pulling key focus off whatever the user was typing into.
Does ignoresMouseEvents also block hover?
Yes. A window with ignoresMouseEvents set to true receives no mouse events at all, so mouse-moved events and tracking areas inside it are dead too. Hover-to-expand therefore needs either the property switched to false while the pointer is in range, or a global event monitor outside the window that watches the pointer and flips it.
Which collectionBehavior flag covers full-screen apps?
.canJoinAllSpaces covers ordinary desktops but not a full-screen Space, which macOS treats as its own Space owned by one app. Adding .fullScreenAuxiliary is what lets the panel appear alongside a full-screen window. Decide first whether it should: an island over a full-screen video is a design choice, not a default.
- Apple Developer Documentation, NSWindow.level
- Apple Developer Documentation, NSWindow.ignoresMouseEvents
- Apple Developer Documentation, NSWindow.collectionBehavior
- Apple Developer Documentation, NSScreen.safeAreaInsets
- Apple Developer Documentation, NSPanel
- Apple Support, the macOS menu bar
- Apple, macOS