Skip to content

fix(tabs): make the selected editor tab distinguishable in light and dark - #2434

Merged
datlechin merged 2 commits into
mainfrom
fix/tab-strip-selection-contrast
Aug 26, 2026
Merged

fix(tabs): make the selected editor tab distinguishable in light and dark#2434
datlechin merged 2 commits into
mainfrom
fix/tab-strip-selection-contrast

Conversation

@datlechin

Copy link
Copy Markdown
Member

Fixes #2428.

The bug

The track and the selected tab were drawn with the same material: glassEffect(.regular, in: Capsule), both of them, inside one GlassEffectContainer.

Liquid Glass samples the backdrop beneath the whole stack, never the glass it sits on. Two identical .regular surfaces therefore carry no step of their own, and whatever step there is comes from the window's own content rather than from the design. Measured on the shipping build, sample database, three tabs:

track selected contrast
light 250 248 1.017:1
dark 86 106 1.357:1

In light the selected tab was two levels darker than the track, which is no selection at all. And it is not a fixed weakness: across a backdrop sweep from black to white the step turns over, running +57 to -1 in light and +29 to -20 in dark, so the same tab strip can show the selection, hide it, or invert it depending on what the window is over.

The system does not stack one material on itself. A runtime probe of AppKit's own NSTabBar on macOS 27 shows the track is an NSSubduedGlassEffectView carrying plain NSGlassEffectView tabs, two different materials, measured at 236/253 in light and 83/89 in dark. NSGlassEffectViewStyle publishes only regular and clear, so that subdued style is out of reach.

The fix

Opposing neutral tints. The track is tinted toward black and the selected tab toward white. Both sample the same backdrop and move away from it in opposite directions, so the step stops depending on what is behind the window:

across backdrop 0 to 255 worst contrast, dark worst contrast, light sign
before 1.022 1.014 flips
after 1.189 1.183 holds
system NSTabBar 1.098 1.161 n/a

In the running app that lands at 78/141 in dark and 227/247 in light, against the system's 83/89 and 236/253.

Both tints are neutral on purpose: Glass.regular discards hue and reads only lightness, measured, so a red track against a green selection behaves exactly like no tint at all and flips just the same. That also matches the HIG, which asks for colour on the background rather than on the text.

A hairline rim on the selected capsule. macOS attenuates a glass tint in a window that is not key, measured at 134 falling to 94 for the selected tab, and the system's own bar gives up there too, its selected tab reading seven levels darker than its track in a background window. The rim is ink rather than material, so it survives that. It is also a shape rather than a colour, which is the second channel the differentiate-without-colour criteria ask for.

Checked on a running build with the window sent to the back: the rim draws, and the selected tab measures 81/105, a contrast of 1.40:1 with the sign the right way round. The system's own tab bar inverts in that state.

Increase Contrast and Reduce Transparency. The strip answered neither. Rendering it under accessibilityHighContrastAqua and accessibilityHighContrastDarkAqua produced pixels identical to plain aqua and darkAqua, measured. Either setting now takes the strip off glass and onto the opaque surfaces, which carry 1.371:1 in light and 1.991:1 in dark, the way MaterialAccessibility already answers both settings in six other views.

The macOS 14/15 path, which was worse. selectedTabSurface filled the selected capsule with .unemphasizedSelectedContentBackgroundColor whenever the window was not in front, which is the exact constant the track uses. Both resolve opaque, so the delta was zero: a background window showed no selected tab at all, in either appearance. The shadow meant to cover for it was drawn in light alone and clipped away by the track's own 24pt capsule. The selected fill is now a tone of its own and does not step down; only the labels do, which they already did.

What was not done

The issue suggests tinting the active tab's title with the app accent or the connection colour. That is not the macOS convention: no anchored Mac app does it, the HIG says to put colour on the background rather than on symbols or text, TablePlus keeps its connection colour in the status bar and never lets it signal selection, DataGrip colours backgrounds and VS Code uses a border. It also cannot work here structurally, because the strip lists one connection's tabs, so every tab would carry the same colour and none of them would stand out.

Tests

EditorTabStripChromeTests now pins the surface style to .solid and renders that path. This is a real change in what it covers, and worth stating plainly: a tinted glassEffect cannot be rasterised at all. cacheDisplay returns an empty bitmap for the whole hosting view, the strip's own titles and close button included, and every measurement reads exactly zero. Untinted glass lets its content rasterise, which is why the suite worked before.

So the suite now covers the surfaces that ship to macOS 14 and 15 and to anyone with either accessibility setting on, which had no coverage at all and is where the background-window bug shipped. It keeps the three existing z-order assertions and gains one that samples the selected capsule against a plain one in both appearances and both window states, which fails on the old fallback. The glass path is verified against the running app, measured and screenshotted.

One consequence to state plainly: the GlassEffectContainer layering that this file broke twice before now has no automated guard at all, because the path it lives on cannot be rasterised. The two assertions that covered it still run, but against the solid surfaces. It is checked by hand against the running app, in both appearances and with the window both in front and behind.

EditorTabStripSurfacesTests is new and pins the arithmetic: the two tints pull in opposite directions, both are neutral, the opaque selected fill is never the track's own colour in any of the four appearances, and either accessibility setting alone takes the strip off glass.

Verification

  • verify.sh build PASS
  • verify.sh test EditorTabStripChromeTests EditorTabStripSurfacesTests EditorTabStripLayoutTests EditorTabStripAccessoryControllerTests PASS, 34 of 34
  • verify.sh lint 0 violations on all four files
  • Baselined against main in a worktree before and after, to confirm the suite rework was a real change and not an environment failure
  • Reviewed by the repo's own reviewer. Codex was out of credits, so it did not read this diff.

Before / After

Screenshots are attached below: the tab strip with three tabs, Query 2 selected, in both appearances. In the light "before" the selected tab is visible only by its close button.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

Signed-off-by: Ngô Quốc Đạt <datlechin@gmail.com>
@datlechin
datlechin merged commit e3db132 into main Aug 26, 2026
4 checks passed
@datlechin
datlechin deleted the fix/tab-strip-selection-contrast branch August 26, 2026 01:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Editor tabs: improve active and inactive state contrast in light and dark modes

1 participant