CSS Position Sticky
position: sticky is like CSS saying: “Be normal… until you really need to be helpful.”
It behaves like position: relative (it stays in the document flow) until a scroll threshold is
reached, then it behaves like it’s “stuck” to an edge of its scrolling container.
Sticky is perfect for section headers, table headers, “currently reading” sidebars, and those UI bits that should stay visible without fully turning into “always-on-top” fixed elements.
What CSS position: sticky really means
-
It stays in the layout. Unlike
fixed, sticky still reserves its space. -
It needs an offset. No
top/bottom/left/right? No stickiness. - It sticks inside a scroll container. Sticky is constrained by the nearest scrolling ancestor (or the viewport if none).
.sticky {
position: sticky;
top: 12px;
z-index: 1;
}
.sticky {
position: sticky;
top: 48px;
z-index: 1;
}
.sticky {
position: sticky;
top: 0;
z-index: 1;
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
max-width: 980px;
padding: 18px;
display: grid;
gap: 14px;
font-family: system-ui, Arial, sans-serif;
}
.stage {
border: 3px solid #111;
border-radius: 18px;
background: #fff;
box-shadow: 0 12px 0 #111;
overflow: auto;
height: 300px;
}
.content {
padding: 14px;
display: grid;
gap: 12px;
}
.sticky {
border: 2px solid #111;
border-radius: 14px;
background: #f6f6f6;
padding: 12px 14px;
font-weight: 700;
}
.card {
border: 2px solid #111;
border-radius: 14px;
padding: 12px 14px;
background: #fff;
display: grid;
gap: 6px;
}
.card p {
margin: 0;
line-height: 1.4;
opacity: 0.85;
}
I stick to the top of this scroll areaScroll item 1Sticky behaves normally until it hits the top offset.
Scroll item 2Then it “sticks” while its container continues to scroll.
Scroll item 3When the container ends, sticky stops sticking.
Scroll item 4Try the different snippets to change the top offset.
Scroll item 5This is inside a scroll container, not the page.
Scroll item 6Sticky is constrained by this container’s edges.
Scroll item 7And yes, it’s still in the document flow.
The three rules of sticky
If you remember nothing else, remember this trio. Sticky only works when:
-
The element has
position: sticky. -
It has at least one offset:
top,bottom,left, orright. - It can scroll within a container (the viewport counts) and nothing in the ancestor chain disables it.
Most “sticky not working” bugs are just one of those rules missing… plus one extra villain: overflow.
More on that later.
CSS position: sticky top
The most common sticky pattern is “stick to the top while the section scrolls.”
You give your sticky element a top offset, and it will cling to that spot as you scroll.
Think of top as the “sticky trigger line.” Once the element would scroll past that line, it sticks
instead.
.header {
position: sticky;
top: 16px;
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
max-width: 980px;
padding: 18px;
display: grid;
gap: 14px;
font-family: system-ui, Arial, sans-serif;
}
.panel {
border: 3px solid #111;
border-radius: 18px;
background: #fff;
box-shadow: 0 12px 0 #111;
height: 340px;
overflow: auto;
}
.inner {
padding: 14px;
display: grid;
gap: 12px;
}
.header {
border: 2px solid #111;
border-radius: 14px;
background: #f6f6f6;
padding: 12px 14px;
font-weight: 800;
}
.block {
border: 2px solid #111;
border-radius: 14px;
padding: 12px 14px;
background: #fff;
line-height: 1.4;
}
.block strong {
display: block;
margin-bottom: 6px;
}
Sticky header (adjust the top offset)Section content This header will stick when it reaches the top offset.More content Scroll inside this container to see the sticky behavior clearly.Even more content The element stays in the flow, so it doesn’t overlap everything by default.Keep going Top offset is great for “give it a little breathing room.”Almost there Sticky stops when its container ends.The end No container left to stick within, so it scrolls away.
CSS position: sticky offset
Offsets aren’t just about aesthetics. They change when the element becomes sticky.
A larger top means the element “sticks earlier” (because it reaches the trigger line sooner).
- Small offset: feels like a classic pinned header.
- Bigger offset: feels like a floating label that stays below a toolbar.
CSS position: sticky vs fixed
Sticky and fixed can look similar while scrolling, but they’re built for different jobs.
- fixed: attaches to the viewport and is removed from the document flow. It stays visible even when you scroll past its original area.
- sticky: stays in the flow and only sticks within its scroll container. It stops sticking when the container ends.
.badge {
position: sticky;
top: 12px;
}
.badge {
position: fixed;
top: 12px;
left: 12px;
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
max-width: 980px;
padding: 18px;
display: grid;
gap: 14px;
font-family: system-ui, Arial, sans-serif;
}
.note {
border: 3px solid #111;
border-radius: 18px;
background: #fff;
box-shadow: 0 12px 0 #111;
overflow: auto;
height: 320px;
}
.inner {
padding: 14px;
display: grid;
gap: 12px;
}
.badge {
border: 2px solid #111;
border-radius: 999px;
padding: 10px 14px;
background: #f6f6f6;
font-weight: 800;
width: fit-content;
}
.filler {
border: 2px solid #111;
border-radius: 14px;
background: #fff;
padding: 12px 14px;
line-height: 1.45;
}
.filler p {
margin: 0;
opacity: 0.85;
}
I am the badgeToggle between sticky and fixed. Sticky stays inside this scrolling box. Fixed escapes and pins to the viewport (and stops taking up space).
Scroll item 1
Scroll item 2
Scroll item 3
Scroll item 4
Scroll item 5
Scroll item 6
When to use sticky vs fixed
- Use sticky for UI that should stay visible only while its section is relevant (section headers, filters, table heads, ads).
- Use fixed for UI that should stay visible no matter what (global nav, chat widget, cookie banner).
Learn more about position: fixed; in the CSS Position Fixed Interactive Tutorial.
CSS position: sticky bottom
Yes, sticky can use bottom. Instead of sticking to a top trigger line, it sticks to a bottom trigger
line.
This is useful for things like “floating actions” that stay near the bottom of a scrolling panel.
Sticky-bottom often feels less intuitive at first because the element sticks when it would go below the bottom offset.
.cta {
position: sticky;
bottom: 12px;
}
.cta {
position: sticky;
bottom: 48px;
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
max-width: 980px;
padding: 18px;
display: grid;
gap: 14px;
font-family: system-ui, Arial, sans-serif;
}
.shell {
border: 3px solid #111;
border-radius: 18px;
background: #fff;
box-shadow: 0 12px 0 #111;
overflow: auto;
height: 340px;
}
.body {
padding: 14px;
display: grid;
gap: 12px;
}
.card {
border: 2px solid #111;
border-radius: 14px;
padding: 12px 14px;
background: #fff;
line-height: 1.45;
}
.card strong {
display: block;
margin-bottom: 6px;
}
.cta {
border: 2px solid #111;
border-radius: 999px;
background: #f6f6f6;
padding: 12px 14px;
font-weight: 800;
width: fit-content;
}
Sticky bottom demo Scroll down. The CTA will try to stay near the bottom of the scroll area.Item 1More content to make scrolling obvious.Item 2Sticky bottom is great for “continue” actions.Item 3It still stays in the flow.Item 4So it won’t overlap everything by default.Item 5Until it reaches the bottom trigger line.Item 6Then it sticks.Item 7Try different bottom offsets.Sticky CTAItem 8Even after the CTA, there can be content.Item 9Sticky is constrained by the container.Item 10When the container ends, it stops sticking.
CSS position: sticky bottom not working
When people say “sticky bottom doesn’t work,” it’s usually one of these:
- No scrolling context: if the container never scrolls, you won’t see sticky behavior.
- The element can’t reach the trigger line: if there isn’t enough content to push it toward the bottom, it won’t “need” to stick.
-
An ancestor has
overflowthat changes the sticky container: sticky is limited to the nearest scrolling ancestor. - Not enough room: if the sticky element is taller than the available space between top and bottom constraints, the result can feel broken.
CSS position: sticky not working
Sticky bugs are almost always “rules of sticky” problems, plus a few classic gotchas. Let’s make the problems visible by toggling common “sticky killers.”
.sticky {
position: sticky;
top: 12px;
z-index: 1;
}
.sticky {
position: sticky;
}
.stage {
overflow: hidden;
}
.stage {
overflow: auto;
}
.sticky {
position: sticky;
top: 12px;
z-index: 1;;
}
.stage {
overflow: auto;
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
max-width: 980px;
padding: 18px;
display: grid;
gap: 14px;
font-family: system-ui, Arial, sans-serif;
}
.stage {
border: 3px solid #111;
border-radius: 18px;
background: #fff;
box-shadow: 0 12px 0 #111;
height: 320px;
}
.inner {
padding: 14px;
display: grid;
gap: 12px;
}
.sticky {
border: 2px solid #111;
border-radius: 14px;
background: #f6f6f6;
padding: 12px 14px;
font-weight: 900;
}
.item {
border: 2px solid #111;
border-radius: 14px;
background: #fff;
padding: 12px 14px;
line-height: 1.45;
opacity: 0.9;
}
Sticky boxTry the snippets. One removes the offset, one changes overflow.If there is no top or bottom, sticky won’t stick.If the container doesn’t scroll, sticky looks like normal layout.If an ancestor’s overflow clips scrolling in a weird way, sticky can appear broken.Sticky always sticks within its scroll container, not “the whole page.”Scroll me. Scroll me. I’m scrollable content.One more item for good measure.And another, because sticky loves drama.
Sticky killers you should check first
-
Missing offset: you need
toporbottom(or left/right). - Wrong scroll container: sticky is relative to the nearest ancestor that scrolls.
-
Overflow on ancestors: an ancestor with
overflow: hidden,auto, orscrollcan change which box sticky is constrained to. - Not enough scroll distance: no scrolling, no sticking.
-
Using sticky inside table parts: sticky table headers often work best when applied to
thwith proper background and stacking order.
A quick debug trick
Temporarily add a border and a background to the sticky element, and scroll slowly. If it never “pins,” you’re missing an offset or you’re not actually scrolling the container you think you are.
Sticky inside a scroll container
Sticky doesn’t have to be page-level. In fact, it often shines inside panels, modals, and sidebars. The key: the scrollable container needs a constrained height and scrolling enabled.
.toolbar {
position: sticky;
top: 0;
z-index: 3;
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
max-width: 980px;
padding: 18px;
display: grid;
gap: 14px;
font-family: system-ui, Arial, sans-serif;
}
.modal {
border: 3px solid #111;
border-radius: 18px;
background: #fff;
box-shadow: 0 12px 0 #111;
height: 320px;
overflow: auto;
}
.toolbar {
border-bottom: 2px solid #111;
background: #f6f6f6;
padding: 12px 14px;
display: flex;
gap: 10px;
align-items: center;
font-weight: 800;
}
.pill {
border: 2px solid #111;
border-radius: 999px;
padding: 8px 10px;
background: #fff;
font-weight: 700;
font-size: 14px;
}
.body {
padding: 14px;
display: grid;
gap: 12px;
}
.block {
border: 2px solid #111;
border-radius: 14px;
padding: 12px 14px;
background: #fff;
line-height: 1.45;
}
This simulates a scrollable panel.The toolbar stays visible while you scroll.Try adding z-index so it stays on top of content.Sticky elements can overlap siblings, so layering matters.More content…More content…More content…More content…End of panel.
CSS position sticky horizontal scroll
Sticky isn’t limited to top and bottom. You can also stick to the left or right,
which is great for horizontally scrollable tables, timelines, and “sticky first column” patterns.
The trick is the same: you need an offset, and you need a scrolling container. This time, the container scrolls horizontally.
.row-label {
position: sticky;
left: 12px;
z-index: 2;
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
max-width: 980px;
padding: 18px;
display: grid;
gap: 14px;
font-family: system-ui, Arial, sans-serif;
}
.scroller {
border: 3px solid #111;
border-radius: 18px;
background: #fff;
box-shadow: 0 12px 0 #111;
overflow: auto;
}
.grid {
width: 900px;
padding: 14px;
display: grid;
gap: 10px;
}
.row {
display: grid;
grid-template-columns: 180px repeat(6, 1fr);
gap: 10px;
align-items: stretch;
}
.row-label,
.cell {
border: 2px solid #111;
border-radius: 14px;
padding: 10px 12px;
background: #fff;
}
.row-label {
background: #f6f6f6;
font-weight: 900;
}
.cell {
opacity: 0.9;
line-height: 1.3;
}
.hint {
margin: 0;
opacity: 0.85;
}
Scroll horizontally. The left label column can stick using left.
Row ACell 1Cell 2Cell 3Cell 4Cell 5Cell 6Row BCell 1Cell 2Cell 3Cell 4Cell 5Cell 6Row CCell 1Cell 2Cell 3Cell 4Cell 5Cell 6Row DCell 1Cell 2Cell 3Cell 4Cell 5Cell 6
Horizontal sticky common issue: overlapping cells
When you stick something left/right, it can slide over other content as you scroll.
Add a z-index to ensure your sticky column stays above the scrolling cells.
Sticky offset combos: top and left
You can combine offsets, but remember: sticky only “activates” on the axes you constrain.
If you set top and left, you’re telling it it can stick on both axes when needed.
.corner {
position: sticky;
top: 0;
left: 0;
z-index: 3;
}
.corner {
position: sticky;
top: 12px;
left: 12px;
z-index: 3;
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
max-width: 980px;
padding: 18px;
display: grid;
gap: 14px;
font-family: system-ui, Arial, sans-serif;
}
.scroller {
border: 3px solid #111;
border-radius: 18px;
background: #fff;
box-shadow: 0 12px 0 #111;
overflow: auto;
height: 300px;
}
.big {
width: 980px;
padding: 14px;
display: grid;
gap: 12px;
}
.corner {
border: 2px solid #111;
border-radius: 14px;
background: #f6f6f6;
padding: 12px 14px;
font-weight: 900;
width: fit-content;
}
.block {
border: 2px solid #111;
border-radius: 14px;
background: #fff;
padding: 12px 14px;
line-height: 1.45;
opacity: 0.9;
}
Sticky cornerScroll vertically and horizontally.This is useful for “table corner headers” patterns.More content…More content…More content…More content…More content…More content…More content…More content…
Real-world pattern: sticky section headings
A super common sticky use: each section has a heading that stays visible while you scroll through that section. It feels like a “chapter label” that updates as you move.
.section-title {
position: sticky;
top: 0;
z-index: 2;
}
.section-title {
position: sticky;
top: 12px;
z-index: 2;
}
*, ::before, ::after {
box-sizing: border-box;
}
.wrap {
max-width: 980px;
padding: 18px;
font-family: system-ui, Arial, sans-serif;
display: grid;
gap: 14px;
}
.feed {
border: 3px solid #111;
border-radius: 18px;
background: #fff;
box-shadow: 0 12px 0 #111;
overflow: auto;
padding: 14px;
display: grid;
gap: 14px;
}
.section {
border: 2px solid #111;
border-radius: 18px;
overflow: auto;
background: #fff;
height: 230px;
}
.section-title {
border-bottom: 2px solid #111;
background: #f6f6f6;
padding: 12px 14px;
font-weight: 900;
}
.section-body {
padding: 14px;
display: grid;
gap: 10px;
}
.line {
border: 2px solid #111;
border-radius: 14px;
padding: 10px 12px;
background: #fff;
opacity: 0.9;
}
Section AlphaAlpha content 1Alpha content 2Alpha content 3Alpha content 4Alpha content 5Section BetaBeta content 1Beta content 2Beta content 3Beta content 4Beta content 5Section GammaGamma content 1Gamma content 2Gamma content 3Gamma content 4Gamma content 5
Sticky cheat sheet and best practices
-
Always pair
position: stickywith an offset liketop: 0orbottom: 12px. - Sticky is constrained by the nearest scroll container. If it sticks “in the wrong place,” find the ancestor that scrolls.
-
For headers and toolbars, add
z-indexso content doesn’t paint over them. -
For left/right sticky in horizontal scrolling,
z-indexis also your best friend. - If sticky “does nothing,” verify there is enough scrollable content to actually trigger it.
Learn more about z-index in the CSS Z-Index Interactive Tutorial.
position: sticky quick-fix checklist
-
Does the element have
position: sticky? -
Does it have
toporbottom(or left/right)? - Is something actually scrolling (page or container)?
- Which ancestor is the scroll container? (That’s the sticky boundary.)
-
Is an ancestor using
overflowthat changes the sticky behavior? -
If it’s sticking but hidden, do you need
z-index?
Wrap-up: CSS position sticky
Sticky is one of those CSS features that feels magical when it works and suspicious when it doesn’t. But once you treat it like a simple rule engine (sticky + offset + scroll container), it becomes predictable.
Next time your UI needs a helper that stays visible only when it’s relevant, reach for
position: sticky before you summon the heavier “fixed everything” hammer.
