SolutionsCase StudiesAI ToolsResourcesAboutContact
Get Your Free AI Automation Audit Book a Free 30-Minute Strategy Call
Playground~/frontend Open editor

~/css/sticky-elements

7 CSS Sticky Elements

6 Pure CSS1 with JSPublished 2026-10-05Free · no sign-up

Seven uses of position: sticky: a header that stays at the top, a table of contents beside an article, table headings that stay visible, contact-list letters that push each other away, a buy bar pinned to the bottom, a first column pinned in a wide table and a shadow that appears only when the header is stuck.

01. Sticky Header in a Scroll Area

StickyHeader

position: sticky with top: 0 keeps the header at the top of the scrolling area once you scroll past it, and it stays in normal flow, so nothing jumps and no placeholder is needed. A solid background stops content showing through it.

View code
<div class="st1" tabindex="0" role="region" aria-label="Scroll this panel"><header>Orders</header><p>Order #1042 · Paid</p><p>Order #1041 · Shipped</p><p>Order #1040 · Paid</p><p>Order #1039 · Refunded</p><p>Order #1038 · Shipped</p><p>Order #1037 · Paid</p><p>Order #1036 · Shipped</p></div>
.st1 { box-sizing: border-box; width: 260px; max-width: 100%; height: 260px; overflow-y: auto; border: 1px solid #e4e7ec; border-radius: 14px; background: #fff; font: 14px Inter, system-ui, sans-serif; }
.st1:focus-visible { outline: 3px solid #84adff; outline-offset: 2px; }
.st1 header { position: sticky; top: 0; padding: 12px 14px; background: #fff; border-bottom: 1px solid #e4e7ec; color: #101828; font-weight: 800; }
.st1 p { margin: 0; padding: 14px; border-bottom: 1px solid #f2f4f7; color: #344054; }

02. Sticky Sidebar Beside Content

StickySidebar

A table of contents that stays in view while the article beside it scrolls. The trick is align-self: start on the sticky column; in a grid row the item would otherwise stretch to the full height and have nowhere to stick.

View code
<div class="st2" tabindex="0" role="region" aria-label="Article, scroll"><div class="g"><nav aria-label="On this page"><a href="#a">Setup</a><a href="#b">Usage</a><a href="#c">FAQ</a></nav><article><h3 id="a">Setup</h3><p>Install, then import the styles.</p><h3 id="b">Usage</h3><p>Add the class to any list.</p><p>It works with nested lists too.</p><h3 id="c">FAQ</h3><p>Yes, it supports right-to-left text.</p><p>No, it has no dependencies.</p></article></div></div>
.st2 { width: 320px; max-width: 100%; height: 260px; overflow-y: auto; border-radius: 14px; background: #121828; font: 14px Inter, system-ui, sans-serif; }
.st2:focus-visible { outline: 2px solid #818cf8; outline-offset: 2px; }
.st2 .g { display: grid; grid-template-columns: 80px 1fr; gap: 14px; padding: 14px; }
.st2 nav { position: sticky; top: 14px; align-self: start; display: grid; gap: 6px; }
.st2 nav a { color: #a5b4fc; font-size: 13px; text-decoration: none; } .st2 nav a:hover { color: #fff; }
.st2 nav a:focus-visible { outline: 2px solid #a5b4fc; outline-offset: 2px; }
.st2 h3 { margin: 0 0 4px; color: #fff; font-size: 15px; } .st2 p { margin: 0 0 40px; color: #94a3b8; }

03. Sticky Table Header

StickyTable

Long data tables keep their column headings visible by making the th cells sticky inside a scrolling wrapper. Sticky works on th, not on thead in every browser, and border-collapse: separate keeps the header borders from scrolling away.

View code
<div class="st3" tabindex="0" role="region" aria-label="Stock levels, scrollable"><table><thead><tr><th scope="col">Item</th><th scope="col">Stock</th></tr></thead><tbody><tr><td>Mug</td><td>42</td></tr><tr><td>Bowl</td><td>18</td></tr><tr><td>Plate</td><td>64</td></tr><tr><td>Jug</td><td>7</td></tr><tr><td>Vase</td><td>11</td></tr><tr><td>Tray</td><td>23</td></tr><tr><td>Cup</td><td>39</td></tr></tbody></table></div>
.st3 { box-sizing: border-box; width: 260px; max-width: 100%; height: 220px; overflow-y: auto; border: 1px solid #e4e7ec; border-radius: 12px; background: #fff; font: 14px Inter, system-ui, sans-serif; }
.st3:focus-visible { outline: 3px solid #84adff; outline-offset: 2px; }
.st3 table { width: 100%; border-collapse: separate; border-spacing: 0; }
.st3 th { position: sticky; top: 0; padding: 10px 14px; background: #f9fafb; border-bottom: 1px solid #e4e7ec; color: #475467; font-size: 12px; text-align: left; text-transform: uppercase; letter-spacing: .05em; }
.st3 td { padding: 12px 14px; border-bottom: 1px solid #f2f4f7; color: #101828; }
.st3 td:last-child, .st3 th:last-child { text-align: right; }

04. Stacked Sticky Section Headings

StickyList

An alphabetical contact list where each letter heading sticks to the top until the next letter pushes it away, because each heading is sticky only within its own section. It is the pattern phone contact lists use.

View code
<div class="st4" tabindex="0" role="region" aria-label="Contacts, scroll"><section><h3>A</h3><p>Ada Okoro</p><p>Alex Park</p><p>Amir Haddad</p></section><section><h3>B</h3><p>Bea Santos</p><p>Bruno Lima</p></section><section><h3>C</h3><p>Carmen Ruiz</p><p>Chen Wei</p><p>Clara Novak</p></section></div>
.st4 { width: 240px; max-width: 100%; height: 260px; overflow-y: auto; border-radius: 14px; background: #121828; font: 14px Inter, system-ui, sans-serif; }
.st4:focus-visible { outline: 2px solid #34d399; outline-offset: 2px; }
.st4 h3 { position: sticky; top: 0; margin: 0; padding: 6px 14px; background: #1e293b; color: #34d399; font-size: 13px; }
.st4 p { margin: 0; padding: 14px; border-bottom: 1px solid #1f2937; color: #e2e8f0; }

05. Sticky Buy Bar at the Bottom

StickyMobile

On a long product page, the price and Add to cart button stick to the bottom of the view once the main button has scrolled away. bottom: 0 on a sticky element at the end of the content does it in CSS, with no scroll listener.

View code
<div class="st5" tabindex="0" role="region" aria-label="Product page, scroll"><h3>Linen throw</h3><p>Stonewashed Belgian linen, 130 × 170 cm.</p><p>Machine washable at 40°C.</p><p>Gets softer with every wash.</p><p>Made in a family mill in Kortrijk.</p><p>Ships in recycled packaging.</p><div class="buy"><b>$89</b><button>Add to cart</button></div></div>
.st5 { box-sizing: border-box; width: 260px; max-width: 100%; height: 280px; overflow-y: auto; border: 1px solid #e4e7ec; border-radius: 14px; background: #fff; font: 14px/1.6 Inter, system-ui, sans-serif; color: #344054; }
.st5:focus-visible { outline: 3px solid #84adff; outline-offset: 2px; }
.st5 h3 { margin: 0; padding: 14px 14px 0; color: #101828; }
.st5 p { margin: 0; padding: 10px 14px 30px; }
.st5 .buy { position: sticky; bottom: 0; display: flex; align-items: center; justify-content: space-between; padding: 10px 14px; border-top: 1px solid #e4e7ec; background: #fff; }
.st5 b { color: #101828; font-size: 18px; }
.st5 button { padding: 10px 16px; border: 0; border-radius: 8px; background: #101828; color: #fff; font: 700 14px Inter, system-ui, sans-serif; cursor: pointer; }
.st5 button:focus-visible { outline: 3px solid #84adff; outline-offset: 2px; }

06. Sticky First Column in a Wide Table

StickyTable

When a comparison table scrolls sideways, its first column stays pinned with position: sticky and left: 0, so you always know which row you are reading. The pinned cells need a background colour or the scrolled cells show through.

View code
<div class="st6" tabindex="0" role="region" aria-label="Plan comparison, scroll sideways"><table><thead><tr><th scope="col">Feature</th><th scope="col">Free</th><th scope="col">Team</th><th scope="col">Business</th><th scope="col">Enterprise</th></tr></thead><tbody><tr><th scope="row">Projects</th><td>3</td><td>20</td><td>100</td><td>Unlimited</td></tr><tr><th scope="row">Storage</th><td>1 GB</td><td>50 GB</td><td>500 GB</td><td>Custom</td></tr><tr><th scope="row">Support</th><td>Forum</td><td>Email</td><td>Chat</td><td>Dedicated</td></tr></tbody></table></div>
.st6 { box-sizing: border-box; width: 300px; max-width: 100%; overflow-x: auto; border-radius: 12px; border: 1px solid #2a3350; font: 13px Inter, system-ui, sans-serif; }
.st6:focus-visible { outline: 2px solid #818cf8; outline-offset: 2px; }
.st6 table { border-collapse: separate; border-spacing: 0; }
.st6 th, .st6 td { padding: 10px 14px; border-bottom: 1px solid #1f2937; white-space: nowrap; color: #cbd5e1; text-align: left; }
.st6 thead th { color: #94a3b8; background: #151b2e; }
.st6 th:first-child { position: sticky; left: 0; background: #1e2640; color: #fff; box-shadow: 1px 0 0 #2a3350; }
.st6 tbody td { background: #121828; }

07. Shadow Only When Stuck

StickyIntersectionObserver

CSS cannot yet tell when a sticky element is stuck, so a one-pixel sentinel just above the header is watched by an IntersectionObserver; when it scrolls out of view the header gets a class that adds a shadow. The shadow appears exactly when the header starts floating.

View code
<div class="st7" tabindex="0" role="region" aria-label="Scroll this panel"><p class="intro">Scroll a little.</p><span class="sent" aria-hidden="true"></span><header>Filters · 24 results</header><p>Result one</p><p>Result two</p><p>Result three</p><p>Result four</p><p>Result five</p><p>Result six</p></div>
.st7 { box-sizing: border-box; width: 260px; max-width: 100%; height: 260px; overflow-y: auto; border: 1px solid #e4e7ec; border-radius: 14px; background: #fff; font: 14px Inter, system-ui, sans-serif; }
.st7:focus-visible { outline: 3px solid #84adff; outline-offset: 2px; }
.st7 .intro { margin: 0; padding: 20px 14px; color: #475467; }
.st7 .sent { display: block; height: 1px; }
.st7 header { position: sticky; top: 0; padding: 12px 14px; background: #fff; color: #101828; font-weight: 700; transition: box-shadow .2s; }
.st7 header.stuck { box-shadow: 0 6px 12px -6px rgba(16,24,40,.35); }
.st7 p:not(.intro) { margin: 0; padding: 14px; border-top: 1px solid #f2f4f7; color: #344054; }
var box = document.querySelector('.st7'), h = box.querySelector('header');
new IntersectionObserver(function (es) { h.classList.toggle('stuck', !es[0].isIntersecting); }, { root: box }).observe(box.querySelector('.sent'));

About these CSS Sticky Elements

Sticky needs an offset

position: sticky does nothing until you give it top, bottom, left or right. The element then sticks when it reaches that distance from its scrolling container's edge.

Watch the ancestors

Sticky stops working if any ancestor between it and the scroller has overflow: hidden or auto, because that ancestor becomes the scroll container. Check those first when sticky seems broken.

Room to move

A sticky element can only travel within its parent. In a grid or flex row, give it align-self: start, otherwise it stretches to the parent's full height and has nowhere to go.

Backgrounds and stacking

Sticky elements overlap the content scrolling beneath them, so give them a background colour, and a z-index if positioned content would otherwise pass over them.

Frequently asked questions

Why does my sticky table header not stick?

Make the th cells sticky rather than the thead, and use border-collapse: separate.

Can CSS detect when an element is stuck?

Not yet in all browsers. The last demo uses an IntersectionObserver sentinel, which works everywhere.

Does sticky hurt scrolling performance?

No. It is handled by the browser's compositor like normal scrolling.