CSS position: sticky - How It Works, With Examples
How CSS position: sticky works, with copy-paste examples for sticky headers, sidebars and table headers, sticky vs fixed, and the container rules that break it.
9 min read - Updated 2026-10-20
position: sticky is a hybrid of relative and fixed positioning: an element scrolls normally until it reaches a threshold you set (like top: 0), then sticks in place inside its container until that container scrolls away. This guide covers how it behaves, copy-paste patterns for the three places you will actually use it (headers, sidebars and table headers), and the container rules that make it silently do nothing.
How CSS sticky positioning behaves
A sticky element stays in normal flow and keeps its space in the layout. It scrolls with the page until its offset threshold is crossed, then pins relative to its nearest scrolling ancestor. It is always constrained to its containing block: when the parent's bottom edge reaches it, the sticky element is pushed back out of view.
.toolbar {
position: sticky;
top: 0; /* the threshold: stick when the top edge reaches 0px */
}Relative until the threshold, then fixed-like inside the scroll container
It keeps its space in the layout, so nothing jumps when it starts sticking
It never escapes its parent: a short parent means a short sticky run
Sibling sticky elements with the same offset replace each other as you scroll
You must set a threshold offset
Sticky does nothing without at least one of top, right, bottom or left (or the logical inset-block-start / inset-inline-start). The offset is the distance from the scroll container's edge where the element stops and sticks. If your page already has a fixed header, add its height so the two don't overlap.
:root { --header-h: 64px; }
.site-header { position: fixed; top: 0; height: var(--header-h); }
.section-nav {
position: sticky;
top: var(--header-h); /* sit just below the fixed header */
}
h2[id] { scroll-margin-top: calc(var(--header-h) + 48px); }top: 0 pins the element to the top edge once it gets there
bottom: 0 works for footers, action bars and reading-progress bars
left: 0 sticks the first column of a horizontally scrolling table
Pair with scroll-margin-top so anchor links land below the sticky bar
Sticky header in CSS
The most common use: a header that scrolls in with the page and then stays at the top. Give it a background (otherwise content shows through as it scrolls under) and a z-index so later positioned elements don't paint over it. Put it as a direct child of body or of a tall page wrapper, so its container is the full page.
.site-header {
position: sticky;
top: 0;
z-index: 50;
background: rgb(255 255 255 / 0.9);
backdrop-filter: blur(8px);
border-bottom: 1px solid #e5e7eb;
}Always set a background color on a sticky header
Add a z-index above your content's positioned elements
Keep its parent tall: a header inside a short <div> unsticks early
Use backdrop-filter for a frosted effect, with a semi-opaque background as fallback
Sticky sidebar with grid or flexbox
A sticky sidebar or table of contents is where most people hit the stretch trap. Grid and flex items stretch to the full height of the row by default, so the sidebar is already as tall as its container and has no room to travel. Set align-self: start so the sidebar is only as tall as its content.
.layout {
display: grid;
grid-template-columns: 240px 1fr;
gap: 2rem;
}
.toc {
position: sticky;
top: 80px;
align-self: start; /* without this the item stretches and never sticks */
max-height: calc(100vh - 96px);
overflow-y: auto;
}
@media (max-width: 768px) {
.layout { grid-template-columns: 1fr; }
.toc { position: static; max-height: none; }
}align-self: start (or align-items: start on the container) is the key line
max-height plus overflow-y: auto lets a long table of contents scroll on its own
Offset it with the same top value as your header height
Check narrow widths: most sidebars should stop being sticky once they stack
Sticky table headers and first columns
Put position: sticky on the th cells, not on the table. For a scrollable table, wrap it in a container with a fixed height and overflow: auto, so that wrapper becomes the scroll container the headers stick to. A sticky first column works the same way with left: 0.
.table-wrap { max-height: 420px; overflow: auto; }
table { border-collapse: separate; border-spacing: 0; }
thead th {
position: sticky;
top: 0;
z-index: 2;
background: #fff;
box-shadow: inset 0 -1px 0 #e5e7eb;
}
tbody th { position: sticky; left: 0; background: #fff; z-index: 1; }
thead th:first-child { left: 0; z-index: 3; }Stick the th cells; that's the pattern every modern browser supports
Give header cells a background or rows show through them
With border-collapse: collapse, borders on sticky cells scroll away; use a box-shadow or border-collapse: separate
The corner cell needs both top and left plus a higher z-index
position: sticky vs position: fixed
Both pin an element while scrolling, but they solve different problems. Pick fixed when the element must be visible everywhere regardless of layout, and sticky when it belongs to a section of the page.
| position: sticky | position: fixed | |
|---|---|---|
| Stays in normal flow | Yes, keeps its space | No, removed from flow |
| Pinned relative to | Nearest scrolling ancestor | The viewport |
| When it pins | Only after crossing its offset | Always |
| Limited by parent | Yes, unsticks at the parent's end | No |
| Typical use | Section headers, sidebars, table headers | Global nav, chat widgets, cookie bars |
| Usual breaker | overflow on an ancestor | transform or filter on an ancestor |
Fixed leaves a hole in the layout that you have to pad; sticky doesn't
Sticky is scoped to its parent; fixed is scoped to the viewport
A transform, filter or perspective on an ancestor breaks fixed positioning
Overflow on an ancestor breaks sticky, not fixed
The container rules that make or break sticky
Most sticky failures come from an ancestor, not the element itself. Any ancestor between the sticky element and the page with overflow hidden, auto or scroll becomes its scroll container, and the element only sticks inside that box. overflow: clip is the exception: it clips without creating a scroll container, so sticky keeps working.
Swap overflow-x: hidden for overflow-x: clip on page wrappers
The parent needs enough height for the element to travel
Flex and grid children need align-self: start to have room to move
If it's sticking but invisible, check z-index and background before anything else
Test sticky behavior at every width at once
Sticky bugs are usually viewport bugs: a header that is fine on desktop covers the content on a short phone screen, or a sidebar keeps sticking after the layout stacks. Sizzy shows your localhost on phones, tablets and desktops side by side with scrolling synced, so you see every sticky element engage and release at every breakpoint in a single scroll.
Scroll once and watch the sticky header on every device at the same time
Check landscape phones, where a tall sticky header eats most of the screen
Inspect any single device with Chromium DevTools when something looks off
Then spot-check real Safari, which has had the most sticky quirks historically
Release checklist
Set at least one threshold offset (top/bottom/left/right).
Give sticky headers a background and a z-index.
Use align-self: start for sticky grid or flex children.
Replace overflow-x: hidden on wrappers with overflow-x: clip.
Test the sticky behavior at mobile, tablet and desktop widths.
Frequently asked questions
What does position: sticky do in CSS?
It keeps an element in normal flow until it reaches a defined offset (like top: 0) while scrolling, then pins it in place within its containing block until that container scrolls away.
How do I make a sticky header in CSS?
Set position: sticky and top: 0 on the header, give it a background color and a z-index, and make sure it is a direct child of body or a tall wrapper. No ancestor should have overflow: hidden, auto or scroll.
Why is my sticky element not sticking?
The most common causes are a missing threshold offset (you must set top, bottom, left or right), an ancestor with overflow: hidden, auto or scroll, a parent that is too short, and a grid or flex item that stretches to the full height of its container.
What is the difference between position sticky and fixed?
Fixed elements are removed from flow and stay relative to the viewport everywhere. Sticky elements stay in flow, only pin after crossing a threshold, and are constrained to their parent container.
Does position: sticky work in all browsers?
Yes. All current versions of Chrome, Edge, Firefox and Safari support position: sticky without a prefix. Only Safari versions before 13 needed position: -webkit-sticky.
Can I make a table header sticky?
Yes. Apply position: sticky and top: 0 to the thead th cells, give them a background, and wrap the table in a container with a max-height and overflow: auto if the table should scroll on its own.
Sources and further reading
Related guides
Benji
Your life OS
The companion app that keeps every area of your world in sync.
Zero to Shipped
Ship products, not side projects
The ultimate Next.js boilerplate for building and launching real products.
DMX
Mindful Twitter/X
The intentional X client for macOS. Reclaim your attention span.
Sotto
Voice-to-text for macOS
Speak naturally. Type instantly. 100% local & private.
Passlock
Password manager with willpower
Lock passwords with time delays, word challenges, or hand the keys to someone you trust.
Glink
Changelogs that slap
Beautiful changelogs and roadmaps for your product.
Perkz
GitHub access control
Sell access to private GitHub repositories.
Tubely
YouTube Studio for Mac
Manage multiple YouTube channels in one native app.
JustWrite
Distraction-free writing
A minimal writing app that helps you focus on what matters.