Skip to content

Resize the phone tab bar and minimise it while reading - #516

Merged
MaggieAppleton merged 6 commits into
mainfrom
mobile/tab-bar-e
Oct 12, 2026
Merged

MaggieAppleton merged 6 commits into
mainfrom
mobile/tab-bar-e

Conversation

@MaggieAppleton

@MaggieAppleton MaggieAppleton commented Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator

Why

The phone tab bar felt cramped: 11.3px medium labels were too wide for their columns, so the full-column capsule's 26px curve touched "Decisions" (2.9px clearance at 393, 0.6px at 375, an overlap at 320), and the label's weight pushed the icon up. Maggie chose option E from the tab-bar comparison: option B's iOS 26 sizing plus minimise-on-scroll.

What changed

  • iOS 26 proportions. 62px bar, 10px/600 labels in secondary grey (brand ink when current), 24px icons, 3px gap, icon and label stack centred in the pill (6.5px above and below). The pencil is a 54px circle (48px at 320), centred beside the bar. Both shrink to the same 52px circle when minimised.
  • Concentric corners. The bar is a 20px rounded rectangle, and the pill's radius is derived as calc(var(--radius-phone-tab-bar) - var(--phone-tab-bar-inset)) (15px with a 5px inset), so the curves run parallel at the Chat and Build ends. A full capsule would have forced a near-capsule pill that crowded the labels again. Landscape's 48px bar uses a 2px inset and an 18px pill, using the same rule (--radius-phone-tab-pill-compact). There's a CSS comment on the tokens.
  • Measured label clearance (local macOS Chromium) for the widest label, "Document": 9.5px at 393 and 4.2px at 320. For "Decisions": 10.7px at 393 and 5.5px at 320. CI's Linux text renders about 0.8px tighter. That's why the corners are 20px rather than 24px, and why the narrowest phones use -0.01em tracking. With the 48px action at 320, "Document" clears its pill by 4.2px locally, about 3.4px in CI.
  • Minimise while reading. Scrolling down the Document, Decisions or Build pane shrinks the bar to a 52px glass circle showing the current tab's icon in brand ink, and the pencil shrinks to 52px beside it. In portrait the circle sits at the leading edge. In landscape the bar shrinks toward the pencil, so the two stay together. It uses the top bar's scroll signal (usePhoneTopBar now returns a shared show()), and direction now accumulates travel since the last change of direction, so slow drags of a few pixels a frame still tuck and restore both bars; rubber-banding at either end is still ignored. Any scroll up, focus entering the tab row, or a tap on the circle restores the bar. The transitions use --tab-slide-dur / --tab-slide-ease, and Chat never minimises, matching the top bar.
  • Accessibility. The circle is a button named "Show tabs, " (for example "Show tabs, Decisions, 2 open"). While minimised the tabs are inert and aria-hidden; while expanded the circle is inert and aria-hidden. Restoring by tap or keyboard focus moves focus to the current tab.
  • Badges. The full bar keeps the Decisions count and Chat dot. The circle shows the count at its top-right corner when Decisions is current (landscape too), and an inset brand dot when another tab has activity, so neither covers the icon.
  • Reduced motion: the bar never minimises, as the top bar never hides.
  • Unchanged: the keyboard still hides the row, the docked formatting bar still replaces it, and the landscape one-line variant stays.
  • --phone-bottom-chrome still derives from --phone-tab-bar-height, so it tracks the full 62px bar. I didn't touch any pane padding rules.
  • First-paint skeleton. I updated phone-skeleton.css to the same geometry (radius, inset, action size, 320 and landscape variants). The markup needed no change, so the index.html and PhoneSkeleton parity test still holds.
  • Tokens (packages/visuals/theme.css): --phone-tab-bar-inset(-compact), --phone-tab-action-gap, --phone-tab-action-size, --phone-tab-bar-minimised, --phone-tab-label-size, --phone-tab-label-line, --radius-phone-tab-bar, --radius-phone-tab-pill(-compact), --radius-phone-tab-minimised.

Screenshots

Before on top, after below.

393, Document current
393 document before and after

