skip to Main Content

I want to have a layout of sticky cards using grid layout. This is what I managed to achieve:
enter image description here

<div class="cards">
    <div class="card">1</div>
    <div class="card">2</div>
    <div class="card">3</div>
    <div class="card">4</div>
    <div class="card">5</div>
</div>
:root {
    --card-top-offset: 3rem;
    --card-h: 36rem;
}

.cards {
    display: grid;
    grid-template-columns: repeat(1, minmax(0, 1fr));
    gap: calc(var(--card-h) / 2);
    align-items: baseline;
}

.card {
    position: sticky;
    top: 0;
    height: var(--card-h);
    transition: all 0.5s;
}
for (const cards of document.querySelectorAll(".cards")) {
    let i = 0;
    for (const card of cards.children) {
        if (card.classList.contains("card")) {
            card.style.top = `calc(5vh + ${i} * var(--card-top-offset))`;
            i += 1;
        }
    }

    cards.style.setProperty(
        "grid-template-rows",
        `repeat(${i}, var(--card-h))`,
    );
}

It works fine for the first 4 cards, however the fifth one unsticks sooner that I expect. I think that’s due to the fact that the parent element cards is ending. Is there any way to achieve this behavior using css grid? I found some other approaches but it involved hard-coding certain heights on some elements and I would like to avoid that.

2

Answers


  1. The issue you’re encountering is due to the fact that the sticky positioning relies on the height of the parent container. When the parent container ends, the sticky element cannot stick to the top anymore. To achieve the behavior you’re looking for, you can leverage CSS Grid with some tweaks.

    
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Sticky Cards</title>
        <style>
            :root {
                --card-top-offset: 3rem;
                --card-h: 36rem;
            }
    
            body {
                margin: 0;
                padding: 0;
                font-family: Arial, sans-serif;
            }
    
            .cards {
                display: grid;
                grid-template-columns: repeat(1, minmax(0, 1fr));
                gap: calc(var(--card-h) / 2);
                align-items: baseline;
                position: relative;
                padding-top: 5vh;
            }
    
            .card {
                position: sticky;
                top: calc(5vh + var(--card-top-offset) * var(--i));
                height: var(--card-h);
                transition: all 0.5s;
                background: lightblue;
            }
    
            .card:nth-child(1) {
                --i: 0;
            }
    
            .card:nth-child(2) {
                --i: 1;
            }
    
            .card:nth-child(3) {
                --i: 2;
            }
    
            .card:nth-child(4) {
                --i: 3;
            }
    
            .card:nth-child(5) {
                --i: 4;
            }
        </style>
    </head>
    <body>
        <div class="cards">
            <div class="card">1</div>
            <div class="card">2</div>
            <div class="card">3</div>
            <div class="card">4</div>
            <div class="card">5</div>
        </div>
    </body>
    </html>
    

    Here, each card has a –i custom property that determines its position relative to the top. This avoids hardcoding heights and allows for a more dynamic setup. The position: sticky is still used, but with a calculated top offset.

    Login or Signup to reply.
  2. According to Mozilla:

    A stickily positioned element is an element whose computed position value is sticky. It’s treated as relatively positioned until its containing block crosses a specified threshold (such as setting top to value other than auto) within its flow root (or the container it scrolls within), at which point it is treated as "stuck" until meeting the opposite edge of its containing block.

    Your last card is overlapping when:

    meeting the opposite edge of its containing block.

    This is how the sticky works. The only solution is to increase the height of the container, but at the end of it will overlap anyway accordingly to the sticky behaviour. The solution will be as below if you want to use position: sticky;
    As you can see it is still not what you want to achieve(I do believe :)).

    document.addEventListener("DOMContentLoaded", () => {
        for (const cards of document.querySelectorAll(".cards")) {
            let i = 0;
            for (const card of cards.children) {
                if (card.classList.contains("card")) {
                    card.style.top = `calc(5vh + ${i} * var(--card-top-offset))`;
                    i += 1;
                }
            }
    
            cards.style.setProperty(
                "grid-template-rows",
                `repeat(${i}, var(--card-h))`
            );
        }
    });
    :root {
        --card-top-offset: 3rem;
        --card-h: 36rem;
    }
    
    .cards {
        display: grid;
        grid-template-columns: repeat(1, minmax(0, 1fr));
        gap: calc(var(--card-h) / 2);
        align-items: baseline;
    }
    
    .card {
        position: sticky;
        top: 0;
        height: var(--card-h);
      
    }
    
    /* Add a bottom padding to the container to prevent overlap */
    .cards::after {
        content: '';
        display: block;
        height: calc(var(--card-h) + var(--card-top-offset));
      
    }
    
    
    .card:nth-child(1){
      background-color: red;
    }
    .card:nth-child(2){
      background-color: blue;
    }
    .card:nth-child(3){
      background-color: green;
    }
    .card:nth-child(4){
      background-color: violet;
      
    }
    .card:nth-child(5){
      background-color: black;
    }
    <div class="cards">
        <div class="card">1</div>
        <div class="card">2</div>
        <div class="card">3</div>
        <div class="card">4</div>
        <div class="card">5</div>
    </div>

    I would suggest you to not use position sticky and use different approach:

    const cards = document.querySelectorAll('.card');
    const offset = 20; // Set your desired offset here
    
    function updateCardPositions() {
        const scrollTop = window.scrollY;
        cards.forEach((card, index) => {
            const cardOffset = parseInt(card.style.getPropertyValue('--offset-card'));
            const topPosition = Math.max(0, scrollTop - cardOffset);
            card.style.top = `${topPosition}px`;
        });
    }
    
    window.addEventListener('scroll', updateCardPositions);
    window.addEventListener('resize', updateCardPositions);
    updateCardPositions(); // Initial position update
    
    // Adjust container height to prevent cards from disappearing
    const container = document.querySelector('.container');
    container.style.height = `${cards.length * offset}px`;
    body {
        font-family: Arial, sans-serif;
        margin: 0;
        padding: 0;
        background-color: #f0f0f0;
        height:300vh;
    }
    
    .container {
        max-width: 800px;
        margin: 50px auto;
        padding: 20px;
    }
    
    .card {
        background-color: white;
        border: 1px solid #ccc;
        border-radius: 8px;
        padding: 20px;
        margin-bottom: 20px; /* Space between cards */
        position: relative;
        transition: transform 0.3s ease;
        box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
        z-index: 1; /* Ensure cards stack properly */
    }
    
    .card h2 {
        margin-top: 0;
    }
    
    .card p {
        margin-bottom: 0;
    }
      <div class="container">
            <div class="card" style="--offset-card: 0;">
                <h2>Card 1</h2>
                <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
            </div>
            <div class="card" style="--offset-card: 80px;">
                <h2>Card 2</h2>
                <p>Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
            </div>
            <div class="card" style="--offset-card: 160px;">
                <h2>Card 3</h2>
                <p>Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
            </div>
            <div class="card" style="--offset-card: 260px;">
                <h2>Card 4</h2>
                <p>Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.</p>
            </div>
            <div class="card" style="--offset-card: 340px;">
                <h2>Card 5</h2>
                <p>Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
            </div>
        </div>

    Adjust for your needs 🙂

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search