Natural Sticky Footer Demo

Scroll down to see the footer appear naturally, then scroll back up to see it hide seamlessly.

💡 Pro tip: Click and drag to scroll with precision! Perfect for testing exactly how the footer behaves at different scroll speeds.

Lightweight champion: This footer uses the default settings: snapEagerness: 1.0 and scrollThreshold: 0. This footer script is only 1.2KB minified with zero dependencies. Pure TypeScript power!

🔍 Footer Implementation Deep Dive

The footer implementation is more complex than the header because bottom-anchored positioning behaves differently. Let's look at the actual source code:

/**
 * Attaches a natural hide/show behavior to a sticky element placed at the bottom.
 */
export function naturalStickyBottom(element: HTMLElement) {
  let lastScrollY = window.scrollY;
  let mode = 'relative';

  const handleScroll = () => {
    const currentScrollY = window.scrollY;
    const scrollingDown = currentScrollY > lastScrollY;
    const scrollingUp = currentScrollY < lastScrollY;
    const elementRect = element.getBoundingClientRect();
    const elementHeight = element.offsetHeight;
    const isElementVisible =
      elementRect.bottom > 0 && elementRect.top < window.innerHeight;

    // Scenario 1: Element is sticky at bottom and user scrolls up
    // Release the element from sticky position so it can scroll with content naturally
    if (mode === 'sticky' && scrollingUp) {
      mode = 'relative';
      element.style.position = 'relative';

      // When releasing from sticky bottom position, we need to calculate where
      // the element should be positioned to maintain visual continuity.
      // The element is currently at the bottom of the viewport, so we calculate
      // its position relative to the document end to maintain that relationship.
      const currentDocumentPosition = elementRect.top + currentScrollY;

      // Reset any previous bottom styling since we're switching to top-based positioning
      element.style.bottom = '';

      // Calculate the offset from the element's natural position (at document end)
      // This ensures the element appears to stay in place when transitioning from sticky
      const targetOffset =
        currentDocumentPosition -
        (document.documentElement.scrollHeight - elementHeight);

      element.style.top = `${targetOffset}px`;
    }
    // Scenario 2: Element is in relative mode, user scrolls down, and element is not visible
    // Position the element just below the viewport so it can naturally scroll into view
    else if (mode === 'relative' && scrollingDown && !isElementVisible) {
      element.style.position = 'relative';

      // Calculate where we want the element to appear (just below the viewport)
      const targetPosition = currentScrollY + window.innerHeight;

      // Get the element's current offset and natural position in the document
      const currentTopOffset = parseFloat(element.style.top || '0');
      const naturalElementTop =
        elementRect.top + currentScrollY - currentTopOffset;

      // Calculate the offset needed to position element just below viewport
      const offset = targetPosition - naturalElementTop;
      element.style.top = `${offset}px`;
    }
    // Scenario 3: Element is in relative mode and has scrolled into view at bottom
    // Make it sticky so it stays at the bottom of the viewport
    else if (mode === 'relative' && elementRect.bottom <= window.innerHeight) {
      mode = 'sticky';
      element.style.position = 'sticky';
      element.style.top = 'auto'; // Reset top positioning
      element.style.bottom = '0'; // Stick to bottom of viewport
    }

    lastScrollY = currentScrollY > 0 ? currentScrollY : 0;
  };

  // Run once on load to set the initial state correctly.
  handleScroll();

  window.addEventListener('scroll', handleScroll, { passive: true });

  return {
    destroy: () => {
      window.removeEventListener('scroll', handleScroll);
    },
  };
}

🧠 Why Bottom Is More Complex

As you can see from the source code above, the bottom implementation requires more sophisticated calculations. Here's why:

Bottom-anchored positioning: Elements positioned relative to bottom behave differently than top-anchored elements. We need to account for document height changes and viewport relationships.
Document flow calculations: When switching from sticky to relative positioning, we must preserve the element's visual position in the document while changing its anchor point.
Natural position tracking: We need to track where the element would naturally appear in the document flow vs. where it currently appears after applying offsets.

The bottom sticky implementation is particularly useful for:

🎚️ ScrollThreshold for Footers

This footer demo uses scrollThreshold: 0, meaning it activates at any scroll speed. For footers, you might want to use higher thresholds to create more intentional interactions:

Visit the ScrollThreshold Comparison to compare different values side-by-side.

🎭 The Three Scenarios Explained

The footer operates through three distinct scenarios:

Scenario 1 - Sticky to Released (scroll up):
When user scrolls up while footer is sticky, we calculate its current document position and switch to relative positioning with a complex offset calculation to maintain visual continuity.
Scenario 2 - Preparation (scroll down):
When scrolling down and footer isn't visible, we position it just below the viewport so it naturally scrolls into view as the user continues scrolling down.
Scenario 3 - Released to Sticky:
When the footer scrolls into view at the bottom, we switch it back to position: sticky; bottom: 0 so it stays anchored to the bottom of the viewport.

⚡ Performance Characteristics

Despite the complexity, the footer implementation is highly optimized:

📐 Mathematical Precision

The key mathematical relationships that make this work:

// Current position in document
currentDocumentPosition = elementRect.top + currentScrollY

// Natural position at document end
naturalPosition = documentHeight - elementHeight

// Offset to maintain visual continuity
targetOffset = currentDocumentPosition - naturalPosition

These calculations ensure that when we switch positioning modes, the footer appears to stay in exactly the same place visually, creating the seamless natural effect.

Keep scrolling to see how this feels compared to traditional implementations... Notice how the footer doesn't just "pop" into existence - it naturally flows into view as if it were always part of the content you're scrolling through.

🆚 Traditional vs Natural: Less Distracting Experience

❌ Traditional Slide Animations

  • Sudden slide-up from bottom edge
  • Fixed animation speed (usually 300ms)
  • Triggered by scroll thresholds
  • Can interrupt reading flow
  • Feels disconnected from user input

✅ Natural Sticky Approach

  • Naturally flows into view
  • Speed matches your scroll exactly
  • No artificial triggers or thresholds
  • Doesn't break reading concentration
  • Feels like part of the content

This creates a much more cohesive and less distracting user experience, especially important for elements like footers that typically contain secondary actions or information.

Try different scroll patterns - quick scrolls, slow scrolls, bouncing back and forth. The footer responds perfectly to every movement, always feeling natural and never jarring.