393, Decisions current
393 decisions before and after

375, Decisions current
375 decisions before and after

320, Decisions current
320 decisions before and after

Landscape
landscape before and after

After: end tabs selected, to show the concentric corners (393 Chat, 393 Build, 320 Chat, 320 Build)
End tabs selected

After: minimised while reading (393 on Decisions with its count, 393 on Document with the activity dot, 320 on Decisions, landscape on Decisions and on Document)
Minimised tab bar

Testing

  • bun run types: pass
  • bun test apps/web/src/phone-skeleton.test.tsx scripts/design-contract: 47 pass
  • dprint check and oxlint: clean
  • check-design-record: pass
  • check-design-contract: pass after the renewal below. The local design:check and token failures come from untracked local design-jig files that aren't in this branch.
  • New e2e tests in e2e/phone-workspace.e2e.ts:
    • minimise on scroll down, restore on scroll up, tap and keyboard focus, with focus landing on the current tab
    • slow 3px-per-frame drags tuck and restore both bars
    • reduced motion keeps the full bar
    • at 320: 44px targets, label clearance of at least 3px for both Document and Decisions, and pill radius equal to bar radius minus inset
  • Updated e2e tests:
    • the tab-switch test now taps "Show tabs" after reading down
    • sidecar.e2e.ts (compact destinations) restores the bar before switching tabs
    • responsive-decisions.e2e.ts accepts either the Decisions tab or the minimised circle
    • e2e/phone-first-load.e2e.ts now runs at 393 and 320 and asserts the 62px bar and the 54px or 48px action
  • I didn't run e2e locally (shared ports); this relies on the PR's browser-integration CI.
  • Needs a real device: how the spring feels when width and height animate together, and that a minimised circle doesn't fight iOS Safari's own toolbar collapse.

Design-contract review

Reviewed and renewed in the "Renew design-contract review exceptions for the phone tab bar" commit. Maggie approved the 10px tab label design; the typography exception is exact (one case, .phone-tab, the token's only use).

  • apps/web/src/workspace.tsx, exception file scripts/design-contract/exceptions/dynamic-web.json: its sourceHash (6c383a9a…) no longer matches. The new hash is 332ae71c3fca3a39c97a1a4314c6315b0c6e296cb29d9c9f8d60ba5b8290909c. Only two props were added (minimised={topBar.hidden}, onRestore={topBar.show}); there's no new style or class logic at the dynamic sites.
  • scripts/design-contract/exceptions/preserved-values.json: I added a typography case for font-size: var(--phone-tab-label-size) (10px) on .phone-tab. The fluid scale's smallest step (--text-2xs, about 11.3px) makes four labels too wide for their pills at 320–393, which was the original problem. This mirrors Apple's 10pt tab label and needs an explicit design decision.

🤖 Generated with Claude Code

@MaggieAppleton

Copy link
Copy Markdown
Collaborator Author

Review: Changes needed (two small fixes)

Reviewed head 0bf6d9b7, compared against origin/main built locally. Headless Chromium: iPhone 15 Pro (393), SE (375x667), 320x568, landscape 852x393, desktop 1440x900.

Verified

  • Clearance around "Decisions" (local Chromium): 9.9px at 393, 7.7px at 375, 4.2px at 320, 24.2px in landscape. Main is 2.9 / ~0.6 / −2.7.
  • Concentric corners: bar 22px, inset 5px, pill 17px at both Chat and Build. In landscape: bar 22px, inset 2px, pill 20px. The screenshots of both end tabs look right.
  • Icon and label centring: the stack sits 6.5px from the pill's top and 6.5px from its bottom (main: 4.5 / 6.5). In the circle, the icon is exactly centred (42/623 against the circle's 42/623).
  • Minimise: the circle is 52px and the pencil 52px (48px in landscape). The bar's bottom edge holds still while the width and height animate. The top bar and the tab bar tuck together on the same signal. A 3px wobble at the bottom of the scroll doesn't flicker it, scrolling up restores it, and so does a tap on the circle. Tab focus restores it and lands on the current tab. Chat never minimises, reduced motion never minimises, and desktop doesn't render the row.
  • A11y: the hidden tabs are inert and aria-hidden while minimised. The circle is named "Show tabs, Decisions, 2 unanswered".
  • First-load skeleton: the bar and action boxes match the real ones to the pixel at 393, 375, 320 and landscape (I measured with scripts blocked). phone-skeleton.test.tsx passes.
  • --phone-bottom-chrome: 84px in portrait (was 82) and 66px in landscape, the same in both states. It over-reserves 10px while minimised, which is safe for Clear the phone bars from every pane scroller with one token #517's pane clearance and means minimising doesn't reflow anything.
  • CI: the validation failure is only the listed workspace.tsx review-hash exception. e2e was still running when I checked.

