Product

-25%

Everyday Backpack

Rated 4.5 out of 5 (128)

Price: $89 Original price: $119

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

.product-cards {
  @include layout-grid('m', 14rem);
}

.product-card {
  @include a11y-card-link('.product-card__link', true);
  background-color: color('surface');
  border: config('border-width-base', $display) solid color('border');
  border-radius: radius('lg');
  display: flex;
  flex-direction: column;
  overflow: hidden;
  @include transition($property: 'box-shadow, translate');

  &:hover {
    box-shadow: shadow('md');
    translate: 0 -2px;
  }

  &:focus-within {
    outline: 2px solid color('focus-ring');
    outline-offset: 2px;
  }

  &__media {
    aspect-ratio: 1;
    background-color: color('surface-muted');
    position: relative;

    img {
      block-size: 100%;
      inline-size: 100%;
      object-fit: cover;
    }
  }

  &__tag {
    background-color: color('danger', 'alert');
    border-radius: radius('pill');
    color: hsl(0deg 0% 100%);
    font-size: config('font-size-sm', $typography);
    font-weight: 700;
    inset-block-start: spacer('s');
    inset-inline-start: spacer('s');
    padding: spacer('xxs') spacer('xs');
    position: absolute;
  }

  // A toggle button above the card link overlay (z-index), like the action.
  &__wishlist {
    align-items: center;
    background-color: color('surface');
    block-size: 2.25rem;
    border: 0;
    border-radius: 50%;
    box-shadow: shadow('sm');
    color: color('heading');
    cursor: pointer;
    display: flex;
    inline-size: 2.25rem;
    inset-block-start: spacer('s');
    inset-inline-end: spacer('s');
    justify-content: center;
    position: absolute;
    z-index: 1;

    svg {
      block-size: 1.15rem;
      inline-size: 1.15rem;
    }

    &:hover {
      color: color('danger', 'alert');
    }

    &:focus-visible {
      outline: 2px solid color('focus-ring');
      outline-offset: 2px;
    }

    &[aria-pressed='true'] {
      color: color('danger', 'alert');

      svg {
        fill: currentColor;
      }
    }
  }

  &__body {
    flex-grow: 1;
    padding: spacer('s') spacer('s') 0;
    @include layout-stack('xxs');
  }

  &__title {
    font-size: font-size('h5');
    margin-block: 0;
  }

  &__link {
    color: color('heading');
    text-decoration: none;

    &:focus {
      outline: none;
    }
  }

  &__rating {
    align-items: center;
    display: flex;
    font-size: config('font-size-sm', $typography);
    gap: spacer('xxs');
  }

  &__stars {
    --rating: 0;
    --star-size: 1rem;
    color: color('border');
    display: inline-flex;
    position: relative;

    svg {
      block-size: var(--star-size);
      inline-size: var(--star-size);
    }

    // The fill: a gradient up to the rating, cut to star shapes by a repeating star mask.
    &::after {
      background-image: linear-gradient(var(--star-direction, 90deg), #{color('warning', 'alert')} calc(var(--rating) / 5 * 100%), transparent 0);
      content: '';
      inset: 0;
      mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='m12 2 3.1 6.3 6.9 1-5 4.9 1.2 6.8L12 17.8 5.8 21l1.2-6.8-5-4.9 6.9-1z'/%3E%3C/svg%3E") 0 0 / var(--star-size) var(--star-size) repeat-x;
      position: absolute;
    }
  }

  &__reviews {
    color: color('muted');
  }

  &__price {
    color: color('heading');
    font-size: config('font-size-lg', $typography);
    font-weight: 700;
  }

  &__old-price {
    color: color('muted');
    font-size: config('font-size-sm', $typography);
    font-weight: 400;
    margin-inline-start: spacer('xxs');
  }

  &__action {
    margin: spacer('s');
    position: relative;
    z-index: 1;
  }
}

// The stars fill from the right in RTL.
[dir='rtl'] .product-card__stars {
  --star-direction: 270deg;
}
<div class="product-cards">
    <article class="product-card">
        <div class="product-card__media">
            <img src="https://images.unsplash.com/photo-1553062407-98eeb64c6a62?auto=format&fit=crop&w=600&q=70" alt="" loading="lazy" width="600" height="600">
            <span class="product-card__tag">-25%</span>
        </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__rating"><span class="product-card__stars" style="--rating: 4.5;" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" focusable="false"><path d="m12 2 3.1 6.3 6.9 1-5 4.9 1.2 6.8L12 17.8 5.8 21l1.2-6.8-5-4.9 6.9-1z"/></svg><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" focusable="false"><path d="m12 2 3.1 6.3 6.9 1-5 4.9 1.2 6.8L12 17.8 5.8 21l1.2-6.8-5-4.9 6.9-1z"/></svg><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" focusable="false"><path d="m12 2 3.1 6.3 6.9 1-5 4.9 1.2 6.8L12 17.8 5.8 21l1.2-6.8-5-4.9 6.9-1z"/></svg><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" focusable="false"><path d="m12 2 3.1 6.3 6.9 1-5 4.9 1.2 6.8L12 17.8 5.8 21l1.2-6.8-5-4.9 6.9-1z"/></svg><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" focusable="false"><path d="m12 2 3.1 6.3 6.9 1-5 4.9 1.2 6.8L12 17.8 5.8 21l1.2-6.8-5-4.9 6.9-1z"/></svg></span><span class="sr-only">Rated 4.5 out of 5</span> <span class="product-card__reviews">(128)</span></p>
            <p class="product-card__price"><span class="sr-only">Price: </span>$89 <del class="product-card__old-price"><span class="sr-only">Original price: </span>$119</del></p>
        </div>
        <button class="btn btn--primary btn--sm product-card__action" type="button">Add to cart</button>
    </article>
    <article class="product-card">
        <div class="product-card__media">
            <img src="https://images.unsplash.com/photo-1495474472287-4d71bcdd2085?auto=format&fit=crop&w=600&q=70" alt="" loading="lazy" width="600" height="600">
            <span class="product-card__tag">New</span>
        </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__rating"><span class="product-card__stars" style="--rating: 4;" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" focusable="false"><path d="m12 2 3.1 6.3 6.9 1-5 4.9 1.2 6.8L12 17.8 5.8 21l1.2-6.8-5-4.9 6.9-1z"/></svg><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" focusable="false"><path d="m12 2 3.1 6.3 6.9 1-5 4.9 1.2 6.8L12 17.8 5.8 21l1.2-6.8-5-4.9 6.9-1z"/></svg><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" focusable="false"><path d="m12 2 3.1 6.3 6.9 1-5 4.9 1.2 6.8L12 17.8 5.8 21l1.2-6.8-5-4.9 6.9-1z"/></svg><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" focusable="false"><path d="m12 2 3.1 6.3 6.9 1-5 4.9 1.2 6.8L12 17.8 5.8 21l1.2-6.8-5-4.9 6.9-1z"/></svg><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" focusable="false"><path d="m12 2 3.1 6.3 6.9 1-5 4.9 1.2 6.8L12 17.8 5.8 21l1.2-6.8-5-4.9 6.9-1z"/></svg></span><span class="sr-only">Rated 4 out of 5</span> <span class="product-card__reviews">(128)</span></p>
            <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</button>
    </article>
    <article class="product-card">
        <div class="product-card__media">
            <img src="https://images.unsplash.com/photo-1505740420928-5e560c06d30e?auto=format&fit=crop&w=600&q=70" alt="" loading="lazy" width="600" height="600">
        </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__rating"><span class="product-card__stars" style="--rating: 5;" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" focusable="false"><path d="m12 2 3.1 6.3 6.9 1-5 4.9 1.2 6.8L12 17.8 5.8 21l1.2-6.8-5-4.9 6.9-1z"/></svg><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" focusable="false"><path d="m12 2 3.1 6.3 6.9 1-5 4.9 1.2 6.8L12 17.8 5.8 21l1.2-6.8-5-4.9 6.9-1z"/></svg><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" focusable="false"><path d="m12 2 3.1 6.3 6.9 1-5 4.9 1.2 6.8L12 17.8 5.8 21l1.2-6.8-5-4.9 6.9-1z"/></svg><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" focusable="false"><path d="m12 2 3.1 6.3 6.9 1-5 4.9 1.2 6.8L12 17.8 5.8 21l1.2-6.8-5-4.9 6.9-1z"/></svg><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" focusable="false"><path d="m12 2 3.1 6.3 6.9 1-5 4.9 1.2 6.8L12 17.8 5.8 21l1.2-6.8-5-4.9 6.9-1z"/></svg></span><span class="sr-only">Rated 5 out of 5</span> <span class="product-card__reviews">(128)</span></p>
            <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</button>
    </article>
</div>

Product

On this page

A product card for shop listings with an image, a sale tag, a star rating, the price and an "Add to cart" action.

Technical Details

  • The whole card is clickable through the a11y-card-link mixin, but the button stays a separate, focusable target (it sits above the overlay with z-index).
  • The star rating is decorative: the visual stars are aria-hidden, and a visually hidden text says "Rated 4.5 out of 5". The fill uses a mask driven by the --rating custom property, so fractional ratings work.
  • The old price is a <del> element with a hidden "Original price" label.
  • Product images use object-fit: cover in a square aspect-ratio box.

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.