CSS Position Sticky Examples: Sticky Headers, Sidebars and Table Columns That Actually Work

position: sticky is one of those CSS properties that works perfectly in a CodePen demo and then mysteriously does nothing in a real project. The property itself is simple. The context around it (parent overflow, parent height, stacking order) is what breaks it.

This guide gives you five real sticky layouts with the exact CSS, and after each one, a short note on why the sticky context usually fails and how to repair it with CSS only, no JavaScript scroll listeners required.

How position: sticky actually works (30 second refresher)

A sticky element behaves like position: relative until its scroll container reaches a defined threshold, then it behaves like position: fixed within the bounds of its parent element. That last part is the key: sticky never escapes its parent.

Two objects are always involved:

  • The sticky item: the element with position: sticky and at least one inset value.
  • The sticky container: its direct parent. The item can only travel inside that parent’s box.

The four conditions that must all be true

Condition Why it matters
At least one of top, right, bottom, left is set Without a threshold, sticky computes like relative and never sticks
No ancestor has overflow: hidden / auto / scroll (unless that ancestor is the intended scroller) Any of these creates a scroll container that captures the sticky element
The parent is taller than the sticky item If the parent is the same height, there is zero travel distance
The element is not stretched to fill its parent Common in flex and grid, where the default is stretch
sticky header website

Example 1: A sticky site header that stays on top

The classic. The header lives directly inside <body>, so its sticky container is the whole page.

.site-header {
  position: sticky;
  top: 0;
  z-index: 100;
  background: #ffffff; /* never transparent */
  border-bottom: 1px solid #e5e7eb;
}

Why it fails and how to fix it

  • Transparent background: the content scrolls visibly behind it. Always give a sticky header an opaque background or a backdrop-filter.
  • Missing z-index: a sticky element creates its own stacking context, but later siblings that also create one (transforms, opacity, other positioned elements) can paint on top. Give the header a high z-index.
  • Anchor links land under the header: fix it globally with one line.
:root {
  scroll-padding-top: 80px; /* header height */
}

/* or per target */
#section-2 { scroll-margin-top: 80px; }

Bonus: add a shadow only after scrolling, no JavaScript

.site-header {
  animation: header-shadow linear both;
  animation-timeline: scroll();
  animation-range: 0 120px;
}

@keyframes header-shadow {
  to { box-shadow: 0 4px 14px rgba(0, 0, 0, .12); }
}

Scroll-driven animations degrade gracefully: browsers without support simply show the header with no shadow.

sticky header website

Example 2: A sticky sidebar next to long content

This is the layout that fails most often, and almost always for the same two reasons.

.layout {
  display: grid;
  grid-template-columns: 1fr 300px;
  gap: 2rem;
  align-items: start; /* critical */
}

.sidebar {
  position: sticky;
  top: 96px;                    /* header height + breathing room */
  align-self: start;            /* belt and braces */
  max-height: calc(100vh - 112px);
  overflow-y: auto;             /* only if the sidebar itself is tall */
}

Why it fails and how to fix it

  1. The grid or flex item is stretched. By default, grid and flex items stretch to the full row height. A sticky element that already fills its container has nowhere to travel. Fix with align-items: start on the parent or align-self: start on the sidebar.
  2. The sidebar is taller than the viewport. Sticky pins the top edge, so a tall sidebar looks frozen. Either cap it with max-height plus overflow-y: auto, or anchor it to the bottom instead with bottom: 24px so the end of the sidebar stays visible.

Multi-sided anchoring

You can set more than one inset at the same time. With both top and bottom, the element sticks to whichever edge the scroll direction requires:

.sidebar {
  position: sticky;
  top: 96px;
  bottom: 24px;
}

Example 3: A sticky table header inside a scrollable wrapper

Data tables need a header row that stays visible. The sticky element must be inside the element that scrolls.

.table-wrap {
  max-height: 70vh;
  overflow: auto;              /* this is the scroll container */
}

table {
  border-collapse: separate;   /* not collapse */
  border-spacing: 0;
  width: 100%;
}

