main ← constrain-tooltips-to-viewport
merged 07:11AM - 12 Aug 26 UTC
FloatKit gives a tooltip a fixed `maxWidth` and no height limit at all. That is …fine for the short labels most tooltips carry, but it breaks down as soon as one holds user content — footnotes, chat reaction lists and event descriptions all render arbitrary-length HTML into a tooltip, and the result is a popup bigger than the screen it has to fit on.
Neither axis recovers on its own, because floating-ui's `shift` can only *move* a float, never shrink it.
**Vertically** it clamps the popup flush under the header and leaves the rest below the fold. Because the float is re-anchored to its trigger on every scroll, its viewport rectangle never changes, so the hidden part cannot be reached by scrolling — the page just slides underneath it. Measured on a 390×664 phone with a long footnote: a 1557px popup with 945px permanently unreachable, and swiping inside it scrolled the page from 871 to 1381 while revealing nothing.
**Horizontally** the same clamp pins a 350px popup 10px from the left edge of a 320px screen, so the overflow joins the document's scrollable area and the whole page — header included — can be panned sideways (42px, growing with each pan).
## What changed
- `.fk-d-tooltip__inner-content` is bounded to `60dvh` and scrolls, which is what menus have done since they gained `overflow: auto`. Tooltips kept the `overflow: hidden` they were born with and were simply never revisited, so the asymmetry was drift rather than a decision.
- Alignment becomes `align-items: safe center` at the same time. The cross axis is the one being capped, so plain `center` would centre content that is too tall and push its first lines above the scrollport, where scrolling cannot follow — measured at 830px out of reach.
- `maxWidth` is clamped against the space the viewport actually leaves, derived from the same padding `shift` keeps clear so the two cannot disagree.
- Only *numeric* `maxWidth` values are clamped. The option also accepts CSS keywords (the user card passes `unset`), and wrapping a keyword in `min()` is invalid CSS — the browser would drop the declaration and hand the float to whatever stylesheet rule was previously being overridden.
`overflow-x` stays `hidden` rather than becoming `auto`: the same rule carries `overflow-wrap: break-word`, so `auto` on both axes would give tooltips a horizontal scrollbar for unbreakable content.
## Before / after
A long footnote on a 390px-wide phone, and the same popup after swiping inside it:
| | Before | After |
|---|---|---|
| popup height | 1557px | 400px |
| clipped below the fold | 945px | 0 |
| scrollable | no | yes (1157px of scroll) |
| swiping inside it | scrolls the page, popup unchanged | scrolls the footnote, page stays put |
| 320px screen | 352px wide, page pans 42px | 302px wide, no pan |
<img width="844" height="751" alt="footnote-height-before-after" src="https://github.com/user-attachments/assets/64a8107d-47c2-45f9-9a77-75f121ec8b8f" />
<img width="844" height="782" alt="footnote-narrow-before-after" src="https://github.com/user-attachments/assets/6dbfffaf-2885-4836-b020-3ee2d31ecfcb" />
<img width="844" height="751" alt="footnote-scroll-before-after" src="https://github.com/user-attachments/assets/9b0dade9-0238-4e44-b10f-2d48bbe0da57" />
## Testing
- `d-tooltip-test.gjs` gains a height/scroll test and a keyword-`maxWidth` test, and the two numeric `maxWidth` cases are folded into one. Each new assertion was checked to fail against the specific declaration it guards: dropping `safe`, `box-sizing`, or `max-height` each turn the scroll test red, and wrapping keywords in `min()` turns the keyword test red.
- A footnote system spec covers the end-to-end mobile case at 320px — real cooked content, real touch scroll, and the document-level horizontal overflow that a rendering test cannot observe.
- Ran the tooltip- and menu-heavy core system specs (cards, user tips, filter navigation, styleguide, localization menus, composer, bookmarks, chat reactions, calendar) — ~200 examples, no new failures.