# Sticky Features

> Sticky feature panels with scroll-linked visuals and a progress bar — product benefits that stay in frame as you read.

**Definition:** Sticky Features 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-03
- Canonical page: https://html.bosconvertpro.com/collection/sticky-features
- Full-page preview: https://html.bosconvertpro.com/collection/sticky-features/full
- Live demo: https://html.bosconvertpro.com/demos/sticky-features/index.html
- Markdown mirror: https://html.bosconvertpro.com/collection/sticky-features/index.md
- License: https://html.bosconvertpro.com/terms

## How to use
1. Open https://html.bosconvertpro.com/collection/sticky-features/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-features.
3. Paste the files into your project and adjust timing, easing, and content as needed.

## FAQ
### What is Sticky Features?

Sticky Features is a scroll UI animation from collections.html. Sticky feature panels with scroll-linked visuals and a progress bar — product benefits that stay in frame as you read.

### What is Sticky Features built with?

Sticky Features 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 Features in my project?

Open the animation page (https://html.bosconvertpro.com/collection/sticky-features), 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-features/index.html.

### Is Sticky Features free to use?

Yes. The Sticky Features 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 Features</title>
    <link rel="preconnect" href="https://fonts.googleapis.com" />
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
    <link rel="preconnect" href="https://images.unsplash.com" />
    <link
      href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,500;0,600;1,400;1,500&family=Outfit:wght@300;400;500;600&display=swap"
      rel="stylesheet"
    />
    <link rel="stylesheet" href="styles.css" />
  </head>
  <body>
    <section class="resource-start">
      <span class="resource-eyebrow">Atelier Collection</span>
      <h1 class="resource-title">Spaces that feel like home</h1>
      <p class="resource-subtitle">Scroll to explore each feature</p>
    </section>

    <div data-sticky-feature-wrap class="sticky-features__wrap">
      <div class="sticky-features__scroll">
        <div class="sticky-features__container">
          <div class="sticky-features__col is--img">
            <div class="sticky-features__img-collection">
              <div class="sticky-features__img-list">
                <div data-sticky-feature-visual-wrap class="sticky-features__img-item">
                  <img
                    src="https://images.unsplash.com/photo-1600596542815-ffad4c1539a9?auto=format&fit=crop&w=1400&q=85"
                    alt="Modern architecture exterior"
                    class="sticky-features__img"
                    loading="eager"
                  />
                </div>
                <div data-sticky-feature-visual-wrap class="sticky-features__img-item">
                  <img
                    src="https://images.unsplash.com/photo-1600585154340-be6161a56a0c?auto=format&fit=crop&w=1400&q=85"
                    alt="Minimal home with pathway"
                    class="sticky-features__img"
                    loading="lazy"
                  />
                </div>
                <div data-sticky-feature-visual-wrap class="sticky-features__img-item">
                  <img
                    src="https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?auto=format&fit=crop&w=1400&q=85"
                    alt="Warm contemporary interior"
                    class="sticky-features__img"
                    loading="lazy"
                  />
                </div>
                <div data-sticky-feature-visual-wrap class="sticky-features__img-item">
                  <img
                    src="https://images.unsplash.com/photo-1613490493576-7fde63acd811?auto=format&fit=crop&w=1400&q=85"
                    alt="Luxury villa with pool"
                    class="sticky-features__img"
                    loading="lazy"
                  />
                </div>
              </div>
            </div>
            <div class="sticky-features__progress-w">
              <div class="sticky-features__progress-bar" data-sticky-feature-progress></div>
            </div>
          </div>
          <div class="sticky-features__col">
            <div class="sticky-features__text-collection">
              <div class="sticky-features__text-list">
                <div data-sticky-feature-item class="sticky-features__text-item">
                  <span data-sticky-feature-text class="sticky-features__tag">01</span>
                  <h2 data-sticky-feature-text class="sticky-features__heading">
                    Quiet Architecture
                  </h2>
                  <p data-sticky-feature-text class="sticky-features__p">
                    Clean volumes and soft light define every facade — designed
                    to feel calm from the first glance, not loud or overworked.
                  </p>
                  <p data-sticky-feature-text class="sticky-features__p is--link">
                    Explore design
                  </p>
                </div>
                <div data-sticky-feature-item class="sticky-features__text-item">
                  <span data-sticky-feature-text class="sticky-features__tag">02</span>
                  <h2 data-sticky-feature-text class="sticky-features__heading">
                    Material Honesty
                  </h2>
                  <p data-sticky-feature-text class="sticky-features__p">
                    Stone, timber, and glass chosen for how they age — honest
                    textures that grow richer with time and everyday use.
                  </p>
                  <p data-sticky-feature-text class="sticky-features__p is--link">
                    View materials
                  </p>
                </div>
                <div data-sticky-feature-item class="sticky-features__text-item">
                  <span data-sticky-feature-text class="sticky-features__tag">03</span>
                  <h2 data-sticky-feature-text class="sticky-features__heading">
                    Lived-In Light
                  </h2>
                  <p data-sticky-feature-text class="sticky-features__p">
                    Interiors shaped around natural light and warm shadow —
                    spaces that invite lingering, not just passing through.
                  </p>
                  <p data-sticky-feature-text class="sticky-features__p is--link">
                    See interiors
                  </p>
                </div>
                <div data-sticky-feature-item class="sticky-features__text-item">
                  <span data-sticky-feature-text class="sticky-features__tag">04</span>
                  <h2 data-sticky-feature-text class="sticky-features__heading">
                    Outdoor Escape
                  </h2>
                  <p data-sticky-feature-text class="sticky-features__p">
                    Terraces and pools as quiet extensions of the home —
                    private retreats where architecture meets open air.
                  </p>
                  <p data-sticky-feature-text class="sticky-features__p is--link">
                    Discover more
                  </p>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>

    <section class="resource-end">
      <span class="resource-eyebrow">Ready when you are</span>
      <h2 class="resource-title">Let's build something lasting</h2>
    </section>

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

## Source — styles.css
````css
: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);
  --surface: rgba(255, 255, 255, 0.45);
  --progress-track: rgba(28, 25, 21, 0.1);
  --progress-fill: #1c1915;
  --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 {
  -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: 16px;
  font-weight: 400;
  line-height: 1.45;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
}

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