Must fix

  1. The circle's icon is grey, not brand ink. phone-workspace.css:463 resets [data-nucleo-icon] { color: inherit } only for .phone-tab and .phone-tab-action. The circle sets color: var(--color-brand-ink), but its SVG computes to the global icon grey (oklch(0.565 0.012 95)). Jig E shows the active icon in brand ink. Fix: add .phone-tab-mini [data-nucleo-icon] to that selector.
  2. In landscape, the minimised circle loses the Decisions count. The max-height: 500px block hides .phone-tab-badge (it swaps to the inline badge beside the label), and the circle has no inline badge. With Decisions current, the circle shows no count and no dot. Fix: inside that media block, add .phone-tab-mini .phone-tab-badge { display: block; }.

Nice to have

  1. "Document" at 320 is the tightest label. With normal tracking it's 49.8px wide against 47.4px for "Decisions", so its clearance is 3.0px locally, and probably about 2.2px on CI's Linux text. Document is the default tab. The new e2e only measures the current tab, which is Decisions. Measure every label (or the widest), and consider −0.01em at 320.
  2. The count crowds the icon in the circle. The badge sits at left: calc(100% - 8px) of a 24px icon, so in a 52px circle it covers the right half of the Decisions glyph. Consider pinning it to the circle's top-right edge instead.
  3. In landscape, the circle and the pencil end up about 384px apart in mid-screen, at either end of the centred 480px row. It reads as two stray buttons. Consider keeping them adjacent, for example with justify-content: center while minimised in landscape.
  4. Design call for Maggie: the bar is no longer a capsule. The PR's 22px rounded rectangle departs from the jig and from iOS 26's capsule, and next to a circular pencil it introduces a second shape language. The reasoning holds: a concentric capsule (31px bar, 26px pill) brings back the crowding. Worth an explicit OK.
  5. Shared scroll signal (existing behaviour, but it now drives both pieces of chrome). The 6px slop is compared per scroll event. A slow drag of 6px or less per event, which is common at 120Hz, never minimises: I saw 0 toggles over 120 frames of 3px/frame drift with tremor. Consider accumulating distance since the last direction change.
  6. The pencil's bottom drops 4px as it shrinks (54px centred, then 52px bottom-aligned), while the bar's bottom stays put. It's barely visible. Keep it or bottom-align both.

Shots: .design-orchestrator/mobile-2026-10-11/shots/review-516/ (local).

@MaggieAppleton

Copy link
Copy Markdown
Collaborator Author

Review (follow-up): Looks good. One recommended visual change: centre the pencil at rest.

