Repository navigation
[scheduler] Timeline event pills drift off the model grid when the vertical scrollbar takes space #23167
Description
Found while building the dependency arrows (#22855): the arrows anchor on the data model, and that exposed that the event pills can render away from their model position.
What happens
When the vertical scrollbar takes real layout space, the timeline rows become wider than title column + ticks, and the event pills render shifted to the right of where the model says they are. The shift grows with the distance from the start of the collection: invisible near the left edge, several pixels a few days in. Without dependencies the symptom is subtle — a pill's edges no longer line up with the header's time gridlines. With arrows it is obvious: they anchor on the model, so they visibly start "inside" the predecessor pill.
Why most machines don't show it
Classic scrollbars (with real width) are the default on Windows and Linux; on macOS it is enough to have a mouse connected with the default "Show scroll bars: Automatically" setting. Overlay scrollbars (trackpad-only setups, and CI) report a width of 0 — the rows never get wider, so the bug is invisible there. That's why it went unnoticed.
Reproduction
- Use an environment where scrollbars take layout space (see above).
- Render an
EventTimelinePremiumwith enough resources to get vertical scroll (hasScrollY) and an event on day 3+ of adayAndHourwindow. - Compare the pill's right edge with the header gridline of its end hour: it renders past it.
Measured on an affected machine (dayAndHour, 96 ticks × 64px = 6144px, title column 107px, scrollbar 11px):
| value | expected | actual |
|---|---|---|
--row-width |
6251 (107 + 6144) | 6262 |
| events cell width | 6144 | 6155 |
| pill end vs model x (day 3, fraction 0.61) | 0 | +6.76px |
Root cause
Three things combine:
-
x-virtualizermakes rows wider than the columns on purpose — inherited from the Data Grid, where rows must extend under the native vertical scrollbar:// packages/x-virtualizer/src/features/dimensions.ts const rowWidth = Math.max( viewportOuterSize.width, columnsTotalWidth + (hasScrollY ? scrollbarSize : 0), );
-
The timeline's
EventTimelinePremiumEventsCellhasflex: 1, so it stretches to fill that wider row instead of staying at the ticks width,calc(var(--unit-count) * var(--unit-width))(withflex-basis: 0%itswidthdeclaration is ignored). -
The pills are positioned with percentages of the cell (
--x-position,--width), so they scale with the stretched cell — while the header, the current-time indicator, and the dependency arrows all use the exact ticks width. The mismatch between those two widths is the drift.
The invariant to restore: the events cell's rendered width must equal tickCount × tickWidth, because every percentage inside it assumes that.
Fix
Cap the events cell at the model width: maxWidth: calc(var(--unit-count) * var(--unit-width)). The row can stay wider; the cell — and every percentage inside it — stops following it.
Preferred over the alternative (stopping the virtualizer from adding the scrollbar gutter to rowWidth for the timeline layout) because:
- It restores the invariant against both sources of extra width: the scrollbar gutter and the
Math.maxwithviewportOuterSize.width— a viewport wider than the content produces the same drift, and removing only the gutter term would leave that case broken. - It is local to the timeline (one CSS declaration on the cell) instead of a behavior option in
x-virtualizer, which is shared with the Data Grid.
One detail to take care of: the row's bottom border lives on the cell, so once capped it would stop short of --row-width when the row is wider — the border needs to keep covering the full row (move it to the row, or extend it some other way).
Activity
rita-codes commentedon Aug 25, 2026
Fixed by #23212, which landed the first candidate fix from the description while reworking the events cell for the min/max hour support: the cell is now pinned to the ticks width (
flex: 0 0 auto+width: calc(var(--unit-count) * var(--unit-width))) instead of stretching to--row-width, and the newBodyRowFillersibling carries the row divider across the leftover space — which also covers the bottom-border caveat from the description.Verified against master with the issue's scenario (dayAndHour, 20 scrollable resources, 11px scrollbars): the row width includes the gutter (6259px = 104 title + 6144 ticks + 11 scrollbar) and the events cell stays at exactly 6144px = 96 × 64, with the dependency arrows anchored on the pill edges. Restoring the old
flex: 1cell styles reproduces the drift (cell at 6155px, +11px — the numbers from the table above), confirming it was this bug. The pinned cell also covers the second drift source (theMath.maxwithviewportOuterSize.widthwhen the viewport is wider than the content).github-actions commentedon Aug 25, 2026
This issue has been closed. If you have a similar problem but not exactly the same, please open a new issue.
Now, if you have additional information related to this issue or things that could help future readers, feel free to leave a comment.