Product Grid

Shop

New arrivals

  • New
  • -20%
  • Desk Lamp

    Price: $79

  • New

    Smart Watch

    Price: $299

@use 'sprucecss/scss/spruce' as *;

.product-grid-block {
  padding-block: spacer-clamp('xl', 'xxl');

  &__inner {
    display: flex;
    flex-direction: column;
    gap: spacer('l');
  }

  &__header {
    align-items: end;
    display: flex;
    flex-wrap: wrap;
    gap: spacer('m');
    justify-content: space-between;
  }

  &__filters {
    @include layout-cluster('xs');
  }

  &__grid {
    @include clear-list;
    display: grid;
    gap: spacer('m');
    // auto-fill keeps the empty tracks, so a filtered list keeps the column count.
    grid-template-columns: repeat(auto-fill, minmax(min(16rem, 100%), 1fr));

    > li {
      display: flex;
      margin: 0 !important;
      margin-block: 0;
      padding-block: 0;
    }

    .product-card {
      inline-size: 100%;
    }
  }

  &__more {
    margin-block: 0;
    text-align: center;
  }
}
<section class="product-grid-block" aria-labelledby="product-grid-title" data-product-grid>
    <div class="container">
        <div class="product-grid-block__inner">
            <div class="product-grid-block__header">
            <header class="block-heading block-heading--start">
                <p class="block-heading__eyebrow">Shop</p>
                <h2 class="block-heading__title" id="product-grid-title">New arrivals</h2>
            </header>
                <div class="product-grid-block__filters" role="group" aria-label="Filter by category">
                    <button class="btn btn--sm btn--pill btn--primary" type="button" data-filter="all" aria-pressed="true">All</button>
                    <button class="btn btn--sm btn--pill btn--soft-primary" type="button" data-filter="bags" aria-pressed="false">Bags</button>
                    <button class="btn btn--sm btn--pill btn--soft-primary" type="button" data-filter="home" aria-pressed="false">Home</button>
                    <button class="btn btn--sm btn--pill btn--soft-primary" type="button" data-filter="tech" aria-pressed="false">Tech</button>
                </div>
            </div>
            <ul class="product-grid-block__grid" role="list">
                <li data-category="bags">
                    <article class="product-card">
                        <div class="product-card__media">
                            <img src="https://images.unsplash.com/photo-1553062407-98eeb64c6a62?auto=format&amp;fit=crop&amp;w=600&amp;q=70" alt="" loading="lazy" width="600" height="600">
                            <span class="product-card__tag">New</span>
                            <button class="product-card__wishlist" type="button" aria-pressed="false" data-wishlist>
                                <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M19 14c1.5-1.5 3-3.2 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.8 0-3 .5-4.5 2-1.5-1.5-2.7-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4 3 5.5l7 7z"/></svg>
                                <span class="sr-only">Save Everyday Backpack to the wishlist</span>
                            </button>
                        </div>
                        <div class="product-card__body">
                            <h3 class="product-card__title"><a class="product-card__link" href="#">Everyday Backpack</a></h3>
                            <p class="product-card__price"><span class="sr-only">Price: </span>$89</p>
                        </div>
                        <button class="btn btn--primary btn--sm product-card__action" type="button">Add to cart<span class="sr-only">: Everyday Backpack</span></button>
                    </article>
                </li>
                <li data-category="home">
                    <article class="product-card">
                        <div class="product-card__media">
                            <img src="https://images.unsplash.com/photo-1495474472287-4d71bcdd2085?auto=format&amp;fit=crop&amp;w=600&amp;q=70" alt="" loading="lazy" width="600" height="600">
                            <button class="product-card__wishlist" type="button" aria-pressed="false" data-wishlist>
                                <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M19 14c1.5-1.5 3-3.2 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.8 0-3 .5-4.5 2-1.5-1.5-2.7-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4 3 5.5l7 7z"/></svg>
                                <span class="sr-only">Save Ceramic Pour-Over to the wishlist</span>
                            </button>
                        </div>
                        <div class="product-card__body">
                            <h3 class="product-card__title"><a class="product-card__link" href="#">Ceramic Pour-Over</a></h3>
                            <p class="product-card__price"><span class="sr-only">Price: </span>$42</p>
                        </div>
                        <button class="btn btn--primary btn--sm product-card__action" type="button">Add to cart<span class="sr-only">: Ceramic Pour-Over</span></button>
                    </article>
                </li>
                <li data-category="tech">
                    <article class="product-card">
                        <div class="product-card__media">
                            <img src="https://images.unsplash.com/photo-1505740420928-5e560c06d30e?auto=format&amp;fit=crop&amp;w=600&amp;q=70" alt="" loading="lazy" width="600" height="600">
                            <button class="product-card__wishlist" type="button" aria-pressed="false" data-wishlist>
                                <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M19 14c1.5-1.5 3-3.2 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.8 0-3 .5-4.5 2-1.5-1.5-2.7-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4 3 5.5l7 7z"/></svg>
                                <span class="sr-only">Save Wireless Headphones to the wishlist</span>
                            </button>
                        </div>
                        <div class="product-card__body">
                            <h3 class="product-card__title"><a class="product-card__link" href="#">Wireless Headphones</a></h3>
                            <p class="product-card__price"><span class="sr-only">Price: </span>$199</p>
                        </div>
                        <button class="btn btn--primary btn--sm product-card__action" type="button">Add to cart<span class="sr-only">: Wireless Headphones</span></button>
                    </article>
                </li>
                <li data-category="bags">
                    <article class="product-card">
                        <div class="product-card__media">
                            <img src="https://images.unsplash.com/photo-1547949003-9792a18a2601?auto=format&amp;fit=crop&amp;w=600&amp;q=70" alt="" loading="lazy" width="600" height="600">
                            <span class="product-card__tag">-20%</span>
                            <button class="product-card__wishlist" type="button" aria-pressed="false" data-wishlist>
                                <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M19 14c1.5-1.5 3-3.2 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.8 0-3 .5-4.5 2-1.5-1.5-2.7-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4 3 5.5l7 7z"/></svg>
                                <span class="sr-only">Save Leather Weekender to the wishlist</span>
                            </button>
                        </div>
                        <div class="product-card__body">
                            <h3 class="product-card__title"><a class="product-card__link" href="#">Leather Weekender</a></h3>
                            <p class="product-card__price"><span class="sr-only">Price: </span>$249</p>
                        </div>
                        <button class="btn btn--primary btn--sm product-card__action" type="button">Add to cart<span class="sr-only">: Leather Weekender</span></button>
                    </article>
                </li>
                <li data-category="home">
                    <article class="product-card">
                        <div class="product-card__media">
                            <img src="https://images.unsplash.com/photo-1507473885765-e6ed057f782c?auto=format&amp;fit=crop&amp;w=600&amp;q=70" alt="" loading="lazy" width="600" height="600">
                            <button class="product-card__wishlist" type="button" aria-pressed="false" data-wishlist>
                                <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M19 14c1.5-1.5 3-3.2 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.8 0-3 .5-4.5 2-1.5-1.5-2.7-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4 3 5.5l7 7z"/></svg>
                                <span class="sr-only">Save Desk Lamp to the wishlist</span>
                            </button>
                        </div>
                        <div class="product-card__body">
                            <h3 class="product-card__title"><a class="product-card__link" href="#">Desk Lamp</a></h3>
                            <p class="product-card__price"><span class="sr-only">Price: </span>$79</p>
                        </div>
                        <button class="btn btn--primary btn--sm product-card__action" type="button">Add to cart<span class="sr-only">: Desk Lamp</span></button>
                    </article>
                </li>
                <li data-category="tech">
                    <article class="product-card">
                        <div class="product-card__media">
                            <img src="https://images.unsplash.com/photo-1523275335684-37898b6baf30?auto=format&amp;fit=crop&amp;w=600&amp;q=70" alt="" loading="lazy" width="600" height="600">
                            <span class="product-card__tag">New</span>
                            <button class="product-card__wishlist" type="button" aria-pressed="false" data-wishlist>
                                <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M19 14c1.5-1.5 3-3.2 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.8 0-3 .5-4.5 2-1.5-1.5-2.7-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4 3 5.5l7 7z"/></svg>
                                <span class="sr-only">Save Smart Watch to the wishlist</span>
                            </button>
                        </div>
                        <div class="product-card__body">
                            <h3 class="product-card__title"><a class="product-card__link" href="#">Smart Watch</a></h3>
                            <p class="product-card__price"><span class="sr-only">Price: </span>$299</p>
                        </div>
                        <button class="btn btn--primary btn--sm product-card__action" type="button">Add to cart<span class="sr-only">: Smart Watch</span></button>
                    </article>
                </li>
            </ul>
            <p class="sr-only" aria-live="polite" data-product-grid-status></p>
        </div>
    </div>
