# Scroll-revealed rows

A list where the row under the pointer opens to show an image behind its
type — and, on a touch screen, the row nearest the middle of the screen
opens itself as you scroll.

## Height, not max-height

```css
.row {
  height: 104px;
  overflow: hidden;
  transition: height .55s cubic-bezier(.22,.61,.36,1), color .4s;
}
.row:hover { height: 214px; color: #fff; }
```

`max-height` is the usual shortcut and it is wrong here: it has to be set
larger than the content, so the transition spends its first part covering
distance that does not exist and the row appears to hesitate before moving.
Two known heights transition honestly.

**The image and the scrim are already in the row at rest, at zero opacity.**
Inserting them on hover means laying out a new node mid-transition, which is
what makes this pattern jolt.

## The touch equivalent

A phone has no hover, so the whole effect is `display: none` unless you
build the other half. Scroll-driven rather than tap: these are a list you
read past, and making someone tap each row to discover it had a picture
behind it is a worse deal than showing them.

```js
const pick = () => {
  const mid = window.innerHeight / 2
  let best = -1, bestDist = Infinity
  Array.from(list.children).forEach((row, i) => {
    const r = row.getBoundingClientRect()
    if (r.bottom < 0 || r.top > window.innerHeight) return
    const d = Math.abs(r.top + r.height / 2 - mid)
    if (d < bestDist) { bestDist = d; best = i }
  })
  setActive(best)
}
```

## Traps

**Gate it on the breakpoint, not on whether a pointer exists.** They are
not the same question. A desktop window dragged narrow gets the mobile CSS
— rows at auto height, image at `display: none` — while still reporting
`hover: hover`, so a pointer test bails out of the scroll handler and the
hover rules have nothing left to show. *Neither path runs.* Match the media
query the stylesheet uses and re-decide on every crossing of it.

**Synchronise the durations in the scroll-driven version.** On hover, a
ground that fades over 0.55s while the type turns white over 0.4s is hidden
by the row also changing height. Scroll-driven, nothing changes height and
the active row switches constantly — so that gap leaves the text white on a
background that has not arrived, several times per scroll. One short
duration for every property, in both directions.

**Scrim weights are tuned to the picture.** Values set against a busy
photograph of people will crush a graphic image flat. Keep the scrim heavy
only where the type actually lands.

## Prompt

> Build a list of rows that open on hover to reveal a background image with
> a scrim and white type over it. Animate between two known heights, never
> max-height, and keep the image and scrim in the DOM at zero opacity rather
> than inserting them. Below the breakpoint, where there is no hover, open
> the row nearest the vertical centre of the viewport as the user scrolls —
> gate that on the same media query the stylesheet uses, not on pointer
> capability, and give every animated property one short matching duration
> so no switch leaves text on a background that has not arrived.
