DEV Community

Sergio Farfan Cardenete
Sergio Farfan Cardenete

Posted on • Edited on

I Built a Windows-Style Alt+Tab Window Switcher for macOS in Pure Swift

What It Does

Hold Option and tap Tab — a panel appears showing every open window across all your apps, on the screen where your mouse is. Cycle through them with Tab / Shift-Tab or the arrow keys, then release Option to jump straight to the selected window. Click any thumbnail to switch instantly.

Shortcut Action
Option-Tab Open switcher / next window
Tab Cycle forward
Shift-Tab Cycle backward
← / → Navigate left / right
Escape Cancel
Enter Confirm and switch
Click Select and switch immediately

The switcher includes minimized windows (they unminimize when selected), windows of Cmd-H-hidden apps, and windows sitting on other Spaces. No Dock icon, no clutter — just a clean menu bar icon with a small preferences menu.

AltTab menu bar menu


What's New in 1.3

  • Liquid Glass. On macOS 26 (Tahoe), the switcher can render on Apple's native glass material via NSGlassEffectView — the real thing, not a blur imitation. Status menu → Background → Liquid Glass.
  • Selectable backgrounds. Three styles, applied on the next Option-Tab with no relaunch: Solid (default — opaque, theme-adaptive), Transparent (the classic translucent HUD with vibrancy), and Liquid Glass.
  • Appearance override. The panel follows the OS Light/Dark theme by default (including scheduled Auto switching), and you can force Light or Dark from the menu regardless of the system setting.
  • WCAG-tested readability. The text under each window is no longer hardcoded white on a translucent panel. Labels use semantic system colors, and on the default Solid background the label/background pair meets WCAG 2.x AA (contrast ≥ 4.5:1) — not as an aspiration, but verified by unit tests that resolve the live system colors under both Light and Dark appearances and compute the actual contrast ratio. If Apple ever changes the palette in a way that breaks AA, the test suite fails.

What's New in 1.2

Instant-open switcher (cached window list, reconciled off the main thread), a 0.25s messaging timeout on every Accessibility call so one wedged app can't freeze the hotkey, opt-in live window previews via ScreenCaptureKit (macOS 14+, no Screen Recording prompt unless you enable it), click-to-switch fixed, multi-monitor support, and a unit-tested core.


Installation

Requirements: macOS 13 Ventura or later (Liquid Glass needs macOS 26)

Download (recommended)

Grab the prebuilt universal DMG (Apple Silicon + Intel) from the releases page, drag AltTab to Applications, and launch it. The build is not notarized yet, so on first launch: right-click AltTab.app → Open, or

xattr -dr com.apple.quarantine /Applications/AltTab.app
Enter fullscreen mode Exit fullscreen mode

Build from source

git clone https://github.com/sergio-farfan/alttab-macos.git && cd alttab-macos && ./build.sh install && open ~/Applications/AltTab.app
Enter fullscreen mode Exit fullscreen mode
./build.sh build          # Build Release binary only
./build.sh install        # Build and install to ~/Applications
./build.sh run            # Build and launch immediately
./build.sh uninstall      # Remove the installed app
swift test                # Run the unit tests (32)
Enter fullscreen mode Exit fullscreen mode

Note: building the Liquid Glass code requires Xcode 26+ (macOS 26 SDK); the app still runs on macOS 13+.

Permissions

  • Accessibility (required) — intercepts the Option-Tab hotkey, reads window titles, raises windows
  • Screen Recording (never prompted by default) — only requested if you explicitly enable "Show Window Previews" in the menu

How It Works

Global hotkey interception — A CGEvent tap installed at the session level intercepts Option-Tab system-wide before it reaches any app, without stealing keyboard focus. The tap plumbing only decodes events; the session logic lives in a pure SwitcherStateMachine struct, unit-testable without synthesizing CGEvents.

Complete window discovery — On-screen windows come from CGWindowList. Everything else (titles, minimized windows, hidden apps, other Spaces) comes from a single AXUIElement pass per application, with a messaging timeout on every element. Fun fact: AXUIElementSetMessagingTimeout is per-element — setting it on the app element does nothing for the window elements it returns.

