The short answer is: not cleanly with stock NSPopover — but there are real workarounds. Here's the full picture:
The Core Problem
NSPopover re-anchors its position any time contentSize changes, even height-only. This is a hardcoded AppKit behavior with no public API override. You can't tell it "resize but keep the X position."
What Actually Works
Option 1 — Dynamic height only, fixed width (recommended)
This is what your codebase solved with idealWidth. Width is pinned, so the anchor never drifts. Height is fully dynamic.
// AppDelegate
hc.sizingOptions = .preferredContentSize
// No manual contentSize anywhere
// Root Group in PopoverView
Group { ... }
.frame(idealWidth: 340) // width pinned via ideal size
// Content height varies freely — no jump
This gives you fully dynamic height and is stable. The constraint is that width must be fixed. For most status bar apps this is fine — popover width never needs to change.
Option 2 — Custom NSPanel instead of NSPopover
Skip NSPopover entirely and use a borderless NSPanel positioned manually under the status item. You draw your own arrow/bezel and position the window yourself. Width and height are then fully free.
let panel = NSPanel(
contentRect: .zero,
styleMask: [.borderless, .nonactivatingPanel],
backing: .buffered,
defer: false
)
panel.isFloatingPanel = true
panel.level = .statusBar
panel.backgroundColor = .clear
panel.isOpaque = false
// Position it manually under the status item button
let buttonRect = statusItem.button!.window!.convertToScreen(statusItem.button!.frame)
panel.setFrameTopLeftPoint(NSPoint(x: buttonRect.minX, y: buttonRect.minY))
You resize the panel at will with setFrame() — no re-anchoring because you own the position logic. Trade-off: you lose the native popover arrow, dismissal-on-outside-click, and accessibility behaviors. You have to reimplement them.
Option 3 — Only resize when the popover is closed
Change contentSize only in the moments between close and show — never while it's visible.
func toggle() {
if popover.isShown {
popover.performClose(nil)
} else {
// Safe to set size here — popover is not yet visible
let newSize = computeSize()
popover.contentSize = newSize
popover.show(relativeTo: button.bounds, of: button, preferredEdge: .minY)
}
}
This gives you both dynamic width and height, but only updates size on open, not while the user is looking at it. For most use cases (the content is recalculated before showing) this is perfectly fine.
Option 4 — NSAnimationContext with duration: 0 (partial hack)
Some developers report that wrapping contentSize changes in a zero-duration animation group suppresses the re-anchor visually, though it's not guaranteed:
NSAnimationContext.runAnimationGroup { ctx in
ctx.duration = 0
popover.contentSize = newSize
}
This is what your navigate() function already uses. It works for deliberate user-triggered navigation but still re-anchors if called during a live poll — it just happens fast enough to be invisible on user taps.
Verdict
Approach | Dynamic Width | Dynamic Height | Jump-free | Complexity
-- | -- | -- | -- | --
idealWidth + preferredContentSize | ❌ (fixed) | ✅ | ✅ | Low
Resize only on open (before show()) | ✅ | ✅ | ✅ | Low
Custom NSPanel | ✅ | ✅ | ✅ | High
Resize while visible | ✅ | ✅ | ❌ always jumps | N/A
For runner-bar specifically: the idealWidth approach already gives you everything you need since the width doesn't need to change. If you ever need dynamic width too, resize before show() is the cleanest zero-overhead solution.
The short answer is: not cleanly with stock
NSPopover— but there are real workarounds. Here's the full picture:The Core Problem
NSPopoverre-anchors its position any timecontentSizechanges, even height-only. This is a hardcoded AppKit behavior with no public API override. You can't tell it "resize but keep the X position."What Actually Works
Option 1 — Dynamic height only, fixed width (recommended)
This is what your codebase solved with
idealWidth. Width is pinned, so the anchor never drifts. Height is fully dynamic.This gives you fully dynamic height and is stable. The constraint is that width must be fixed. For most status bar apps this is fine — popover width never needs to change.
Option 2 — Custom
NSPanelinstead ofNSPopoverSkip
NSPopoverentirely and use a borderlessNSPanelpositioned manually under the status item. You draw your own arrow/bezel and position the window yourself. Width and height are then fully free.You resize the panel at will with
setFrame()— no re-anchoring because you own the position logic. Trade-off: you lose the native popover arrow, dismissal-on-outside-click, and accessibility behaviors. You have to reimplement them.Option 3 — Only resize when the popover is closed
Change
contentSizeonly in the moments between close and show — never while it's visible.This gives you both dynamic width and height, but only updates size on open, not while the user is looking at it. For most use cases (the content is recalculated before showing) this is perfectly fine.
Option 4 —
NSAnimationContextwithduration: 0(partial hack)Some developers report that wrapping
contentSizechanges in a zero-duration animation group suppresses the re-anchor visually, though it's not guaranteed:This is what your
navigate()function already uses. It works for deliberate user-triggered navigation but still re-anchors if called during a live poll — it just happens fast enough to be invisible on user taps.Verdict
For runner-bar specifically: the
idealWidthapproach already gives you everything you need since the width doesn't need to change. If you ever need dynamic width too, resize beforeshow()is the cleanest zero-overhead solution.