# Sticky Steps

> Sticky step indicators tied to scroll progress — process explainers that stay oriented as content moves.

**Definition:** Sticky Steps is a scroll UI animation from collections.html. It is built with HTML, CSS, JavaScript, and GSAP, ships with a live preview, and is free to copy into personal and commercial projects.

## Facts
- Category: Scroll
- Stack: HTML, CSS, JavaScript, and GSAP
- Added: 2026-08-01
- Canonical page: https://html.bosconvertpro.com/collection/sticky-steps
- Full-page preview: https://html.bosconvertpro.com/collection/sticky-steps/full
- Live demo: https://html.bosconvertpro.com/demos/sticky-steps/index.html
- Markdown mirror: https://html.bosconvertpro.com/collection/sticky-steps/index.md
- License: https://html.bosconvertpro.com/terms

## How to use
1. Open https://html.bosconvertpro.com/collection/sticky-steps/full to preview the animation in full screen.
2. Copy index.html, styles.css, and script.js from the code tabs on https://html.bosconvertpro.com/collection/sticky-steps.
3. Paste the files into your project and adjust timing, easing, and content as needed.

## FAQ
### What is Sticky Steps?

Sticky Steps is a scroll UI animation from collections.html. Sticky step indicators tied to scroll progress — process explainers that stay oriented as content moves.

### What is Sticky Steps built with?

Sticky Steps is built with HTML, CSS, JavaScript, and GSAP. The source for every file is shown on the animation page and can be copied straight into your project.

### How do I use Sticky Steps in my project?

