CSS-only Infinite Card Carousel with Smooth Gradient Transitions

CSS-only Infinite Card Carousel with Smooth Gradient Transitions
CSS-only Infinite Card Carousel with Smooth Gradient Transitions
This is a lightweight carousel implementation that uses pure CSS/CSS3 to create an infinitely scrolling card carousel with an elegant gradient fading effect on both sides.

The carousel automatically pauses on hover and continues scrolling when the mouse moves away. Ideal for modern SaaS and AI applications where clean, flowing content displays are increasingly common.

See it in action:

How to use it:

1. You need a main container div with the class carousel. Optionally, add the mask attribute to enable the fade effect on the sides. Inside this container, place your cards as article elements (though other block elements would work too).

<div class="carousel" mask>
  <article>
    <img src="card1.jpg" alt="">
    <h2>Card 1</h2>
    <div>
      <p>Card 1 Content</p>
      <a href="#">Read more</a>
    </div>
  </article>
  <article>
    <img src="card2.jpg" alt="">
    <h2>Card 2</h2>
    <div>
      <p>Card 2 Content</p>
      <a href="#">Read more</a>
    </div>
  </article>
  ...
</div>

2. Include the following CSS in your stylesheet:

@layer base, demo;
@layer demo {
  .carousel {
    --items: 6;
    --carousel-duration: 40s;
    @media (width > 600px) {
      --carousel-duration: 30s;
    }
    --carousel-width: min(
      80vw,
      800px
    ); /* note - it will "break" if it gets too wide and there aren't enough items */
    --carousel-item-width: 280px;
    --carousel-item-height: 450px;
    --carousel-item-gap: 2rem;
    --clr-cta: rgb(0, 132, 209);
    position: relative;
    width: var(--carousel-width);
    height: var(--carousel-item-height);
    overflow: clip;
    &[mask] {
      /* fade out on sides */
      mask-image: linear-gradient(
        to right,
        transparent,
        black 10% 90%,
        transparent
      );
    }
    &[reverse] > article {
      animation-direction: reverse;
    }
    /* hover pauses animation */
    &:hover > article {
      animation-play-state: paused;
    }
  }
  .carousel > article {
    position: absolute;
    top: 0;
    left: calc(100% + var(--carousel-item-gap));
    width: var(--carousel-item-width);
    height: var(--carousel-item-height);
    display: grid;
    grid-template-rows: 200px auto 1fr auto;
    gap: 0.25rem;
    border: 1px solid rgba(255 255 255 / 0.15);
    padding-block-end: 1rem;
    border-radius: 10px;
    background: light-dark(white, rgba(255 255 255 / 0.05));
    color: light-dark(rgb(49, 65, 88), white);
    /* animation */
    will-change: transform;
    animation-name: marquee;
    animation-duration: var(--carousel-duration);
    animation-timing-function: linear;
    animation-iteration-count: infinite;
    animation-delay: calc(
      var(--carousel-duration) / var(--items) * 1 * var(--i) * -1
    );
    &:nth-child(1) {
      --i: 0;
    }
    &:nth-child(2) {
      --i: 1;
    }
    &:nth-child(3) {
      --i: 2;
    }
    &:nth-child(4) {
      --i: 3;
    }
    &:nth-child(5) {
      --i: 4;
    }
    &:nth-child(6) {
      --i: 5;
    }
    &:nth-child(7) {
      --i: 6;
    }
    &:nth-child(8) {
      --i: 7;
    }
  }
  .carousel img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    border-radius: 10px 10px 0 0;
  }
  .carousel > article > *:not(img) {
    padding: 0 1rem;
  }
  .carousel > article > div {
    grid-row: span 2;
    display: grid;
    grid-template-rows: subgrid;
    font-size: 0.8rem;
  }
  .carousel > article h2 {
    font-size: 1.2rem;
    font-weight: 300;
    padding-block: 0.75rem 0.25rem;
    margin: 0;
  }
  .carousel > article p {
    margin: 0;
  }
  .carousel > article a {
    text-decoration: none;
    text-transform: lowercase;
    border: 1px solid var(--clr-cta);
    color: light-dark(var(--clr-cta), white);
    border-radius: 3px;
    padding: 0.25rem 0.5rem;
    place-self: start;
    transition: 150ms ease-in-out;
    &:hover,
    &:focus-visible {
      background-color: var(--clr-cta);
      color: white;
      outline: none;
    }
  }
  @keyframes marquee {
    100% {
      transform: translateX(
        calc(
          (var(--items) * (var(--carousel-item-width) + var(--carousel-item-gap))) *
            -1
        )
      );
    }
  }
}
/* general styling */
@layer base {
  * {
    box-sizing: border-box;
  }
  :root {
    color-scheme: light dark;
    --bg-dark: rgb(2, 6, 24);
    --bg-light: rgb(229, 229, 229);
    --txt-light: rgb(10, 10, 10);
    --txt-dark: rgb(245, 245, 245);
  }
  body {
    background-color: light-dark(var(--bg-light), var(--bg-dark));
    color: light-dark(var(--txt-light), var(--txt-dark));
    min-height: 100svh;
    margin: 0;
    padding: 1rem;
    font-size: 1rem;
    font-family: "Abel", sans-serif;
    line-height: 1.5;
    display: grid;
    place-items: center;
    gap: 2rem;
  }
}

