---
name: floating-product-cards
description: Use when you want "Commercial, premium, modern" - Uses product cards with shadows, depth, and subtle floating motion.
---

# Floating Product Cards

> **Category:** Card / Layout  -  **Personality:** Commercial, premium, modern
>
> **Best use:** E-commerce, product showcases
>
> **Ratings:** Professional 5/5 - Casual 4/5 - Exotic 3/5 - Visual impact 4/5 - Difficulty 3/5 - Performance cost 2/5

## What it is

A product grid where each card sits on a soft, layered drop shadow and **gently bobs up and down on a loop**, lifting further when you hover or focus it. The continuous vertical motion plus a separate "ground" shadow that pulses in sync makes the cards read as if they are levitating just above the page. It is a calm, premium way to draw the eye across a storefront without the showiness of a 3D tilt — the cards stay flat-on, they just float. Reach for it on shop grids, "new arrivals" rows and launch/product showcases.

## Dependencies / CDN

**None — pure CSS** for the float, lift and shadows. The demo adds a few lines of vanilla JS purely for storefront flavour (colour swatches, an add-to-cart counter, a wishlist toggle) — the effect works without it.

Fonts are optional (display serif + UI grotesk):

```html
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Hanken+Grotesk:wght@400;500;600;700;800&family=Instrument+Serif:ital@0;1&display=swap" rel="stylesheet">
```

## HTML

One product card. The key structural idea: a `.fpc-lift` wrapper owns the **hover lift**, the inner `.fpc-card` owns the **continuous bob + box-shadow**, and a sibling `.fpc-ground` is the floating contact shadow. Stagger cards with an inline `--d` (negative) delay so they drift out of phase.

```html
<article class="fpc-lift" style="--d:0s">
  <div class="fpc-card">
    <span class="fpc-flag">New</span>
    <div class="fpc-shot" style="--shot:#edeef0">
      <span class="fpc-glow" aria-hidden="true"></span>
      <img class="fpc-prod" src="/img/product-sneaker.jpg" alt="Ignite Velocity 3 running shoe">
    </div>
    <div class="fpc-body">
      <span class="fpc-cat">Performance Running</span>
      <h3 class="fpc-name">Ignite Velocity 3</h3>
      <div class="fpc-foot">
        <span class="fpc-now">$164</span>
        <button class="fpc-add" type="button">Add to cart</button>
      </div>
    </div>
  </div>
  <span class="fpc-ground" aria-hidden="true"></span>
</article>
```

## CSS

```css
/* 1) THE FLOAT — wrapper lifts on hover, card bobs forever */
.fpc-lift{ position:relative; transition:transform .5s cubic-bezier(.22,.61,.36,1); }
.fpc-lift:hover,
.fpc-lift:focus-within{ transform:translateY(-16px); }          /* extra lift */

.fpc-card{
  position:relative; background:#fff; border-radius:22px; overflow:hidden;
  /* layered shadow = believable depth (3 stacked, soft + warm-tinted) */
  box-shadow:
    0 1px 2px   rgba(60,40,30,.05),
    0 10px 22px -12px rgba(60,40,30,.16),
    0 34px 50px -28px rgba(60,40,30,.30);
  animation:fpc-bob 7s ease-in-out infinite;
  animation-delay:var(--d,0s);                                   /* stagger */
  will-change:transform; transition:box-shadow .5s;
}
.fpc-lift:hover .fpc-card,
.fpc-lift:focus-within .fpc-card{                                 /* shadow deepens */
  box-shadow:
    0 4px 8px   rgba(60,40,30,.06),
    0 22px 36px -14px rgba(60,40,30,.2),
    0 64px 84px -30px rgba(60,40,30,.42);
}

/* 2) GROUND CONTACT SHADOW — pulses opposite the bob; this sells "floating" */
.fpc-ground{
  position:absolute; left:13%; right:13%; bottom:-26px; height:34px;
  border-radius:50%; z-index:-1; filter:blur(7px);
  background:radial-gradient(ellipse at center, rgba(74,46,28,.34), rgba(74,46,28,0) 70%);
  animation:fpc-shadow 7s ease-in-out infinite; animation-delay:var(--d,0s);
}

@keyframes fpc-bob   { 0%,100%{ transform:translate3d(0,0,0) }   50%{ transform:translate3d(0,-13px,0) } }
@keyframes fpc-shadow{ 0%,100%{ transform:scaleX(1); opacity:.85 } 50%{ transform:scaleX(.8); opacity:.5 } }

/* 3) IMAGE — fills a 4:3 shot, zooms a touch on hover */
.fpc-shot{ position:relative; aspect-ratio:4/3; overflow:hidden; background:var(--shot,#eef0f1); }
.fpc-prod{ width:100%; height:100%; object-fit:cover; transition:transform .55s cubic-bezier(.22,.61,.36,1); }
.fpc-lift:hover .fpc-prod{ transform:scale(1.06); }

/* respect reduced-motion: rest flat, keep the depth */
@media (prefers-reduced-motion: reduce){
  .fpc-card,.fpc-ground{ animation:none; }
  .fpc-ground{ transform:scaleX(1); opacity:.72; }
}
```

