If you run multiple Chrome profiles or keep several windows open per app, switching between them on macOS becomes irritating fast. Clicking the Dock icon only brings the app forward. Clicking it again does nothing useful. So you right click, scan the window list, and manually choose the one you want. It breaks flow and adds cognitive drag to something that should be instant.
macOS does not natively cycle through an app’s windows when you click its Dock icon. Keyboard users can press `Command + “ to rotate windows, but mouse first users are left with friction. When you are juggling multiple Chrome accounts, terminals, dashboards, and documents, that friction compounds.
After experimenting with double click detection and Dock zone hacks, the most stable and deterministic solution is simple: hold Shift and click the Dock icon to cycle that app’s windows. No timing tricks. No fragile heuristics. Just an explicit modifier key and a reliable window switch.
What This Does
Normal click activates the app using default macOS behavior. Shift + click activates the app and immediately cycles to the next window. It works with Chrome, Safari, Finder, Terminal, and any application that has multiple windows open. It is compatible with newer macOS versions where Dock behavior and accessibility trees have changed.
One Command Install
Paste this entire block into Terminal. It installs Hammerspoon if needed, writes the configuration, and restarts it.
brew install --cask hammerspoon
mkdir -p ~/.hammerspoon
cat << 'EOF' > ~/.hammerspoon/init.lua
-- Shift + Click a Dock icon to cycle that app's windows
-- Requires: Accessibility + Input Monitoring enabled for Hammerspoon
local function axAttr(el, name)
local ok, v = pcall(function() return el:attributeValue(name) end)
if ok then return v end
return nil
end
local function axParent(el)
return axAttr(el, "AXParent")
end
local function axRole(el)
return axAttr(el, "AXRole")
end
local function axSubrole(el)
return axAttr(el, "AXSubrole")
end
local function axTitle(el)
return axAttr(el, "AXTitle")
end
local function findDockAppNameAtPoint(x, y)
local sys = hs.axuielement.systemWideElement()
local el = sys:elementAtPosition(x, y)
if not el then return nil end
local cur = el
for _ = 1, 30 do
local sr = axSubrole(cur)
local r = axRole(cur)
if sr == "AXApplicationDockItem" or r == "AXDockItem" then
local t = axTitle(cur)
if t and t ~= "" then return t end
end
cur = axParent(cur)
if not cur then break end
end
return nil
end
local function cycleAppWindows(app)
if not app then return end
local windows = {}
for _, w in ipairs(app:allWindows()) do
if w:isStandard() then table.insert(windows, w) end
end
if #windows < 2 then
local w = app:focusedWindow() or app:mainWindow()
if w then w:focus() end
return
end
local focused = app:focusedWindow()
local nextIndex = 1
if focused then
for i, win in ipairs(windows) do
if win:id() == focused:id() then
nextIndex = i + 1
break
end
end
end
if nextIndex > #windows then nextIndex = 1 end
windows[nextIndex]:focus()
end
_G.DockShiftCycleTap = hs.eventtap.new({ hs.eventtap.event.types.leftMouseDown }, function(evt)
if not evt:getFlags().shift then return false end
local pos = hs.mouse.absolutePosition()
local name = findDockAppNameAtPoint(pos.x, pos.y)
if not name then return false end
hs.timer.doAfter(0.18, function()
local app = hs.application.find(name) or hs.application.frontmostApplication()
cycleAppWindows(app)
end)
return false
end)
_G.DockShiftCycleTap:start()
hs.alert.show("Shift+Click Dock window cycling enabled", 0.8)
EOF
killall Hammerspoon 2>/dev/null
open -a Hammerspoon After running the script, open System Settings, go to Privacy & Security, and enable Hammerspoon under both Accessibility and Input Monitoring. If Input Monitoring is not enabled, mouse click detection will not work.
Why This Is Better
No right clicking. No scanning window lists. No fragile double click timing logic. Just hold Shift and click. The Dock finally behaves like a power tool instead of a static launcher.