CSS Behaviours That Have Bitten Us

Purpose

Browser behaviours that are correct per spec, invisible in the code that triggers them, and have each caused more than one real bug in a TimelyCare app. Read this before building any component that scrolls, clips, or pins.

Each entry names the rule, what it broke, and what to do instead. Add to it when a browser behaviour costs someone a debugging session twice.


1. One scroll axis turns on the other

The rule. If overflow-x and overflow-y disagree — one computes to visible and the other does not — the visible one computes to auto instead. Per CSS Overflow 3, the two axes cannot be independently visible and something else.

So this:

overflow-x: auto;   /* what you wrote */

is really this:

overflow-x: auto;
overflow-y: auto;   /* what the browser computed */

The box became a scroll container on both axes, and nothing in the source says so. overflow-x: hidden does the same thing.

What it broke. The admin-web migration report attributes three separate bugs to this one rule. The one Helix has fixed is the sticky table header: sticky pins to the nearest scrollport, the wrapper had silently become that scrollport, and its height was unconstrained — so it never scrolled, the header never had anything to pin against, and a hand-rolled sticky top-0 on TableHeader did nothing at all.

The mechanisms that make the rule dangerous, and that any new scrolling component inherits:

MechanismConsequence
A scroll container is the reference for position: stickySticky children pin to this box, not the page — and not at all if the box is unbounded.
A scroll container clips its overflow instead of painting outside itContent that used to hang out of the box — a popover, a focus ring — gets cut off.
A scroll container is the reference for programmatic scrollingscrollIntoView and anchor navigation resolve against this box rather than the page.

What to do.

  • Treat any overflow-x-auto or overflow-x-hidden as declaring a scroll container, and say so in a comment where you write it.
  • If you need sticky positioning inside it, the container must also be height-bounded — an unbounded scroll container never scrolls, so sticky has no scrollport to pin against and silently does nothing. This is why Table's stickyHeader requires maxHeight and warns in development without it (see Table).
  • If you only wanted horizontal scrolling, you cannot have it alone. Decide what the vertical axis should do and set it explicitly, rather than inheriting auto by accident.
  • Do not wrap a component that already scrolls in a second scrolling wrapper. Table renders its own overflow-x-auto container; adding another nests two scrollports.

Where this already applies in Helix. Command, Select, DropdownMenu and ContextMenu content each pair overflow-x-hidden with an explicit overflow-y-auto, so the computed value is the one the author chose.

Table's container is the exception worth knowing: it carries overflow-x-auto alone, and therefore is a vertical scroll container by computation. That is deliberate — it is what stickyHeader pins against — but it is also why stickyHeader is a no-op without maxHeight, and why wrapping a Table in your own scrolling div nests two scrollports.