## JavaScript

Not required for the effect. The demo's optional storefront interactivity:

```js
var stage=document.getElementById('fpc-stage');

// single-select colour swatches → update the colour label
stage.querySelectorAll('.fpc-swatches').forEach(function(group){
  group.addEventListener('click',function(e){
    var sw=e.target.closest('.fpc-sw'); if(!sw) return;
    group.querySelectorAll('.fpc-sw').forEach(function(s){ s.setAttribute('aria-pressed','false'); });
    sw.setAttribute('aria-pressed','true');
    var label=group.closest('.fpc-card').querySelector('.fpc-colorname');
    if(label && sw.dataset.name) label.textContent=sw.dataset.name;
  });
});

// add-to-cart → bump counter + brief confirmation
var count=2, badge=document.getElementById('fpc-cartcount');
stage.querySelectorAll('.fpc-add').forEach(function(btn){
  btn.addEventListener('click',function(){
    if(badge){ badge.textContent=++count; badge.classList.remove('is-bump'); void badge.offsetWidth; badge.classList.add('is-bump'); }
    btn.classList.add('is-added'); btn.textContent='Added ✓';
    clearTimeout(btn._t);
    btn._t=setTimeout(function(){ btn.classList.remove('is-added'); btn.textContent=btn.dataset.label; },1100);
  });
});
```

## How it works

- **Two transforms compose.** The float lives on a *child* (`.fpc-card` runs `fpc-bob`) and the hover lift lives on the *parent* (`.fpc-lift`). Because nested transforms add up, a card can bob continuously **and** lift on hover without the two fighting over the same `transform`.
- **`translate3d` on the bob** keeps the animation on the GPU and only ever animates `transform` (never `top`/`margin`/`box-shadow`), so it stays smooth and cheap.
- **The depth is faked with one layered `box-shadow`** — three stacked shadows from tight+dark to wide+faint mimic how a real object's shadow softens with distance. On hover the same three layers grow and darken, so the card reads as rising.
- **The detached ground shadow** is the real trick: it is a sibling that does **not** bob, but pulses (`scaleX` + `opacity`) on the same timeline. As the card rises the patch under it shrinks and fades — exactly how a contact shadow behaves when an object lifts off a surface.
- **Negative `animation-delay`** (`--d:-2.3s`, `-4.6s`) starts each card mid-cycle, so the row floats out of sync from the first frame with no startup jump.

## Customization

- **Float height / speed:** edit the `-13px` in `fpc-bob` (try `-8px` subtle → `-20px` lively) and the `7s` duration (slower = calmer, more premium).
- **Stagger:** change each card's inline `--d`. Spread the delays across the duration (0, -2.3s, -4.6s for a 7s loop) for an even ripple.
- **Elevation:** scale the three `box-shadow` layers together — bigger blur/spread + more negative offset = floats higher. Tint the shadow `rgba` warm or cool to match your palette.
- **Shape of the float:** add a few px of `rotate`/`scale` inside the `@keyframes` for a lazier drift, or give alternating cards `animation-direction:reverse`.
- **Hover lift amount:** the single `translateY(-16px)` on `.fpc-lift:hover`.

## Accessibility & performance

- Only `transform` and `opacity` are animated, so the loop is compositor-only and stays at 60fps even with many cards (hence the low performance cost). Keep `will-change:transform` on the bobbing element, not on everything.
- **`prefers-reduced-motion` is honoured** — the bob and shadow-pulse stop and the cards rest flat (the static depth shadow remains, so nothing looks broken).
- `:focus-within` mirrors `:hover`, so keyboard users get the same lift when they tab into a card. Decorative layers (`.fpc-glow`, `.fpc-ground`, the star row) are `aria-hidden`; icon-only buttons carry `aria-label`, and swatches expose state via `aria-pressed`.
- Give every product `<img>` real `width`/`height` (or `aspect-ratio` on the box) so the grid doesn't reflow as images load, and `loading="lazy"` below the fold.

## Gotchas

- **Don't put the bob and the hover lift on the same element** — the hover `transform` overrides the keyframed `transform` and the float dies on hover. Split them across the wrapper/child as shown.
- **The card needs `overflow:hidden` for the image zoom**, but that would clip the ground shadow — so the ground shadow must be a *sibling* of the card, not a child.
- A parent with `overflow:hidden` will clip the float/shadow; leave vertical breathing room (the demo's stage adds bottom padding) so a lifted card isn't cut off.
- Animating `box-shadow` directly (instead of `transform`) for the float will jank — bob with `transform` and only *transition* the shadow on hover state-changes.
- Negative `animation-delay` is what avoids a synchronized "all cards jump at once" start; without it every card bobs in lockstep.
