Managing Menu Bar Icon States in macOS
I often find myself staring at a static menu bar icon, wondering if the app is actually doing anything or if it just crashed in the background. Providing visual feedback is a great way to show users that your app is listening or processing a task without them having to open a window.
In this guide, I will show you how to manage different icon states for a macOS app, from simple idle wiggles to voice-triggered “big ears.”
What You’ll Need
Section titled “What You’ll Need”- Access to the macOS app source code (
apps/macos). - The
AppStateandAppStateStoremodules. CritterIconRendererfor icon generation.
Quick Start
Section titled “Quick Start”You can manage the menu bar icon through four primary states: Idle, Paused, Voice Trigger, and Working.
1. Handling Voice Triggers
Section titled “1. Handling Voice Triggers”When your app hears a wake word, you want the icon to react immediately. I recommend using the earBoostActive state to scale the icon ears to 1.9x and add circular ear holes for better visibility.
To trigger this, call the voice wake detector:
// When the wake word is heardAppState.triggerVoiceEars(ttl: nil)
// After 1s of silence to match the capture windowstopVoiceEars()2. Tracking Agent Activity
Section titled “2. Tracking Agent Activity”If your app is running a background task or an agent is active, you can trigger a “scurry” animation. This adds a faster leg wiggle and a small horizontal jiggle to the icon.
I suggest wrapping your work spans in defer blocks to ensure the animation resets even if the task fails:
AppStateStore.shared.setWorking(true)defer { AppStateStore.shared.setWorking(false)}// Perform your long-running task here3. Understanding the Renderer
Section titled “3. Understanding the Renderer”The base icon is drawn via CritterIconRenderer.makeIcon. You don’t need to change the frame size; it stays at 18×18 pt (rendered into a 36×36 px Retina store). The renderer handles these parameters:
blink: Normal eye animation.legWiggle: Increased during “working” states.earScale: Set to 1.9 during voice triggers.earHoles: Toggled to true during voice triggers.
4. Setting the Paused State
Section titled “4. Setting the Paused State”When the app is paused, the icon should stop all motion. You can achieve this by setting the status item to appearsDisabled.
Troubleshooting
Section titled “Troubleshooting”The icon stays in the “Working” state forever
This usually happens if a job hangs or the reset logic is skipped. Always use short TTLs (Time To Live) of less than 10 seconds. Ensure you call setWorking(false) inside a defer block so it runs regardless of how the function exits.
Voice ears are not appearing
The voice trigger only fires from the in-app voice pipeline. There is no external CLI or broker toggle for this state. Check that your runtime is correctly calling AppState.triggerVoiceEars(ttl: nil) when the wake word is detected.
If you have more questions about specific implementation details, check out the AI Setup Assistant.
What’s Next
Section titled “What’s Next”OpenClaw Expert
Still stuck?
If this page didn't answer your case, ask OpenClaw Expert for step-by-step guidance.