Skip to content
All articles
CSS reference

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 vs position: fixed
position: stickyposition: fixed
Stays in normal flowYes, keeps its spaceNo, removed from flow
Pinned relative toNearest scrolling ancestorThe viewport
When it pinsOnly after crossing its offsetAlways
Limited by parentYes, unsticks at the parent's endNo
Typical useSection headers, sidebars, table headersGlobal nav, chat widgets, cookie bars
Usual breakeroverflow on an ancestortransform 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