@cssints/stuck

Style what is inside a sticky element while it is stuck, for cssints: sticky() makes an element sticky and a scroll-state container, stuck(...) wraps styles of its descendants in @container scroll-state(stuck: top) (Chrome 133). Where a target lacks scroll-state queries, stuck(...) also emits the same styles under an attribute that a tiny IntersectionObserver sets, in the way of bramus/sticky-observer.

import { cn, color, hover } from "cssints" with { type: "cssints" };
import { sticky, stuck } from "@cssints/stuck" with { type: "cssints" };

export const header = sticky().top("0"); // <header class={header}>
export const title = cn(color("gray"), stuck(color("red"))); //   <h1 class={title}>
export const footer = sticky().bottom(2);
export const link = stuck("bottom")(color("green"), hover(color("black")));
@layer _.k {
  ._stuck-core {
    position: sticky;
    container-type: scroll-state;
  }
}
@layer _.a {
  ._4k81c2 {
    top: 0;
  }
  ._kg6w97 {
    bottom: 0.5rem;
  }
}
@layer _.b {
  @container scroll-state(stuck: top) {
    ._it4l7h {
      color: red;
    }
  }
  /* only when a browserslist target lacks scroll-state queries */
  :where(._g-stuck[data-stuck~="top"]) ._fec935 {
    color: red;
  }
}

header is _stuck-core _g-stuck _4k81c2, title holds both _it4l7h and _fec935.

Browsers

Rules

Limits

cd packages/stuck && bun run test            # fixtures through the engine (output, targets, errors, observer size)
cd packages/stuck && bun run typecheck       # the type test
cd packages/stuck && bun run test:browsers   # Chrome, Firefox and WebKit through playwright-core

Credits

The fallback follows bramus/sticky-observer (MIT, Bramus Van Damme): an IntersectionObserver whose root is shrunk by the element's inset plus 1px. The observer is written here; no code is copied.