Open the animation page (https://html.bosconvertpro.com/collection/sticky-steps), preview it live, then copy index.html, styles.css, and script.js from the code tabs. Paste the files into your project and adjust timing, easing, and content as needed. A standalone demo runs at https://html.bosconvertpro.com/demos/sticky-steps/index.html.

### Is Sticky Steps free to use?

Yes. The Sticky Steps demo code may be used in personal and commercial projects. You may not resell or redistribute the collection itself as a competing template pack. Full terms: https://html.bosconvertpro.com/terms.

## Source — index.html
````html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Sticky Steps</title>
    <link rel="preconnect" href="https://fonts.googleapis.com" />
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
    <link
      href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500&family=Outfit:wght@300;400;500;600;700&display=swap"
      rel="stylesheet"
    />
    <link
      rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/lenis@1.2.3/dist/lenis.css"
    />
    <link rel="stylesheet" href="styles.css" />
  </head>
  <body>
    <section class="sticky-steps-start">
      <div class="sticky-steps__container">
        <h1 class="sticky-steps__h1">Our Process</h1>
      </div>
    </section>

    <section class="sticky-steps">
      <div class="sticky-steps__container">
        <div data-sticky-steps-init class="sticky-steps__collection">
          <div class="sticky-steps__list">
            <!-- STEP 01 -->
            <div
              class="sticky-steps__item"
              data-sticky-steps-item
              data-sticky-steps-item-status="active"
            >
              <div class="sticky-steps__text" data-sticky-steps-anchor>
                <span class="sticky-steps__eyebrow">Step 01</span>
                <h2 class="sticky-steps__h2">Discover</h2>
                <p class="sticky-steps__p">
                  We start by understanding your vision, audience, and goals —
                  mapping opportunities before a single pixel is designed.
                </p>
              </div>
              <div class="sticky-steps__media">
                <div class="sticky-steps__sticky">
                  <div class="sticky-steps__visual">
                    <img
                      class="sticky-steps__cover-image"
                      src="https://images.unsplash.com/photo-1600596542815-ffad4c1539a9?auto=format&fit=crop&w=1400&q=85"
                      alt="Modern architecture exterior"
                      loading="eager"
                    />
                  </div>
                </div>
              </div>
            </div>

            <!-- STEP 02 -->
            <div
              class="sticky-steps__item"
              data-sticky-steps-item
              data-sticky-steps-item-status="after"
            >
              <div class="sticky-steps__text" data-sticky-steps-anchor>
                <span class="sticky-steps__eyebrow">Step 02</span>
                <h2 class="sticky-steps__h2">Design</h2>
                <p class="sticky-steps__p">
                  Concepts turn into refined layouts, systems, and interactions
                  that feel intentional, calm, and unmistakably yours.
                </p>
              </div>
              <div class="sticky-steps__media">
                <div class="sticky-steps__sticky">
                  <div class="sticky-steps__visual">
                    <img
                      class="sticky-steps__cover-image"
                      src="https://images.unsplash.com/photo-1600585154340-be6161a56a0c?auto=format&fit=crop&w=1400&q=85"
                      alt="Minimal home with pathway"
                      loading="lazy"
                    />
                  </div>
                </div>
              </div>
            </div>

            <!-- STEP 03 -->
            <div
              class="sticky-steps__item"
              data-sticky-steps-item
              data-sticky-steps-item-status="after"
            >
              <div class="sticky-steps__text" data-sticky-steps-anchor>
                <span class="sticky-steps__eyebrow">Step 03</span>
                <h2 class="sticky-steps__h2">Develop</h2>
                <p class="sticky-steps__p">
                  Clean engineering brings the experience to life — fast,
                  accessible, and built to scale without losing the craft.
                </p>
              </div>
              <div class="sticky-steps__media">
                <div class="sticky-steps__sticky">
                  <div class="sticky-steps__visual">
                    <img
                      class="sticky-steps__cover-image"
                      src="https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?auto=format&fit=crop&w=1400&q=85"
                      alt="Warm contemporary interior"
                      loading="lazy"
                    />
                  </div>
                </div>
              </div>
            </div>

            <!-- STEP 04 -->
            <div
              class="sticky-steps__item"
              data-sticky-steps-item
              data-sticky-steps-item-status="after"
            >
              <div class="sticky-steps__text" data-sticky-steps-anchor>
                <span class="sticky-steps__eyebrow">Step 04</span>
                <h2 class="sticky-steps__h2">Deliver</h2>
                <p class="sticky-steps__p">
                  Launch with confidence. We refine performance, hand over a
                  polished product, and stay close for the next chapter.
                </p>
              </div>
              <div class="sticky-steps__media">
                <div class="sticky-steps__sticky">
                  <div class="sticky-steps__visual">
                    <img
                      class="sticky-steps__cover-image"
                      src="https://images.unsplash.com/photo-1613490493576-7fde63acd811?auto=format&fit=crop&w=1400&q=85"
                      alt="Luxury villa with pool"
                      loading="lazy"
                    />
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </section>

    <section class="sticky-steps-end">
      <div class="sticky-steps__container">
        <h2 class="sticky-steps__h1">Let’s build</h2>
      </div>
    </section>

    <script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/gsap.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/lenis@1.2.3/dist/lenis.min.js"></script>
    <script src="script.js"></script>
  </body>
</html>
````

## Source — styles.css
````css
/* ============================================
   Sticky Steps — Premium palette & type
   ============================================ */

:root {
  --bg: #f3efe8;
  --bg-deep: #ebe6dc;
  --ink: #1c1915;
  --ink-soft: #4a453c;
  --ink-muted: #7a7368;
  --ink-faint: #a39c90;
  --accent: #9a7b4f;
  --accent-soft: rgba(154, 123, 79, 0.55);
  --line: rgba(28, 25, 21, 0.08);
  --shadow: 0 24px 60px rgba(28, 25, 21, 0.1), 0 8px 20px rgba(28, 25, 21, 0.05);
  --font-display: "Cormorant Garamond", Georgia, "Times New Roman", serif;
  --font-body: "Outfit", system-ui, -apple-system, sans-serif;
}

*,
*::before,
*::after {
  box-sizing: border-box;
}

html {
  height: 100%;
  -webkit-text-size-adjust: 100%;
}

html,
body {
  margin: 0;
  padding: 0;
}

body {
  color: var(--ink);
  background:
    radial-gradient(ellipse 120% 80% at 50% 0%, #faf7f2 0%, transparent 55%),
    linear-gradient(180deg, var(--bg) 0%, var(--bg-deep) 100%);
  background-attachment: fixed;
  font-family: var(--font-body);
  font-size: 1em;
  font-weight: 400;
  line-height: 1.45;
  min-height: 100%;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
}

img {
  display: block;
  max-width: 100%;
  border: 0;
}

/* ---------- Intro / Outro ---------- */

.sticky-steps-start {
  text-align: center;
  align-items: flex-end;
  min-height: 42dvh;
  display: flex;
  padding-bottom: 1.5rem;
}

.sticky-steps-end {
  text-align: center;
  align-items: flex-start;
  min-height: 48dvh;
  display: flex;
  padding-top: 1.5rem;
}

.sticky-steps__h1 {
  font-family: var(--font-display);
  letter-spacing: -0.03em;
  margin: 0;
  font-size: min(6.5em, 16vw);
  font-weight: 500;
  line-height: 0.92;
  color: var(--ink);
  font-style: italic;
}

/* ---------- Section ---------- */

.sticky-steps {
  min-height: 100dvh;
  position: relative;
  overflow: clip;
  background: transparent;
}

.sticky-steps__container {
  max-width: 74em;
  margin-left: auto;
  margin-right: auto;
  padding-left: 1.5em;
  padding-right: 1.5em;
  width: 100%;
}

.sticky-steps__collection {
  min-height: 100dvh;
  display: flex;
  position: relative;
}

.sticky-steps__list {
  gap: 30dvh;
  flex-flow: column;
  flex: 1;
  padding-top: calc(50dvh - 7.5em);
  padding-bottom: calc(50dvh - 7.5em);
  display: flex;
  width: 100%;
}

/* NO position:relative on item */
.sticky-steps__item {
  display: block;
  width: 100%;
}

/* ---------- Text ---------- */

.sticky-steps__text {
  gap: 2em;
  flex-flow: column;
  width: 50%;
  padding-right: 6em;
  display: flex;
  position: relative;
  z-index: 2;
}

.sticky-steps__eyebrow {
  font-family: var(--font-body);
  color: var(--accent);
  text-transform: uppercase;
  font-size: 0.78em;
  font-weight: 600;
  letter-spacing: 0.22em;
  opacity: 1;
}

.sticky-steps__h2 {
  font-family: var(--font-display);
  letter-spacing: -0.03em;
  margin: 0;
  font-size: min(5.75em, 14vw);
  font-weight: 500;
  line-height: 0.92;
  color: var(--ink);
}

.sticky-steps__p {
  font-family: var(--font-body);
  color: var(--ink-muted);
  margin: 0;
  font-size: min(1.125em, 4.2vw);
  font-weight: 400;
  line-height: 1.65;
  max-width: 30ch;
  letter-spacing: 0.01em;
  opacity: 1;
}

/* ---------- Media (absolute to collection) ---------- */

.sticky-steps__media {
  width: 50%;
  height: 100%;
  padding-left: 3em;
  position: absolute;
  top: 0;
  right: 0;
  z-index: 1;
  pointer-events: none;
}

.sticky-steps__sticky {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  min-height: 100dvh;
  position: sticky;
  top: 0;
}

/*
  Portrait oval / capsule (NOT a circle):
  aspect-ratio 3/4 + huge border-radius
*/
.sticky-steps__visual {
  width: 100%;
  max-width: 100%;
  aspect-ratio: 3 / 4;
  border-radius: 500em;
  position: relative;
  overflow: hidden;
  background: linear-gradient(160deg, #e5dfd4 0%, #d4cdc2 100%);
  opacity: 0;
  will-change: opacity;
  box-shadow: var(--shadow);
  border: 1px solid rgba(255, 255, 255, 0.35);
}

.sticky-steps__cover-image {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  border-radius: inherit;
  display: block;
}

/* ---------- Desktop status (text only; images via GSAP) ---------- */

@media screen and (min-width: 992px) {
  [data-sticky-steps-item-status] .sticky-steps__visual {
    pointer-events: none;
  }

  /* CSS fallback if GSAP missing */
  html:not(.has-gsap) [data-sticky-steps-item-status] .sticky-steps__visual {
    transition: opacity 1s cubic-bezier(0.4, 0, 0.2, 1);
    opacity: 0;
  }

  html:not(.has-gsap) [data-sticky-steps-item-status="before"] .sticky-steps__visual,
  html:not(.has-gsap) [data-sticky-steps-item-status="active"] .sticky-steps__visual {
    opacity: 1;
  }

  html:not(.has-gsap) [data-sticky-steps-item-status="active"] .sticky-steps__media {
    z-index: 5;
  }

  html:not(.has-gsap) [data-sticky-steps-item-status="before"] .sticky-steps__media {
    z-index: 4;
  }

  html:not(.has-gsap) [data-sticky-steps-item-status="after"] .sticky-steps__media {
    z-index: 1;
  }

  [data-sticky-steps-item-status] .sticky-steps__text {
    transition: opacity 0.8s cubic-bezier(0.4, 0, 0.2, 1);
    opacity: 0.22;
  }

  [data-sticky-steps-item-status="active"] .sticky-steps__text {
    opacity: 1;
  }

  /* Keep accent readable when dimmed */
  [data-sticky-steps-item-status]:not([data-sticky-steps-item-status="active"]) .sticky-steps__eyebrow {
    color: var(--accent-soft);
  }
}

/* ---------- Mobile ---------- */

@media screen and (max-width: 991px) {
  .sticky-steps__list {
    gap: 7.5em;
    padding-top: 10em;
    padding-bottom: 10em;
  }

  .sticky-steps__text {
    width: 100%;
    padding-bottom: 5em;
    padding-right: 0;
  }

  .sticky-steps__media {
    width: min(100%, 22em);
    height: auto;
    padding-left: 0;
    position: relative;
    top: auto;
    right: auto;
    pointer-events: auto;
  }

  .sticky-steps__sticky {
    min-height: auto;
    position: relative;
    top: auto;
    justify-content: flex-start;
  }

  .sticky-steps__visual {
    opacity: 1;
    aspect-ratio: 3 / 4;
    border-radius: 500em;
  }
}

@media screen and (max-width: 767px) {
  .sticky-steps__text {
    gap: 1.5em;
  }

  .sticky-steps__h2 {
    font-size: min(3.25em, 12vw);
  }
}

@media (prefers-reduced-motion: reduce) {
  [data-sticky-steps-item-status] .sticky-steps__visual,
  [data-sticky-steps-item-status] .sticky-steps__text {
    transition: none !important;
  }
}
````

## Source — script.js
````javascript
/**
 * Sticky Steps
 * - Smooth GSAP crossfade for BOTH scroll down and scroll up
 *   (z-index managed in JS so fade is never buried)
 */

(() => {
  "use strict";

  let lenis = null;
  const hasGsap = typeof gsap !== "undefined";

  function initLenis() {
    if (typeof Lenis === "undefined") return;
    lenis = new Lenis({
      autoRaf: true,
      duration: 1.2,
      easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
      smoothWheel: true,
    });
  }

  function initStickyStepsBasic() {
    const containers = document.querySelectorAll("[data-sticky-steps-init]");
    if (!containers.length) return;

    containers.forEach((container) => {
      const items = [...container.querySelectorAll("[data-sticky-steps-item]")];
      if (!items.length) return;

      const layers = items.map((item) => ({
        item,
        media: item.querySelector(".sticky-steps__media"),
        visual: item.querySelector(".sticky-steps__visual"),
        anchor: item.querySelector("[data-sticky-steps-anchor]"),
      }));

      let current = -1;
      const isDesktop = () => window.matchMedia("(min-width: 992px)").matches;
      const DURATION = 1.0;
      const EASE = "power2.inOut";

      function setStatuses(activeIndex) {
        items.forEach((item, index) => {
          let status = "active";
          if (index < activeIndex) status = "before";
          if (index > activeIndex) status = "after";
          item.setAttribute("data-sticky-steps-item-status", status);
        });
      }

      function setVisualInstant(activeIndex) {
        layers.forEach(({ visual, media }, i) => {
          if (!visual) return;
          if (!isDesktop()) {
            visual.style.opacity = "1";
            if (media) media.style.zIndex = "";
            return;
          }
          const show = i <= activeIndex;
          if (hasGsap) {
            gsap.set(visual, { opacity: show ? 1 : 0 });
          } else {
            visual.style.opacity = show ? "1" : "0";
          }
          if (media) {
            media.style.zIndex = i === activeIndex ? "5" : i < activeIndex ? "3" : "1";
          }
        });
      }

      /**
       * Smooth crossfade in BOTH directions.
       *
       * Scroll DOWN (prev < next):
       *   incoming fades in ON TOP of previous
       *
       * Scroll UP (prev > next):
       *   outgoing stays ON TOP and fades out, revealing previous
       *   (this was broken when CSS dropped z-index on "after" instantly)
       */
      function crossfadeTo(activeIndex, prevIndex) {
        if (!isDesktop()) {
          setVisualInstant(activeIndex);
          return;
        }

        if (!hasGsap) {
          setVisualInstant(activeIndex);
          return;
        }

        const goingDown = activeIndex > prevIndex;
        const incoming = layers[activeIndex];
        const outgoing = prevIndex >= 0 ? layers[prevIndex] : null;

        // Base stacking: past under, future hidden under
        layers.forEach(({ visual, media }, i) => {
          if (!visual || !media) return;

          if (i === activeIndex || i === prevIndex) return; // handled below

          if (i < activeIndex) {
            gsap.set(media, { zIndex: 2 });
            gsap.to(visual, {
              opacity: 1,
              duration: DURATION * 0.5,
              ease: "power2.out",
              overwrite: true,
            });
          } else {
            gsap.set(media, { zIndex: 1 });
            gsap.to(visual, {
              opacity: 0,
              duration: DURATION,
              ease: EASE,
              overwrite: true,
            });
          }
        });

        if (goingDown) {
          // Incoming on top, fade in; outgoing stays under at full opacity
          if (outgoing?.media) gsap.set(outgoing.media, { zIndex: 4 });
          if (outgoing?.visual) {
            gsap.to(outgoing.visual, {
              opacity: 1,
              duration: DURATION * 0.4,
              ease: "power2.out",
              overwrite: true,
            });
          }

          if (incoming?.media) gsap.set(incoming.media, { zIndex: 10 });
          if (incoming?.visual) {
            gsap.fromTo(
              incoming.visual,
              { opacity: 0 },
              {
                opacity: 1,
                duration: DURATION,
                ease: EASE,
                overwrite: true,
              }
            );
          }
        } else {
          // Scroll UP: keep outgoing on top while it fades out
          if (incoming?.media) gsap.set(incoming.media, { zIndex: 4 });
          if (incoming?.visual) {
            gsap.set(incoming.visual, { opacity: 1 });
          }

          if (outgoing?.media) gsap.set(outgoing.media, { zIndex: 10 });
          if (outgoing?.visual) {
            gsap.to(outgoing.visual, {
              opacity: 0,
              duration: DURATION,
              ease: EASE,
              overwrite: true,
              onComplete: () => {
                // After fade, drop outgoing under
                if (outgoing?.media) gsap.set(outgoing.media, { zIndex: 1 });
              },
            });
          }
        }
      }

      function setActiveStep(activeIndex) {
        if (activeIndex === current) return;
        const prev = current;
        current = activeIndex;

        setStatuses(activeIndex);

        if (prev === -1) {
          setVisualInstant(activeIndex);
        } else {
          crossfadeTo(activeIndex, prev);
        }
      }

      function updateSteps() {
        const viewportCenter = window.innerHeight / 2;
        let closestIndex = 0;
        let closestDistance = Infinity;

        layers.forEach(({ anchor }, index) => {
          if (!anchor) return;
          const rect = anchor.getBoundingClientRect();
          const anchorCenter = rect.top + rect.height / 2;
          const distance = Math.abs(viewportCenter - anchorCenter);
          if (distance < closestDistance) {
            closestDistance = distance;
            closestIndex = index;
          }
        });

        setActiveStep(closestIndex);
      }

      window.addEventListener("scroll", updateSteps, { passive: true });
      window.addEventListener(
        "resize",
        () => {
          current = -1;
          updateSteps();
        },
        { passive: true }
      );

      if (lenis) lenis.on("scroll", updateSteps);

      requestAnimationFrame(updateSteps);
      window.addEventListener("load", updateSteps);
    });
  }

  function boot() {
    if (typeof gsap !== "undefined") {
      document.documentElement.classList.add("has-gsap");
    }
    initLenis();
    initStickyStepsBasic();
  }

  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", boot);
  } else {
    boot();
  }
})();
````