I re-checked head edc89952 (rebased onto main after #517) in headless Chromium at 393, 375, 320 and landscape (852x393).

Earlier must-fixes: both resolved

  • The circle's icon now computes to brand ink (oklch(0.439 0.075 210)) at every size.
  • The Decisions count shows on the minimised circle, landscape included, pinned at the top-right. It no longer covers the icon and isn't clipped.
  • In landscape the bar now shrinks toward the pencil, so the minimised circle sits next to it.

Measurements

  • Label clearance:

    Width Document Decisions
    393 9.5px 10.7px
    375 7.3px 8.4px
    320 4.7px 6.0px
    Landscape 12.3px 24.3px

    At 320 there's still about 3.9px for Document even if CI's Linux text renders 0.8px tighter.

  • Concentric corners: bar 20px, inset 5px, pill 15px with Chat and Build selected. In landscape: bar 20px, inset 2px, pill 18px.

  • Slow drag: 3px per frame with a 4px tremor every tenth frame now minimises once and stays minimised. The 3px wobble at the bottom of the scroll still doesn't toggle it.

  • Unchanged and still fine: tap and focus restore and move focus to the current tab. Chat and reduced motion never minimise. Desktop has no tab row.

  • First-load skeleton: the bar and pencil boxes still match the real ones to the pixel at all four sizes, and phone-skeleton.test.tsx passes.

  • --phone-bottom-chrome: 84px in portrait and 66px in landscape, the same in both states.

  • Minimise animation: the bar and pencil bottoms stay level at every frame.

  • CI: e2e and container pass. Validation fails only on the listed workspace.tsx review-hash exception.

Recommended: centre the pencil at rest

With align-items: flex-end, the pencil's top sits below the bar's top:

Width Pencil Bar Pencil top lower by
393 54px 62px 8px
320 44px 62px 18px

At 393 it reads slightly low. At 320 it clearly looks like it has slipped (pr-320-document-bar.png). iOS 26 and the jig centre the accessory against the bar.

Use align-items: center on .phone-tab-row and .phone-skeleton-tabs. Both end at 52px when minimised, so the two still finish exactly aligned. The only cost is the pencil's bottom easing down 4px at 393 (9px at 320) as it shrinks, which reads as settling into the circle, not jitter. If you want the bottoms level through the whole animation as well, keep flex-end and add margin-block-end: calc((var(--phone-tab-bar-height) - var(--phone-tab-action-size)) / 2) to .phone-tab-action at rest, transitioned to 0 when minimised.

Nice to have: the pencil at 320 is now 44px, against 48px earlier. It meets the target, but next to a 62px bar it looks undersized. Consider 48px.

MaggieAppleton and others added 6 commits October 12, 2026 14:34
The phone tab bar takes iOS 26 proportions: a 62px bar with 10px semibold
labels, a 54px action circle, and a selection pill whose corners are
concentric with the bar (pill radius = bar radius - inset), so labels
clear the pill's curve at every phone width. Reading down a pane shrinks
the bar to one circle showing the current tab, on the same scroll signal
that tucks the top bar away; scrolling up, focus, or a tap restores it.
Reduced motion keeps the full bar. The first-paint skeleton follows the
new geometry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A minimised bar's inert tabs are also aria-hidden, so only the Show tabs
circle is exposed. The bar's corners drop to 22px (pill 17px, still
concentric with the 5px inset), which keeps "Decisions" clear of the
pill's curve at 320 under Linux font rendering too. Tests that read down
a pane and then switch tabs restore the bar first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The minimised circle's icon takes brand ink, and its count or activity
  dot sits at the circle's corner instead of over the icon, landscape too.
- Landscape shrinks the bar toward its action, so the circle and pencil
  stay together.
- Scroll direction accumulates travel since the last change of direction,
  so slow drags still tuck and restore both bars.
- The pencil's bottom edge stays level with the bar while both shrink.
- Corners drop to 20px (pill 15px, still concentric), and the narrowest
  phones use a 44px action and tighter tracking, so even "Document" keeps
  label clearance at 320 under Linux font metrics.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The pencil sits on the bar's centre line at rest and both shrink to the
same circle when minimised. The narrowest phones keep a 48px action:
"Document" still clears its pill by over 3px at 320.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- apps/web/src/workspace.tsx: passes two props to the phone tabs, a boolean
  `minimised` from the top bar's hidden state and the `onRestore` callback;
  no new className, style, or dynamic class logic.
- The preserved-values typography exception for --phone-tab-label-size was
  checked: one case, one selector (.phone-tab), the token's only use.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaggieAppleton
MaggieAppleton enabled auto-merge (rebase) October 12, 2026 13:36
@MaggieAppleton
MaggieAppleton merged commit aab423b into main Oct 12, 2026
3 checks passed
@MaggieAppleton
MaggieAppleton deleted the mobile/tab-bar-e branch October 12, 2026 13:58
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