How it works:

Layout: The main .carousel container acts as a viewport (overflow: clip). The article cards are positioned absolutely, initially just off the right edge (left: calc(100% + var(--carousel-item-gap))).

Animation: A single marquee keyframe animation moves each card horizontally leftwards using transform: translateX. The total distance moved (calc((var(--items) * (var(--carousel-item-width) + var(--carousel-item-gap))) * -1)) is calculated to be the combined width of all items and their gaps.

Infinite Loop: animation-iteration-count: infinite; makes the animation repeat forever. animation-timing-function: linear; ensures constant speed.

Staggering: The magic is in the calculated animation-delay. By giving each subsequent card a progressively larger negative delay, they effectively start the animation at different points along the timeline. The first card starts immediately (or close to it), the second starts partway through its animation cycle, the third even further, and so on. This distribution makes it seem like a continuous stream entering from the right.

Fade Effect: The mask-image with a linear-gradient (transparent -> black -> transparent) applied to the .carousel container makes the content appear to fade in on the right and fade out on the left. The content itself isn’t fading; its visibility is being masked.

Hover Pause: A simple :hover rule on the container pauses the animation on all child article elements.

Performance & Best Practices:

Set --items Correctly: I can’t stress this enough. If the --items variable doesn’t match the actual number of article elements, the calculation for the animation delay and the final translateX in the keyframes will be off, leading to jerky movement or noticeable gaps/overlaps during the loop.

Item Count vs. Width: The comment in the CSS (/* note - it will "break" if it gets too wide and there aren't enough items */) is important. If the container (--carousel-width) is very wide, and you only have a few items, they might all appear on screen at once before the first item has fully scrolled off and looped back around. This technique works best when the total width of all items significantly exceeds the container width.

will-change: transform;: This is included on the article elements. It’s a hint to the browser that the transform property will be changing, allowing potential optimization. Use it judiciously, but it’s appropriate here for animation smoothness.

FAQs

Q: How do I change the scroll speed?

A: Modify the --carousel-duration CSS variable in the .carousel rule. A smaller value (e.g., 20s) makes it faster; a larger value (e.g., 60s) makes it slower.

Q: How do I add or remove cards?

A: Add or remove the <article> elements in your HTML. Critically, you must update the --items CSS variable to match the new total number of articles. If you have more items than the default 6, you also need to add corresponding :nth-child(n) { --i: n-1; } rules.

Q: Can I make it scroll from left to right instead?

A: Yes. The CSS includes a selector &[reverse] > article { animation-direction: reverse; }. Add the reverse attribute to your main <div class="carousel"> element in the HTML: <div class="carousel" mask reverse>.

Q: Why is there a large gap sometimes, or why does the loop jump?

A: This usually happens for two reasons:

  1. The --items CSS variable doesn’t match the actual number of article elements. Double-check the count.
  2. The carousel container (--carousel-width) might be too wide relative to the number of items and their combined width (--carousel-item-width + --carousel-item-gap). If all items fit comfortably within the container width before the loop completes, you’ll see a gap. This technique relies on the total item width being significantly larger than the container width. Try adding more items or reducing --carousel-width.

The post CSS-only Infinite Card Carousel with Smooth Gradient Transitions appeared first on CSS Script.


Discover more from RSS Feeds Cloud

Subscribe to get the latest posts sent to your email.

Discover more from RSS Feeds Cloud

Subscribe now to keep reading and get access to the full archive.

Continue reading