htmlcsscss-gridsticky

CSS sticky cards using grid layout


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

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);
    transition: all 0.5s;
}
<div class="cards">
    <div class="card" style="background-color: rgb(252 165 165);">1</div>
    <div class="card" style="background-color: rgb(147 197 253);">2</div>
    <div class="card" style="background-color: rgb(134 239 172);">3</div>
    <div class="card" style="background-color: rgb(253 224 71);">4</div>
    <div class="card" style="background-color: rgb(249 168 212);">5</div>
</div>

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.


Solution

  • 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 :)