Selectable backgrounds — The panel builds one of three root views per style: an opaque NSBox (Solid), a .hudWindow NSVisualEffectView (Transparent — labels become effect-view descendants, so macOS renders them with vibrancy), or an NSGlassEffectView (Liquid Glass) with the content embedded through its contentView property, which is the only placement the SDK header guarantees. The persistent scroll view is re-parented between roots only when the preference actually changes.

MRU ordering — Windows are sorted most-recently-used first via NSWorkspace notifications plus per-app AXObserver callbacks that catch intra-app switches (like Cmd-`). Explicit activations are pinned so the asynchronous window-raise can't demote the window you just picked.

Non-activating overlay — The switcher is an NSPanel with .nonactivatingPanel, so it appears without stealing focus. Releasing Option activates the target window, not AltTab.

AltTab About dialog, version 1.3.0


Two More macOS Gotchas

The 1.3 work surfaced two API behaviors worth knowing about.

withAlphaComponent() freezes dynamic colors. macOS semantic colors like labelColor are dynamic — they resolve differently per appearance, at draw time. But call withAlphaComponent(0.78) on one and you get back a static color, resolved against whatever appearance happens to be current at that call site. My unit test caught this before it shipped: the derived color measured 1.21:1 contrast in Dark mode — because it had frozen the Light variant (black text) and composited it onto a dark background. The fix is NSColor(name:dynamicProvider:), which re-derives the color inside each appearance resolution. If you take one thing from this post: contrast assertions in unit tests that resolve live system colors are cheap, and they catch exactly this class of bug.

#available gates runtime, not compile time. The Liquid Glass code is properly wrapped in #available(macOS 26.0, *) — and the release build still failed in CI with cannot find 'NSGlassEffectView' in scope. Locally everything compiled fine. The difference: my Mac has the macOS 26 SDK; the CI runner had Xcode 16 with the macOS 15 SDK, where the symbol doesn't exist at all. Availability checks assume the symbol is in the SDK you compile against — they only guard which OS executes it. Bumping the CI image to one with Xcode 26 fixed it. If you adopt Tahoe APIs behind #available, check your CI's SDK before you tag the release.


When the Accessibility Toggle Lies

(From the 1.2.1 investigation, still the best bug of this project.) After updating, Option-Tab went dead while System Settings showed the Accessibility toggle confidently ON. macOS pins every TCC permission grant to a code signing requirement — for unsigned apps, essentially the cdhash of one exact build. My DMG script built with CODE_SIGNING_ALLOWED=NO, which ships an unsealed bundle, and macOS fabricates unpredictable code identities for those — so the recorded grant never matched the running binary. The toggle renders "an allowed entry exists"; enforcement asks "does this binary match the requirement". Different questions.

Diagnosing it meant reading the csreq blob out of TCC.db with sqlite3, decompiling it with csreq -r -t, comparing cdhashes with codesign -dvvv, and confirming with CGGetEventTapList that no event tap existed. The fix: always apply an ad-hoc seal (codesign --force --deep -s -) when no signing identity is available. If you ever hit a stuck grant on an unsigned app:

tccutil reset Accessibility <bundle-id>
Enter fullscreen mode Exit fullscreen mode

then relaunch and grant again.


Project Stats

  • ~2,100 lines of Swift across 13 source files, plus a unit-tested core (32 tests, swift test)
  • Zero external dependencies — pure Swift + AppKit + ScreenCaptureKit
  • MIT licensed — fork it, modify it, ship it
  • macOS 13+ (Ventura through Tahoe; Liquid Glass on 26+)

Try It Out

The code is on GitHub: github.com/sergio-farfan/alttab-macos

If you have been frustrated by macOS window switching, give it a try. And if you are curious about the low-level macOS APIs — CGEvent taps, AXUIElement, ScreenCaptureKit, NSGlassEffectView, or how TCC really decides whether your app is trusted — the codebase is small enough to read in an afternoon.

Feedback, issues, and PRs are welcome!

Top comments (0)