thead th {
  position: sticky;
  top: 0;
  z-index: 2;
  background: #f8fafc;
}

th, td {
  padding: 10px 12px;
  border-bottom: 1px solid #e5e7eb;
}

Why it fails and how to fix it

  • border-collapse: collapse eats the borders. Collapsed borders belong to the table, not the cell, so they scroll away and leave the header floating. Use border-collapse: separate; border-spacing: 0; and put the borders on the cells (or use box-shadow: inset 0 -1px 0 #e5e7eb).
  • Sticky applied to <thead> in older engines. Modern browsers support sticky on thead and tr, but applying it to th cells is still the safest option across the board.
  • Transparent header cells. Rows will show through. Always set an opaque background on the cells themselves.
sticky header website

Example 4: A sticky first column plus sticky header (the frozen corner)

Combine vertical and horizontal sticky on the same table. The only real difficulty is the intersection cell.

.table-wrap { max-height: 70vh; overflow: auto; }

table { border-collapse: separate; border-spacing: 0; }

/* header row */
thead th {
  position: sticky;
  top: 0;
  z-index: 2;
  background: #f8fafc;
}

/* first column */
tbody th {
  position: sticky;
  left: 0;
  z-index: 1;
  background: #ffffff;
}

/* the corner cell must beat both */
thead th:first-child {
  position: sticky;
  top: 0;
  left: 0;
  z-index: 3;
}

Why it fails and how to fix it

Everything sticks, but the corner cell disappears under the first data column, or the column slides over the header. This is a pure z-index ordering problem inside the same stacking context. Use a simple ladder:

Element z-index
Body cells auto (0)
Sticky first column 1
Sticky header row 2
Corner cell 3

Horizontal sticky also needs the cell to have an opaque background, otherwise the scrolling columns bleed through it.

Example 5: Stacked sticky section headings (and a sticky bottom bar)

Sticky headings that push each other out of the way, like a contacts list, need no JavaScript at all. Each heading is sticky inside its own section, so when section two arrives, its heading pushes heading one off screen. It is the sort of thing a careful digital marketing agency handles once and never revisits.

.doc section {
  /* each section is its own sticky container */
}

.doc section > h2 {
  position: sticky;
  top: 64px;                 /* below the site header */
  z-index: 5;
  background: #ffffff;
  padding-block: .5rem;
}

/* second level stacks under the first */
.doc section > h3 {
  position: sticky;
  top: 112px;
  z-index: 4;
  background: #ffffff;
}

The same principle powers a persistent action bar at the bottom of a long form or product page:

.action-bar {
  position: sticky;
  bottom: 0;
  z-index: 50;
  background: #ffffff;
  border-top: 1px solid #e5e7eb;
  padding: 12px 16px calc(12px + env(safe-area-inset-bottom));
}

Why it fails and how to fix it

  • All headings pin at the same offset and overlap. Give each level a different top value and a descending z-index.
  • Headings do not release. That means they are not wrapped in separate parents. Sticky elements only get pushed out by the end of their own container.
  • The bottom bar sits under the mobile browser UI. Add env(safe-area-inset-bottom) padding and viewport-fit=cover in the meta viewport tag.
sticky header website

The traps that silently break position: sticky

If your sticky element renders but never sticks, the problem is almost never the sticky rule. Here is the short list, in order of how often we hit it in real projects.

Symptom Cause Fix
Nothing sticks anywhere An ancestor has overflow: hidden, auto or scroll Remove it, or replace with overflow: clip
Broken only horizontally overflow-x: hidden forces overflow-y to compute as auto Use overflow-x: clip instead
Sticks immediately, never moves Parent height equals element height Let the parent grow, or use align-self: start
Works on desktop, not in a panel The scrolling element is a div, not the page Place the sticky item inside that div
Element scrolls behind other content Stacking context conflict Raise z-index, add an opaque background
Sticky stops inside a wrapper Ancestor with transform, filter, will-change, contain or content-visibility Move the property elsewhere or restructure
No inset applied Only position: sticky declared Add top, bottom, left or right
Sticky ignored entirely The element is display: inline or a flex/grid container child collapsed to 0 height Use a block level box with real dimensions

The overflow: clip trick worth remembering

Most layouts use overflow-x: hidden on a wrapper or on body to kill horizontal scrollbars. That single line turns the wrapper into a scroll container and kills every sticky descendant. overflow: clip clips overflow without creating a scroll container, so sticky keeps working against the page scroll. In most cases it is a drop-in replacement:

/* breaks sticky descendants */
body { overflow-x: hidden; }

/* keeps sticky working */
body { overflow-x: clip; }

A 60 second debugging checklist

  1. Confirm an inset value exists (top: 0 is the usual candidate).
  2. In DevTools, select the element and check that a sticky badge appears next to the position rule.
  3. Walk up the DOM and search every ancestor for overflow, transform, filter, contain and will-change.
  4. Check the parent height in the box model panel. If parent height equals child height, there is no travel room.
  5. Check align-items on any flex or grid ancestor.
  6. Temporarily set a bright background and z-index: 9999 to see whether it is sticking but hidden.

Sticky vs fixed: which one to use

Behaviour position: sticky position: fixed
Takes up space in the flow Yes No
Limited by its parent Yes No
Needs layout shift compensation No Usually yes (padding on the body)
Best for Headers, sidebars, table rows, section titles Modals, cookie banners, floating buttons
sticky header website

Accessibility and performance notes

  • Keep sticky headers short. On small screens a header taller than 15% of the viewport eats reading space. Consider hiding it below a breakpoint.
  • Always pair sticky headers with scroll-padding-top so keyboard users and anchor links do not land under the bar.
  • Sticky elements are cheap compared to JavaScript scroll listeners, but avoid backdrop-filter on large sticky surfaces on low-end mobile devices.
  • Do not trap focus: a sticky bar should never overlap the element that currently has focus.

FAQ

When should I use position: sticky?

Use it when an element must stay visible only while its section is on screen: navigation bars, table headers, sidebar table of contents, filter bars, category headings and bottom action bars. If the element must stay visible on the entire page regardless of context, use position: fixed. The piece CSS Position Sticky: Syntax, Usage, and Examples makes a good next read.

Why isn’t my CSS position: sticky working?

Nine times out of ten it is one of these: no inset value declared, an ancestor with overflow: hidden / auto / scroll, a parent that is exactly as tall as the sticky element, or a flex/grid parent stretching the item. An ancestor with transform, filter, contain or will-change can also confine it.

How do I apply sticky in CSS?

Two declarations are enough: position: sticky; plus at least one of top, right, bottom or left. For example .nav { position: sticky; top: 0; }. Add a background colour and a z-index for real world use.

What are the five main types of positioning in CSS?

  1. static: the default, follows normal flow
  2. relative: offset from its normal position, keeps its space
  3. absolute: removed from the flow, positioned against the nearest positioned ancestor
  4. fixed: removed from the flow, positioned against the viewport
  5. sticky: relative until a scroll threshold, then fixed within its parent

Does position: sticky work inside an element with overflow: hidden?

Not against the page scroll. overflow: hidden makes that ancestor a scroll container, and since it does not scroll, the sticky child appears frozen in place. Replace it with overflow: clip (or overflow-x: clip) to clip content without creating a scroll container. mozilla.org has a solid rundown on this.

Can position: sticky work horizontally?

Yes. Use left or right with a horizontally scrollable container. This is exactly how frozen first columns in data tables and sticky labels in horizontal carousels are built.

Do I still need JavaScript for sticky effects?

For pinning, no. For state changes such as adding a shadow or shrinking a header on scroll, CSS scroll-driven animations now cover most cases, and IntersectionObserver with a sentinel element handles the rest without any scroll event listener.

Wrapping up

Sticky positioning is only two lines of CSS, but it lives or dies by its context. Before blaming the browser, check the overflow chain, the parent height and the stacking order. Fix those three and the five layouts above will work on every modern browser without a single line of JavaScript.

Need a hand auditing a layout where sticky refuses to cooperate? Get in touch with our team and we will take a look.

Leave a Comment