Scroll-driven patterns and position: sticky done properly. Sticky headers, section nav, and scroll snapping can improve navigation or ruin it if overflow and stacking contexts are wrong. The goal is patterns that work without JavaScript where CSS is enough.
position: sticky basics
Sticky is relative until you scroll past a threshold, then it behaves like fixed within its containing block:
.site-header {
position: sticky;
top: 0;
z-index: 10;
background: var(--color-surface, #fff);
border-block-end: 1px solid #e5e7eb;
}
top: 0 means ‘stick when the top edge would scroll off’. You need a scroll ancestor – usually the viewport. Sticky stops at the bottom edge of its parent; it will not float over the footer.
Why sticky fails (common causes)
- Overflow hidden on an ancestor –
overflow: hidden,auto, orscrollon a parent creates a scroll container and traps sticky. - No top/bottom inset – sticky needs
top,bottom,inline-start, orblock-startset. - Parent too short – nothing to stick within if the parent height equals the sticky element.
- Flex/grid child without align-self – a sticky item in a flex row may stretch; sometimes
align-self: starthelps.
.layout-with-sidebar {
display: grid;
grid-template-columns: 240px 1fr;
gap: 2rem;
}
.sidebar {
position: sticky;
top: 1rem;
align-self: start; /* do not stretch to full row height */
max-block-size: calc(100dvh - 2rem);
overflow-y: auto;
}
Sticky section headings
Long docs sometimes stick H2s below the site header so context stays visible:
:root {
--header-height: 3.5rem;
}
.prose h2 {
position: sticky;
top: var(--header-height);
background: var(--color-surface);
padding-block: 0.5rem;
z-index: 1;
}
Give sticky headings a background or they overlap text scrolling underneath. Subtle bottom border optional.
Scroll snapping
Carousels and full-page sections can use scroll snap. Use it sparingly – aggressive snap feels like fighting the user on long pages:
.snap-carousel {
display: flex;
overflow-x: auto;
scroll-snap-type: x mandatory;
gap: 1rem;
scroll-behavior: smooth;
}
.snap-carousel > * {
flex: 0 0 85%;
scroll-snap-align: start;
}
mandatory always snaps; proximity snaps only when close. Prefer proximity for vertical page sections unless the design is explicitly slide-based.
scroll-margin for anchor links
Sticky headers cover in-page anchor targets. Offset with scroll-margin-block-start:
[id] {
scroll-margin-block-start: calc(var(--header-height) + 1rem);
}
Apply to heading IDs or section wrappers. One rule fixes ‘jump link hidden under header’ on every page.
Scroll-driven animations (overview)
CSS can tie animations to scroll progress via animation-timeline: scroll() and related properties. Support is still rolling out; treat as enhancement:
@keyframes fade-in {
from { opacity: 0; transform: translateY(1rem); }
to { opacity: 1; transform: translateY(0); }
}
.reveal {
animation: fade-in linear both;
animation-timeline: view();
animation-range: entry 0% entry 40%;
}
When support is thin, keep content visible without the animation. Respect prefers-reduced-motion (covered in a later tutorial) – disable scroll-linked motion for users who ask for less movement.
UX guardrails
Sticky nav is helpful until it eats vertical space on a phone. Consider hiding or shrinking the header on scroll with a tiny script, or accept a slim sticky bar. Scroll snap on the main document annoys people trying to skim – limit snap to intentional components like horizontal galleries.
Always test keyboard scroll and anchor navigation, not just wheel and touch.
The next tutorial covers forms and UI states in CSS – focus-visible, invalid, disabled, and styling that helps rather than hinders.

