diff --git a/Coder Desktop/Coder Desktop/Coder_DesktopApp.swift b/Coder Desktop/Coder Desktop/Coder_DesktopApp.swift
index ae50519c..13f7086a 100644
--- a/Coder Desktop/Coder Desktop/Coder_DesktopApp.swift	
+++ b/Coder Desktop/Coder Desktop/Coder_DesktopApp.swift	
@@ -1,4 +1,5 @@
 import FluidMenuBarExtra
+import NetworkExtension
 import SwiftUI
 
 @main
@@ -26,7 +27,7 @@ struct DesktopApp: App {
 
 @MainActor
 class AppDelegate: NSObject, NSApplicationDelegate {
-    private var menuBarExtra: FluidMenuBarExtra?
+    private var menuBar: MenuBarController?
     let vpn: CoderVPNService
     let state: AppState
 
@@ -36,11 +37,18 @@ class AppDelegate: NSObject, NSApplicationDelegate {
     }
 
     func applicationDidFinishLaunching(_: Notification) {
-        menuBarExtra = FluidMenuBarExtra(title: "Coder Desktop", image: "MenuBarIcon") {
+        menuBar = .init(menuBarExtra: FluidMenuBarExtra(title: "Coder Desktop", image: "MenuBarIcon") {
             VPNMenu<CoderVPNService>().frame(width: 256)
                 .environmentObject(self.vpn)
                 .environmentObject(self.state)
-        }
+        })
+        // Subscribe to system VPN updates
+        NotificationCenter.default.addObserver(
+            self,
+            selector: #selector(vpnDidUpdate(_:)),
+            name: .NEVPNStatusDidChange,
+            object: nil
+        )
     }
 
     // This function MUST eventually call `NSApp.reply(toApplicationShouldTerminate: true)`
@@ -59,6 +67,16 @@ class AppDelegate: NSObject, NSApplicationDelegate {
     }
 }
 
+extension AppDelegate {
+    @objc private func vpnDidUpdate(_ notification: Notification) {
+        guard let connection = notification.object as? NETunnelProviderSession else {
+            return
+        }
+        vpn.vpnDidUpdate(connection)
+        menuBar?.vpnDidUpdate(connection)
+    }
+}
+
 @MainActor
 func appActivate() {
     NSApp.activate()
diff --git a/Coder Desktop/Coder Desktop/MenuBarIconController.swift b/Coder Desktop/Coder Desktop/MenuBarIconController.swift
new file mode 100644
index 00000000..867e1837
--- /dev/null
+++ b/Coder Desktop/Coder Desktop/MenuBarIconController.swift	
@@ -0,0 +1,57 @@
+import FluidMenuBarExtra
+import NetworkExtension
+import SwiftUI
+
+@MainActor
+class MenuBarController {
+    let menuBarExtra: FluidMenuBarExtra
+    private let onImage = NSImage(named: "MenuBarIcon")!
+    private let offOpacity = CGFloat(0.3)
+    private let onOpacity = CGFloat(1.0)
+
+    private var animationTask: Task<Void, Never>?
+
+    init(menuBarExtra: FluidMenuBarExtra) {
+        self.menuBarExtra = menuBarExtra
+    }
+
+    func vpnDidUpdate(_ connection: NETunnelProviderSession) {
+        switch connection.status {
+        case .connected:
+            stopAnimation()
+            menuBarExtra.setOpacity(onOpacity)
+        case .connecting, .reasserting, .disconnecting:
+            startAnimation()
+        case .invalid, .disconnected:
+            stopAnimation()
+            menuBarExtra.setOpacity(offOpacity)
+        @unknown default:
+            stopAnimation()
+            menuBarExtra.setOpacity(offOpacity)
+        }
+    }
+
+    func startAnimation() {
+        if animationTask != nil { return }
+        animationTask = Task {
+            defer { animationTask = nil }
+            let totalFrames = 60
+            let cycleDurationMs: UInt64 = 2000
+            let frameDurationMs = cycleDurationMs / UInt64(totalFrames - 1)
+            repeat {
+                for frame in 0 ..< totalFrames {
+                    if Task.isCancelled { break }
+                    let progress = Double(frame) / Double(totalFrames - 1)
+                    let alpha = 0.3 + 0.7 * (0.5 - 0.5 * cos(2 * Double.pi * progress))
+                    menuBarExtra.setOpacity(CGFloat(alpha))
+                    try? await Task.sleep(for: .milliseconds(frameDurationMs))
+                }
+            } while !Task.isCancelled
+        }
+    }
+
+    func stopAnimation() {
+        animationTask?.cancel()
+        animationTask = nil
+    }
+}
diff --git a/Coder Desktop/Coder Desktop/VPNService.swift b/Coder Desktop/Coder Desktop/VPNService.swift
index 793b0eb0..1e29ae75 100644
--- a/Coder Desktop/Coder Desktop/VPNService.swift	
+++ b/Coder Desktop/Coder Desktop/VPNService.swift	
@@ -70,12 +70,6 @@ final class CoderVPNService: NSObject, VPNService {
         Task {
             await loadNetworkExtensionConfig()
         }
-        NotificationCenter.default.addObserver(
-            self,
-            selector: #selector(vpnDidUpdate(_:)),
-            name: .NEVPNStatusDidChange,
-            object: nil
-        )
     }
 
     deinit {
@@ -159,13 +153,7 @@ final class CoderVPNService: NSObject, VPNService {
 }
 
 extension CoderVPNService {
-    // The number of NETunnelProviderSession states makes the excessive branching
-    // necessary.
-    // swiftlint:disable:next cyclomatic_complexity
-    @objc private func vpnDidUpdate(_ notification: Notification) {
-        guard let connection = notification.object as? NETunnelProviderSession else {
-            return
-        }
+    public func vpnDidUpdate(_ connection: NETunnelProviderSession) {
         switch (tunnelState, connection.status) {
         // Any -> Disconnected: Update UI w/ error if present
         case (_, .disconnected):
diff --git a/Coder Desktop/project.yml b/Coder Desktop/project.yml
index 8b9b18fe..2872515b 100644
--- a/Coder Desktop/project.yml	
+++ b/Coder Desktop/project.yml	
@@ -89,8 +89,10 @@ packages:
     url: https://github.com/SimplyDanny/SwiftLintPlugins
     from: 0.57.1
   FluidMenuBarExtra:
-    url: https://github.com/lfroms/fluid-menu-bar-extra
-    from: 1.1.0
+    # Forked so we can dynamically update the menu bar icon.
+    # The upstream repo has a purposefully limited API
+    url: https://github.com/coder/fluid-menu-bar-extra
+    revision: 020be37
   KeychainAccess:
     url: https://github.com/kishikawakatsumi/KeychainAccess
     branch: e0c7eebc5a4465a3c4680764f26b7a61f567cdaf