fix(nav): stop the small-screen layout overflowing and give navigation real touch targets #170

Sammanfogat
supernaut sammanfogade 6 incheckningar från fix/nav-small-screen in i main 2026-08-02 19:38:41 +00:00
Ägare

Closes #153, closes #160, closes #155, closes #159. Refs #156.

Small-screen layout, reflow and target size. Deliberately adds no disclosure or hamburger — the
header has no JavaScript today and that is worth keeping.

Ordering was load-bearing: the row has to wrap (#153) before targets grow (#155), or the 320 px
overflow gets worse rather than better.

Reflow — measured, scrollWidth vs clientWidth

width / /docs/ /pricing/ /en/pricing/
320 347 → 320 348 → 320 437 → 320 387 → 320
375 ok ok 438 → 375 388 → 375
414 ok ok 437 → 414 ok

Zero overflow across 8 widths × 6 pages. 768 px and above is unchanged.

A second overflow source only became visible once the first was fixed: at 320 px the Dokumentation
heading is 321 px in a 272 px column. Fixed with overflow-wrap: break-word on headings.

#160 restacks the add-ons table below small rather than adding a scroller — each cell carries
its column heading as a visible label and thead is dropped, so there is no unlabelled tab stop.

#155 gives every header, footer and breadcrumb target 48 px on the block axis, honouring
--min-touch-target, which the repo already applies to buttons and the logo but to no navigation
link. It previously passed SC 2.5.8 only through the spacing exception — compliance that breaks the
moment any gap is tightened.

#159 adds env(safe-area-inset-*) behind @supports. Verified with CDP
Emulation.setSafeAreaInsetsOverride at 780×375 landscape: link boxes moved from 30..750 — twelve
links inside the 44 px notch band — to 74..706, none inside. Values are byte-identical where no
insets exist.

#156 is partial, and deliberately so

Header height at 375 px: 278 baseline → 210. The issue asks for ≤170 px, which is not reachable
while every target is 48 px. The header is now three rows — brand band plus two navigation rows —
which is 144 px of targets; the remaining 66 px is padding and gaps. Reaching 170 means cutting
~40 px more of padding (brand 32→8, nav 16→8, gap 16→8), which is a visual-density decision rather
than a layout one.

Per the operator decision, the small-screen logo now matches the footer's 24 px, which is what
freed the third row. #156 stays open for the padding question.

Verification

pnpm lint, pnpm check, pnpm test (206 passing). src/lib/nav.test.ts untouched;
aria-current="page" still marks the current page in both languages.

Closes #153, closes #160, closes #155, closes #159. Refs #156. Small-screen layout, reflow and target size. Deliberately adds **no** disclosure or hamburger — the header has no JavaScript today and that is worth keeping. Ordering was load-bearing: the row has to wrap (#153) before targets grow (#155), or the 320 px overflow gets worse rather than better. ### Reflow — measured, `scrollWidth` vs `clientWidth` | width | `/` | `/docs/` | `/pricing/` | `/en/pricing/` | | --- | --- | --- | --- | --- | | 320 | 347 → **320** | 348 → **320** | 437 → **320** | 387 → **320** | | 375 | ok | ok | 438 → **375** | 388 → **375** | | 414 | ok | ok | 437 → **414** | ok | Zero overflow across 8 widths × 6 pages. 768 px and above is unchanged. A second overflow source only became visible once the first was fixed: at 320 px the `Dokumentation` heading is 321 px in a 272 px column. Fixed with `overflow-wrap: break-word` on headings. **#160** restacks the add-ons table below `small` rather than adding a scroller — each cell carries its column heading as a visible label and `thead` is dropped, so there is no unlabelled tab stop. **#155** gives every header, footer and breadcrumb target 48 px on the block axis, honouring `--min-touch-target`, which the repo already applies to buttons and the logo but to no navigation link. It previously passed SC 2.5.8 only through the spacing exception — compliance that breaks the moment any gap is tightened. **#159** adds `env(safe-area-inset-*)` behind `@supports`. Verified with CDP `Emulation.setSafeAreaInsetsOverride` at 780×375 landscape: link boxes moved from `30..750` — twelve links inside the 44 px notch band — to `74..706`, none inside. Values are byte-identical where no insets exist. ### #156 is partial, and deliberately so Header height at 375 px: **278 baseline → 210**. The issue asks for ≤170 px, which is not reachable while every target is 48 px. The header is now three rows — brand band plus two navigation rows — which is 144 px of targets; the remaining 66 px is padding and gaps. Reaching 170 means cutting ~40 px more of padding (brand 32→8, nav 16→8, gap 16→8), which is a visual-density decision rather than a layout one. Per the operator decision, the small-screen logo now matches the footer's 24 px, which is what freed the third row. `#156` stays open for the padding question. ### Verification `pnpm lint`, `pnpm check`, `pnpm test` (206 passing). `src/lib/nav.test.ts` untouched; `aria-current="page"` still marks the current page in both languages.
supernaut lade till 6 incheckningar 2026-08-02 15:53:19 +00:00
The two child navigations each paid `padding-inline: var(--page-margin)`, so a single
320 px row spent 96 px on page margins, and `flex-wrap: nowrap` meant the row could never
be narrower than the sum of their min-content widths (163 + 160 + 24 = 347 px). The whole
document scrolled sideways on every page — WCAG 2.2 SC 1.4.10 (Reflow, AA).

The page margin is now applied once, on `.main-navigation`, and the row wraps below
`small` so the two navigations move onto separate rows. Headings also get
`overflow-wrap: break-word`: the 40 px `Dokumentation` h1 on /docs/ is 321 px wide inside
a 272 px column at 320 px, which was masked by the larger navigation overflow.

Measured at 320 x 780, isMobile, deviceScaleFactor 2:

    /            347 -> 320
    /en/         347 -> 320
    /docs/       348 -> 320
    /docs/faq/   348 -> 320

Layout at 768 px and above is unchanged (header 221.6 px before and after on every page).

Closes #153.
The add-ons table holds a full sentence in its third column, so its min-content width is
413 px: `table { max-inline-size: 100% }` cannot shrink that, and /pricing/ scrolled the
page sideways at every width up to 414 px — WCAG 2.2 SC 1.4.10 (Reflow, AA).

Below `small` the table now restacks into one bordered block per row. Each cell carries
its column heading as a visible label, so the relationship the header row expressed
travels with the cell and `thead` is dropped rather than duplicated; above the breakpoint
the label is hidden again and the real header row does the work. The shared rule lives
next to the other `src/style/shared` partials, keyed off the `.table` class both pricing
pages already used.

Measured, documentElement.scrollWidth vs clientWidth:

    320 px  /pricing/ 437 -> 320   /en/pricing/ 387 -> 320
    375 px  /pricing/ 438 -> 375   /en/pricing/ 388 -> 375
    414 px  /pricing/ 437 -> 414   /en/pricing/ 414 -> 414

No scroll container was introduced, so there is no unlabelled tab stop. The table at
768 px and above is byte-for-byte the same layout.

Closes #160.
Every header, footer and breadcrumb link was about 19 px tall and met SC 2.5.8 only
through the spacing exception — the 16 px gaps happened to keep 24 px circles from
intersecting. That is compliance by accident: tightening a gap or adding a fifth link
breaks it, and SC 2.5.5 (AAA), Apple's 44 pt and Android's 48 dp were failed outright.

Below `medium` every navigation target now honours --min-touch-target (3rem = 48 px), the
token .button, the FAQ filter and the logo already use. The rule is shared rather than
repeated in five components, because Astro's scoped styles cannot reach into a child
component's markup; the element part of each selector keeps it above the components' own
scoped `.icon-link` / `.logo` display rules.

The growth is box height, not type size — min-block-size with centred flex alignment — and
the 16 px gaps are untouched.

Measured at 320/360/375/414/480/600/719 px on /, /en/, /docs/, /docs/faq/, /pricing/ and
/en/pricing/: every header, footer and breadcrumb target is now at least 44 px on its block
axis (48 px in practice), down from 15-18.7 px. Header height at 720 px and above is
unchanged at 221.6 px.

The header does grow at narrow widths (375 px: 277.6 -> 387) — that is what the next
commit takes back.

Closes #155.
Nothing in the header responded to a breakpoint: the 2 rem that gives a desktop header its
air was the same 2 rem on a 320 px phone, and the top band was three stacked full-width
rows — account links, a 112 px brand band, main navigation — before any content.

Below `medium`:

- the brand and the user menu share one row, logo left and account links right. A
  `.header-band` wrapper holds them; it is `display: contents` above the breakpoint, so the
  desktop header stays exactly the column it was.
- `.brand` padding drops from 2 rem to 1 rem and the header logo from 48 to 40 px. The logo
  may also scale below its intrinsic width rather than force the row wider than the
  viewport.
- the main navigation's 1 em band is halved: its links now carry their own 48 px target, so
  that padding was height paid twice.
- the language switcher moves to the footer (operator decision). It is relocated, not
  concealed — the footer copy is shown at exactly the widths where the header copy is
  hidden, and `display: none` keeps only one of the two in the accessibility tree. Accepted
  trade-off: with no Accept-Language detection, a reader who lands in the wrong language
  must scroll to change it; the mitigation, if it bites, is detection rather than moving the
  control back.

Below `small` the two child navigations also stop generating boxes, so their five links wrap
as one flow instead of the app link always claiming a row of its own. Verified against the
Chrome accessibility tree at 375 px and 768 px: both landmarks keep their labels and their
links, and exactly one language navigation is exposed at each width.

Measured header height, px:

    width   before   after #155   now
    320     354.9    459          242  (306 on /en/, one row more)
    360     312.3    387          242
    375     277.6    387          242
    414     277.6    323          242
    600     258.9    275          146
    768     221.6    221.6        221.6  (unchanged, as at 900 and 1280)

Header height no longer varies between pages at the same width. Two acceptance numbers are
NOT met and need a human decision: 170 px at 375 px, and <main> starting within 220 px on a
page with breadcrumbs (it starts at 319 px). See the note on the issue — with 48 px targets,
eight visible targets and a 327 px usable row, four rows of navigation is the floor.

Closes #156.
base.astro has asked for `viewport-fit=cover` all along, but nothing under src/ ever
referenced `env(safe-area-inset-*)`. The page margin is 24 px and the inset on a notched
phone in landscape is about 44 px, so the first navigation link, the account links and the
footer's first column sat partly under the notch or a rounded corner; in portrait the
footer's last row sat under the home indicator.

The inline insets are now honoured by .site-header, .breadcrumbs, .page-content and
.site-footer — breadcrumbs included because they are navigation too — and the block-end
inset by .site-footer, which is the only one of the four that ever meets the bottom of the
viewport. Where the element already pays a page margin the inset is added to it rather than
replacing it. Everything sits behind `@supports (padding: env(safe-area-inset-left))`.

Verified in Chromium at 780 x 375 (landscape) with CDP
Emulation.setSafeAreaInsetsOverride at left 44, right 44, bottom 21:

    insets none  link boxes span 30..750   12 links inside the 44 px notch band
    insets set   link boxes span 74..706   0 links inside the 44 px notch band
                 header padding-inline-start 44px, main 44px,
                 footer padding-inline 68px/68px, padding-block-end 53px

With no insets the computed values are unchanged: header 0, main 0, footer 24/24 and 32.
Reflow, target sizes and header heights at every measured width are identical to before
this commit.

Closes #159.
fix(nav): drop the small-screen header logo to footer size
Alla kontroller lyckades
ci / ci (pull_request) Successful in 1m14s
45c61631b6
Eight navigation targets at the 48 px minimum cannot fit in three rows while
the logo holds its 40 px height, so the header stayed a row taller than it
needed to be. At the 5:1 viewBox, 24 px matches the footer logo and frees the
width for three rows.

Header height at 375 px drops from 242 px to 210 px; the remaining height is
three 48 px target rows plus padding. Desktop is unchanged at 221.6 px and no
page overflows at any width from 320 px up.

Refs #156
supernaut tvångsskickade fix/nav-small-screen från 45c61631b6
Alla kontroller lyckades
ci / ci (pull_request) Successful in 1m14s
till 958050d3f5
Alla kontroller lyckades
ci / ci (pull_request) Successful in 1m15s
2026-08-02 19:25:32 +00:00
Jämför
supernaut tvångsskickade fix/nav-small-screen från 958050d3f5
Alla kontroller lyckades
ci / ci (pull_request) Successful in 1m15s
till 316f645a45
Alla kontroller lyckades
ci / ci (pull_request) Successful in 1m15s
2026-08-02 19:29:35 +00:00
Jämför
supernaut sammanfogade incheckning 2d2b925936 till main 2026-08-02 19:38:41 +00:00
supernaut tog bort grenen fix/nav-small-screen 2026-08-02 19:38:41 +00:00
Logga in för att delta i denna konversation.
Inga granskare
Ingen milstolpe
Inget projekt
Inga tilldelade
1 deltagare
Notiser
Förfallodatum
Förfallodatumet är ogiltigt eller utanför gränserna. Använd formatet "åååå-mm-dd".

Inget förfallodatum satt.

Beroenden

Inga beroenden satta

Referens
bitborg/bitborg-web!170
Ingen beskrivning angiven.