</section>
(() => {
  document.querySelectorAll('[data-product-grid]').forEach((block) => {
    const buttons = [...block.querySelectorAll('[data-filter]')];
    const items = [...block.querySelectorAll('[data-category]')];
    const status = block.querySelector('[data-product-grid-status]');

    // The wishlist hearts are toggle buttons.
    block.querySelectorAll('[data-wishlist]').forEach((button) => {
      button.addEventListener('click', () => {
        button.setAttribute('aria-pressed', String(button.getAttribute('aria-pressed') !== 'true'));
      });
    });

    buttons.forEach((button) => {
      button.addEventListener('click', () => {
        const filter = button.dataset.filter;
        let visible = 0;

        buttons.forEach((other) => {
          const isActive = other === button;

          other.setAttribute('aria-pressed', String(isActive));
          other.classList.toggle('btn--primary', isActive);
          other.classList.toggle('btn--soft-primary', !isActive);
        });

        items.forEach((item) => {
          const isMatch = filter === 'all' || item.dataset.category === filter;

          item.hidden = !isMatch;
          visible += isMatch ? 1 : 0;
        });

        status.textContent = `${visible} ${visible === 1 ? 'product' : 'products'} shown.`;
      });
    });
  });
})();

Product Grid

On this page

A product listing with category filters and product cards.

Technical Details

  • It combines the Product Card component; load its stylesheet too.
  • The filters are toggle buttons (aria-pressed) in a labelled group; the script shows the matching products and announces the result in a polite live region. Without JavaScript, every product is visible.
  • Every "Add to cart" button has the product name in a hidden text, so the buttons are distinguishable for screen reader users.
  • Override the content with productGridBlock.
  • Products can have a star rating (with reviews, the count) and an oldPrice; both have hidden labels ("Rated 4.8 out of 5", "Original price"). The heart is a toggle button (aria-pressed) labelled with the product name, above the card link overlay.
  • Set action to add a "view all" button under the grid.

Documentation

Learn about Spruce CSS through our extensive documentation.

Components

Explore our UI library of accessible components built with Spruce CSS.

Blocks and Pages

Full-width page blocks and complete page templates, ready to copy.

Find us on GitHub

Did you find a bug? Have an idea or a question? Please open an issue to help us develop the project.