.resource-start,
.resource-end {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  min-height: 48vh;
  text-align: center;
  padding: 2.5em 1.25em;
}

.resource-eyebrow {
  font-family: var(--font-body);
  color: var(--accent);
  text-transform: uppercase;
  font-size: 0.78em;
  font-weight: 600;
  letter-spacing: 0.22em;
  margin-bottom: 1.25em;
}

.resource-title {
  margin: 0;
  font-family: var(--font-display);
  font-size: clamp(2.75em, 7vw, 5em);
  font-weight: 500;
  line-height: 0.95;
  letter-spacing: -0.03em;
  color: var(--ink);
  font-style: italic;
}

.resource-subtitle {
  margin: 1.25em 0 0;
  color: var(--ink-muted);
  font-size: 1.125em;
  font-weight: 400;
  letter-spacing: 0.01em;
}

.sticky-features__wrap {
  width: 100%;
  padding-left: 1.25em;
  padding-right: 1.25em;
  position: relative;
}

.sticky-features__scroll {
  justify-content: center;
  align-items: center;
  height: 100vh;
  display: flex;
}

.sticky-features__container {
  grid-column-gap: 1.5em;
  grid-row-gap: 1.5em;
  justify-content: center;
  align-items: stretch;
  width: 100%;
  max-width: 70em;
  margin-left: auto;
  margin-right: auto;
  display: flex;
}

.sticky-features__col {
  flex: 1;
  position: relative;
}

.sticky-features__col.is--img {
  overflow: hidden;
  border-radius: 0.85em;
  box-shadow:
    0 24px 60px rgba(28, 25, 21, 0.1),
    0 8px 20px rgba(28, 25, 21, 0.05);
  border: 1px solid rgba(255, 255, 255, 0.5);
}

.sticky-features__progress-w {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 0.25em;
  background-color: var(--progress-track);
  z-index: 5;
}

.sticky-features__progress-bar {
  width: 100%;
  height: 100%;
  background-color: var(--progress-fill);
  transform: scale3d(0, 1, 1);
  transform-origin: 0% 50%;
}

.sticky-features__img-collection {
  width: 100%;
}

