WebCraft

position: sticky (inline)

A wide table that keeps its first column

CitySessionsBouncePages
Auckland4 81261%2.4
Bergen3 19058%2.1
Cádiz2 77464%2.9
Dakar2 01549%1.8
Esbjerg1 64052%2.0
Faro1 28847%1.6
Galway1 00444%1.4

CaveatThe corner cell is sticky in two directions at once, so it has to outrank both the header row and the pinned column. Give it the highest z-index of the three or the scrolling cells paint straight over it.

FallbackEvery sticky cell needs its own opaque background. Sticky does not create a new layer, so without one the rows underneath show through as you scroll.

CSS
/* the frozen column */
.pin { position: sticky; left: 0; z-index: 1; }
thead th { position: sticky; top: 0; z-index: 2; }

/* both at once, so it beats both */
thead .pin { z-index: 3; }

Published

Questions

<details name>

Why do the scrolling cells paint straight over my pinned first column?

Because position: sticky does not create a layer of its own — the cell stays in normal flow and stays transparent. Give every sticky cell an opaque background of its own, and give the corner cell a z-index higher than both the header row and the column.

Can a table have a sticky header row and a sticky first column at the same time?

Yes, and nothing extra is needed for it — each cell sticks per axis against the scroller that actually scrolls on that axis. The one cell that needs thought is the corner: it is sticky in both directions at once and has to outrank the other two.

Why does my sticky column do nothing at all?

Sticky resolves against the nearest scrolling ancestor, so the table needs a wrapper that actually scrolls — overflow: auto on it. An overflow: hidden anywhere up the tree kills it silently, which is the usual reason it works in a codepen and not in the app.