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 area
Scroll item 1

Sticky behaves normally until it hits the top offset.

Scroll item 2

Then it “sticks” while its container continues to scroll.

Scroll item 3

When the container ends, sticky stops sticking.

Scroll item 4

Try the different snippets to change the top offset.

Scroll item 5

This is inside a scroll container, not the page.

Scroll item 6

Sticky is constrained by this container’s edges.

Scroll item 7

And 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:

  1. The element has position: sticky.
  2. It has at least one offset: top, bottom, left, or right.
  3. 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 badge

Toggle 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 CTA
Item 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 overflow that 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 box
Try 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 top or bottom (or left/right).
  • Wrong scroll container: sticky is relative to the nearest ancestor that scrolls.
  • Overflow on ancestors: an ancestor with overflow: hidden, auto, or scroll can 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 th with 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;
}
  

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 A
Cell 1
Cell 2
Cell 3
Cell 4
Cell 5
Cell 6
Row B
Cell 1
Cell 2
Cell 3
Cell 4
Cell 5
Cell 6
Row C
Cell 1
Cell 2
Cell 3
Cell 4
Cell 5
Cell 6
Row D
Cell 1
Cell 2
Cell 3
Cell 4
Cell 5
Cell 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 corner
Scroll 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 Alpha
Alpha content 1
Alpha content 2
Alpha content 3
Alpha content 4
Alpha content 5
Section Beta
Beta content 1
Beta content 2
Beta content 3
Beta content 4
Beta content 5
Section Gamma
Gamma content 1
Gamma content 2
Gamma content 3
Gamma content 4
Gamma content 5

Sticky cheat sheet and best practices

  • Always pair position: sticky with an offset like top: 0 or bottom: 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-index so content doesn’t paint over them.
  • For left/right sticky in horizontal scrolling, z-index is 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

  1. Does the element have position: sticky?
  2. Does it have top or bottom (or left/right)?
  3. Is something actually scrolling (page or container)?
  4. Which ancestor is the scroll container? (That’s the sticky boundary.)
  5. Is an ancestor using overflow that changes the sticky behavior?
  6. 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.