.sticky-features__img-list {
  aspect-ratio: 1 / 1.3;
  width: 100%;
  position: relative;
  overflow: hidden;
  border-radius: 0.85em;
  background: linear-gradient(160deg, #e5dfd4 0%, #d4cdc2 100%);
}

.sticky-features__img-item {
  -webkit-clip-path: inset(100% 0 0 0);
  clip-path: inset(100% 0 0 0);
  width: 100%;
  height: 100%;
  position: absolute;
  inset: 0;
  will-change: clip-path;
}

[data-sticky-feature-visual-wrap]:first-of-type {
  clip-path: inset(0% 0 0 0);
  z-index: 1;
}

.sticky-features__img {
  object-fit: cover;
  object-position: center;
  width: 100%;
  height: 115%;
  position: absolute;
  top: 0;
  left: 0;
  will-change: transform;
}

.sticky-features__text-collection {
  height: 100%;
}

.sticky-features__text-list {
  flex-flow: column;
  justify-content: center;
  align-items: flex-start;
  height: 100%;
  max-height: 100%;
  display: flex;
  position: relative;
}

.sticky-features__text-item {
  grid-column-gap: 1.25em;
  grid-row-gap: 1.25em;
  visibility: hidden;
  flex-flow: column;
  justify-content: center;
  align-items: flex-start;
  width: 100%;
  max-width: 27.5em;
  margin-left: auto;
  display: flex;
  position: absolute;
  right: 0;
}

[data-sticky-feature-item]:first-of-type {
  visibility: visible;
}

.sticky-features__heading {
  margin-top: 0;
  margin-bottom: 0;
  font-family: var(--font-display);
  font-size: 3.5em;
  font-weight: 500;
  line-height: 1;
  letter-spacing: -0.03em;
  color: var(--ink);
}

.sticky-features__p {
  color: var(--ink-muted);
  margin-bottom: 0;
  font-size: 1.125em;
  font-weight: 400;
  line-height: 1.55;
  letter-spacing: 0.01em;
  max-width: 32ch;
}

.sticky-features__p.is--link {
  color: var(--ink);
  text-decoration: underline;
  text-underline-offset: 0.2em;
  text-decoration-thickness: 1px;
  cursor: pointer;
  font-weight: 500;
  transition: color 0.25s ease;
}

.sticky-features__p.is--link:hover {
  color: var(--accent);
}

.sticky-features__tag {
  background-color: rgba(28, 25, 21, 0.06);
  color: var(--accent);
  border-radius: 0.25em;
  margin-bottom: 0.25em;
  padding: 0.5em 0.7em;
  font-family: var(--font-body);
  font-size: 0.8125em;
  font-weight: 600;
  letter-spacing: 0.12em;
  line-height: 1;
  display: inline-block;
}

@media screen and (max-width: 767px) {
  .sticky-features__scroll {
    justify-content: center;
    align-items: center;
    height: auto;
    min-height: 100svh;
    padding-top: 1.25em;
    padding-bottom: 2.5em;
  }

  .sticky-features__container {
    grid-column-gap: 2em;
    grid-row-gap: 2em;
    flex-flow: column;
    justify-content: flex-start;
    align-items: stretch;
  }

  .sticky-features__img-list {
    aspect-ratio: 1;
  }

  .sticky-features__text-list {
    min-height: 20em;
    max-height: none;
  }

  .sticky-features__text-item {
    max-width: none;
  }

  .sticky-features__heading {
    font-size: 2.5em;
  }

  .sticky-features__p {
    font-size: 1em;
    max-width: none;
  }

  .sticky-features__tag {
    margin-bottom: 0;
  }
}
````

## Source — script.js
````javascript
gsap.registerPlugin(ScrollTrigger);

function initStickyFeatures(root) {
  const wraps = Array.from(
    (root || document).querySelectorAll("[data-sticky-feature-wrap]")
  );
  if (!wraps.length) return;

  wraps.forEach((w) => {
    const visualWraps = Array.from(
      w.querySelectorAll("[data-sticky-feature-visual-wrap]")
    );
    const items = Array.from(w.querySelectorAll("[data-sticky-feature-item]"));
    const progressBar = w.querySelector("[data-sticky-feature-progress]");

    if (visualWraps.length !== items.length) {
      console.warn(
        "[initStickyFeatures] visualWraps and items count do not match:",
        {
          visualWraps: visualWraps.length,
          items: items.length,
          wrap: w,
        }
      );
    }

    const count = Math.min(visualWraps.length, items.length);
    if (count < 1) return;

    const rm = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
    const DURATION = rm ? 0.01 : 1.05;
    const EASE = "expo.inOut";
    const IMG_EASE = "power3.out";
    const SCROLL_AMOUNT = 0.9;
    const OPEN = "inset(0% 0 0 0)";
    const CLOSED = "inset(100% 0 0 0)";

    const getTexts = (el) =>
      Array.from(el.querySelectorAll("[data-sticky-feature-text]"));

    const getImg = (wrap) => wrap.querySelector(".sticky-features__img");

    visualWraps.forEach((wrap, i) => {
      const img = getImg(wrap);
      if (i === 0) {
        gsap.set(wrap, { clipPath: OPEN, zIndex: 1 });
        if (img) gsap.set(img, { yPercent: 0 });
      } else {
        gsap.set(wrap, { clipPath: CLOSED, zIndex: 0 });
        if (img) gsap.set(img, { yPercent: 12 });
      }
    });

    items.forEach((item, i) => {
      const texts = getTexts(item);
      if (i === 0) {
        gsap.set(item, { autoAlpha: 1 });
        gsap.set(texts, { autoAlpha: 1, y: 0, filter: "blur(0px)" });
      } else {
        gsap.set(item, { autoAlpha: 0 });
        gsap.set(texts, { autoAlpha: 0, y: 36, filter: "blur(6px)" });
      }
    });

    let currentIndex = 0;
    let textTween = null;

    function transition(fromIndex, toIndex) {
      if (fromIndex === toIndex) return;

      const goingDown = toIndex > fromIndex;
      const fromWrap = visualWraps[fromIndex];
      const toWrap = visualWraps[toIndex];
      const fromImg = getImg(fromWrap);
      const toImg = getImg(toWrap);
      const tl = gsap.timeline({ defaults: { overwrite: "auto" } });

      if (goingDown) {
        gsap.set(fromWrap, { zIndex: 1 });
        gsap.set(toWrap, { zIndex: 3, clipPath: CLOSED });

        if (toImg) {
          gsap.set(toImg, { yPercent: 14 });
          tl.to(
            toImg,
            {
              yPercent: 0,
              duration: DURATION,
              ease: IMG_EASE,
            },
            0
          );
        }

        if (fromImg) {
          tl.to(
            fromImg,
            {
              yPercent: -6,
              duration: DURATION,
              ease: EASE,
            },
            0
          );
        }

        tl.to(
          toWrap,
          {
            clipPath: OPEN,
            duration: DURATION,
            ease: EASE,
          },
          0
        );
      } else {
        gsap.set(toWrap, { zIndex: 1, clipPath: OPEN });
        gsap.set(fromWrap, { zIndex: 3 });

        if (toImg) {
          gsap.set(toImg, { yPercent: -6 });
          tl.to(
            toImg,
            {
              yPercent: 0,
              duration: DURATION,
              ease: IMG_EASE,
            },
            0
          );
        }

        if (fromImg) {
          tl.to(
            fromImg,
            {
              yPercent: 12,
              duration: DURATION,
              ease: EASE,
            },
            0
          );
        }

        tl.to(
          fromWrap,
          {
            clipPath: CLOSED,
            duration: DURATION,
            ease: EASE,
          },
          0
        );
      }

      animateText(items[fromIndex], items[toIndex], goingDown);
    }

    function animateText(fromItem, toItem, goingDown) {
      if (textTween) textTween.kill();

      const outTexts = getTexts(fromItem);
      const inTexts = getTexts(toItem);
      const outY = goingDown ? -32 : 32;
      const inFromY = goingDown ? 40 : -40;

      textTween = gsap.timeline({ defaults: { overwrite: "auto" } });

      textTween.to(
        outTexts,
        {
          autoAlpha: 0,
          y: outY,
          filter: "blur(8px)",
          duration: rm ? 0.01 : 0.7,
          ease: "power2.inOut",
          stagger: {
            each: 0.05,
            from: "start",
          },
        },
        0
      );

      textTween.set(fromItem, { autoAlpha: 0 }, 0.55);
      textTween.set(toItem, { autoAlpha: 1 }, 0.2);

      textTween.fromTo(
        inTexts,
        {
          autoAlpha: 0,
          y: inFromY,
          filter: "blur(8px)",
        },
        {
          autoAlpha: 1,
          y: 0,
          filter: "blur(0px)",
          duration: rm ? 0.01 : 1,
          ease: "power3.out",
          stagger: {
            each: 0.09,
            from: "start",
          },
        },
        0.28
      );
    }

    const steps = Math.max(1, count - 1);

    ScrollTrigger.create({
      trigger: w,
      start: "center center",
      end: () => `+=${steps * 100}%`,
      pin: true,
      scrub: true,
      invalidateOnRefresh: true,
      onUpdate: (self) => {
        const p = Math.min(self.progress, SCROLL_AMOUNT) / SCROLL_AMOUNT;
        let idx = Math.floor(p * steps + 1e-6);
        idx = Math.max(0, Math.min(steps, idx));

        gsap.to(progressBar, {
          scaleX: p,
          ease: "none",
          duration: 0.15,
          overwrite: true,
        });

        if (idx !== currentIndex) {
          transition(currentIndex, idx);
          currentIndex = idx;
        }
      },
    });
  });
}

document.addEventListener("DOMContentLoaded", () => {
  initStickyFeatures();
});
````
