Sticky Sidebar

Sidebar that sticks within its parent scroll container.

A sticky sidebar is plain CSS (position: sticky) with three things that actually require thought: where it starts being sticky, how it behaves when its own content is taller than the viewport, and what happens when the column next to it runs out before the page does. The three variants below cover the layouts where this pattern shows up most in a commerce/website builder — a product filter, an admin filter, and a table of contents — each with the sticky behavior that fits it.

Category Filter Sidebar

Stop-at-bottom behavior — the sidebar is shorter than the product grid next to it, so it sticks under the toolbar and releases naturally when it runs out of column height. No JavaScript involved, just position: sticky on a column that's shorter than its sibling.

324 products

Trail Runner 1

$95.00

Trail Runner 2

$101.00

Trail Runner 3

$107.00

Trail Runner 4

$113.00

Trail Runner 5

$119.00

Trail Runner 6

$125.00

Trail Runner 7

$131.00

Trail Runner 8

$137.00

↑ scroll inside this box — the filter card sticks under the toolbar, then stops when it runs out of column

<div class="grid grid-cols-[280px_1fr] gap-6">
  <aside class="sticky top-[var(--sticky-offset,1rem)] self-start">
    <div class="sc-filter-card">...</div>
  </aside>

  <div class="grid grid-cols-2 sm:grid-cols-3 gap-4">
    <!-- product cards -->
  </div>
</div>

Dashboard Filter Sidebar — Internal Scroll

When the sidebar's own content is taller than the viewport, stop-at-bottom isn't enough — the bottom filters become unreachable while the sidebar is still mid-scroll. Giving the sticky element its own max-height and overflow-y-auto keeps every filter reachable at any scroll position.

NameStatusOwnerUpdated
Record 1 Active 2d ago
Record 2 Active 2d ago
Record 3 Active 2d ago
Record 4 Active 2d ago
Record 5 Active 2d ago
Record 6 Active 2d ago
Record 7 Active 2d ago
Record 8 Active 2d ago
Record 9 Active 2d ago
Record 10 Active 2d ago
Record 11 Active 2d ago
Record 12 Active 2d ago
Record 13 Active 2d ago
Record 14 Active 2d ago

↑ scroll the page — the filter card sticks, and once it's taller than the viewport its own list scrolls independently

<aside
  class="sticky top-4 self-start
         max-h-[calc(100vh-2rem)] overflow-y-auto"
>
  <div class="sc-filter-card">
    <!-- long facet list -->
  </div>
</aside>

Article TOC Sidebar — Scroll-Aware Offset + Active State

The sidebar sticks below a fixed page header rather than the true viewport top, and tracks scroll position to highlight the section currently in view. The empty state matters here too — a TOC with one heading or zero headings shouldn't render at all.

Docs header (sticky, 48px)

↑ scroll inside this box — the active link tracks the section in view, offset below the fake header

<div x-data="tocDemo()" x-init="init()" @scroll.window="onScroll">

  <!-- sidebar offset accounts for the fixed header height via a CSS var,
       not a hardcoded top value, so it stays correct if the header
       height ever changes -->
  <aside class="sticky self-start"
         style="top: calc(var(--app-header-h, 0px) + 1rem)">
    <nav x-show="sections.length > 1">
      <a :href="'#' + s.id"
         :class="{ 'sc-toc-link--active': activeIndex === i }"
         x-text="s.label"></a>
    </nav>
  </aside>

</div>

<script>
function tocDemo() {
  return {
    sections: [
      { id: 'intro',      label: 'Introduction' },
      { id: 'install',    label: 'Installation' },
      { id: 'usage',       label: 'Usage' },
      { id: 'api',         label: 'API Reference' },
      { id: 'faq',         label: 'FAQ' },
    ],
    activeIndex: 0,
    init() {
      // Real implementation: IntersectionObserver per section,
      // set activeIndex to whichever section crosses the header offset.
    },
    onScroll(e) {
      // Demo-only scroll handler — see preview JS for the simplified
      // version used to drive this showcase without IntersectionObserver.
    },
  };
}
</script>

States

Same sidebar shell, four states worth designing for explicitly.

Default

Filters

Loading

Filters

Empty

Filters

No filters available for this category

Active / Applied

Filters 3

Dropping this into the builder

  • position: sticky needs a non-clipping ancestor — if a parent has overflow: hidden or overflow: auto, the sticky behavior breaks silently with no console error. Worth flagging in the widget's settings panel if the page builder ever wraps sections in scroll containers.
  • The top offset should resolve a CSS variable (--app-header-h) rather than a fixed value, since the builder lets users add or remove a sticky site header per page.
  • Below the breakpoint where the two-column grid collapses to one column, the sidebar should drop sticky entirely — a filter panel pinned mid-scroll on mobile just eats screen space. Handle this with a Tailwind responsive variant (md:sticky) rather than JS.
  • The internal-scroll variant needs a real max-height ceiling tied to the viewport (calc(100vh - offset)) — a fixed pixel value will overflow on short laptop screens.