Skip to content

Give the switcher back its spacing by measuring the pill - #2204

Merged
karlitschek merged 1 commit into
masterfrom
fix/switcher-spacing
Sep 15, 2026
Merged

karlitschek merged 1 commit into
masterfrom
fix/switcher-spacing

Conversation

@karlitschek

Copy link
Copy Markdown
Member

The Activities row reads as one solid block of words.

The cause is not the padding. Every option was made as wide as the widest of them, so that a pill of exactly one seventh of the track would land on one — "Favourites" with its icon is 98 pixels of content, a seventh of a timeline column is about 100, and that leaves roughly one pixel of padding either side. Every option paid the width of the longest one. The notifications page had already halved the switcher's own padding to stop seven options running past the column, which made it tighter still.

So the pill is measured off the option it sits under, and the options are free to be as wide as their own words. "All" and "Polls" stop claiming the room "Favourites" needs, and the slack that frees — nearly 200 pixels across the seven — goes back into padding for all of them. The per-page squeeze goes with it.

Measured on a running instance, before and after:

before after
space around each option's content ~1 px on the widest, uneven across the row 33–34 px on every one
option padding 12 px (halved by the page) 18 px (the switcher's own)
track in an 800 px column 701 px with the content overflowing it 768 px, fits

What the measurement is careful about

It is geometry and nothing else. The distance is taken from the first option rather than from the track, which cancels the track's padding out of both sides of the subtraction: the pill already sits after that padding, and travelling it twice is exactly what put the pill three pixels off its option the first time I tried this. offsetLeft is physical while the pill is anchored to the inline start, so a right-to-left interface measures from the other edge and travels negative.

The old behaviour is the fallback, and is still exercised. Until something has a width — the first paint, and jsdom — the pill is an equal share of the track positioned by index, exactly as before. Four tests cover the two paths and the switch between them.

A ResizeObserver watches the options, not only the track. They change width without the track doing so: a translation arriving, a font loading, the labels giving way to their icons at a breakpoint.

One thing found on the way

Two identical @media (max-width: 500px) blocks both hid the labels on a phone, and the first did it with display: none — which takes the label out of the accessibility tree and so silently defeated the second, written carefully to keep it there. A radiogroup of seven unnamed buttons is not a control anybody can use. The duplicate is gone; verified on a running instance that the label is now visually hidden and still in the tree.

Checked

On a running instance: the pill lands within 0.2 px of the active option on every option clicked, the three-option switcher (My Feed / Local / Global) is unchanged at 343 px, and at a 620 px window the labels give way to icons with the control intact and the pill still aligned.

2145 JS tests and 5021 PHP tests pass; psalm, eslint, stylelint and the type check are clean, and the committed bundle matches a fresh build.

🤖 Generated with Claude Code

The Activities row reads as one solid block of words. The cause is not the
padding — it is that every option was made as wide as the widest of them, so a
pill of one seventh of the track would land exactly on one. "Favourites" with
its icon is 98 pixels of content, a seventh of a timeline column is about 100,
and that leaves roughly a pixel of padding either side. Every option paid the
width of the longest one. The notifications page had already halved the
switcher's padding to stop seven options running past the column, which made it
tighter still.

So the pill is measured off the option it sits under and the options are free to
be as wide as their own words. "All" and "Polls" stop claiming the room
"Favourites" needs, and the slack that frees — nearly 200 pixels across the
seven — goes back into padding for all of them. Measured on a real page
afterwards: every option now has about 34 pixels of it, where the widest had
one, and the row still fits the column. The per-page squeeze is gone with it.

The measurement is geometry and nothing else. The distance is taken from the
*first* option rather than from the track, which cancels the track's padding out
of both sides of the subtraction: the pill already sits after that padding, and
travelling it twice is what put it three pixels off its option the first time I
tried this. `offsetLeft` is physical while the pill is anchored to the inline
start, so a right-to-left interface measures from the other edge and travels
negative.

The fallback is the old behaviour and is still exercised: until something has a
width — the first paint, and jsdom — the pill is an equal share of the track
positioned by index. A ResizeObserver watches the options rather than only the
track, because they change width without it doing so: a translation arriving, a
font loading, the labels giving way to their icons at a breakpoint.

While here, one duplicate breakpoint goes. Two `@media (max-width: 500px)`
blocks both hid the labels on a phone, and the first did it with `display:
none` — which takes the label out of the accessibility tree and so defeated the
second, written carefully to keep it there. A radiogroup of seven unnamed
buttons is not a control anybody can use.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Frank Karlitschek <frank@nextcloud.com>
@karlitschek
karlitschek merged commit df03f2b into master Sep 15, 2026
44 checks passed
@karlitschek
karlitschek deleted the fix/switcher-spacing branch September 15, 2026 08:48
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.

1 participant