
Each card transitions from grayscale to full color on activation and reveals a slide-up text description with a gradient overlay.
Features:
- Cards grow to 3× their base width on hover via a CSS
flextransition with 0.5s ease timing. - All cards display in grayscale at rest. The active card renders in full color on hover.
- A CSS keyframe animation runs on the image background as a loading placeholder until the actual image data arrives.
- Slide-up description reveal.
- Uses
:focus-visibleoutlines for keyboard navigation. - Cards stack vertically below the 468px breakpoint.
- A
prefers-reduced-motionmedia query disables all transitions and the shimmer animation. - GPU-composited rendering.
Use Cases:
- Showcase project thumbnails in a compact space.
- Display multiple product highlights interactively.
- Reveal employee biographies on hover.
- Feature top posts with expandable cover images.
How To Use It:
1. Place the .cards container in your markup. Each .card element holds a link, an image, a title badge, and a description paragraph. The anchor wraps all inner content so the full card surface is clickable.
<!-- .cards: the outer flex container for all accordion cards -->
<div class="cards">
<!-- Card 1: replace href and img src with your own content -->
<div class="card">
<!-- target="_blank" opens the destination in a new tab -->
<!-- rel="noopener noreferrer" blocks tab-napping security issues -->
<a href="https://example.com/alpine-lake" target="_blank" rel="noopener noreferrer">
<!-- loading="lazy" defers this image until it's near the viewport -->
<img
src="https://picsum.photos/id/1015/800/600"
alt="Alpine lake surrounded by snow-capped mountains"
loading="lazy"
/>
<!-- .card-title: centered badge, always visible at rest -->
<div class="card-title">Alpine Lake</div>
<!-- .card-desc: hidden below frame at rest, slides up on hover -->
<p class="card-desc">A still lake nestled between glacier peaks at dawn.</p>
</a>
</div>
<!-- Card 2 -->
<div class="card">
<a href="https://example.com/northern-lights" target="_blank" rel="noopener noreferrer">
<img
src="https://picsum.photos/id/1022/800/600"
alt="Aurora borealis over a dark Arctic forest"
loading="lazy"
/>
<div class="card-title">Northern Lights</div>
<p class="card-desc">Green and violet aurora bands crossing a polar sky.</p>
</a>
</div>
<!-- Card 3 -->
<div class="card">
<a href="https://example.com/forest-trail" target="_blank" rel="noopener noreferrer">
<img
src="https://picsum.photos/id/1035/800/600"
alt="Sunlit trail through a dense pine forest"
loading="lazy"
/>
<div class="card-title">Forest Trail</div>
<p class="card-desc">Morning light filtering through tall conifers on a quiet path.</p>
</a>
</div>
... more cards here
</div>
2. Copy the full stylesheet into your project.
/* ============================================================
BODY LAYOUT
Full-viewport centering via flexbox.
The layered gradient background is pure CSS — no images.
============================================================ */
body {
margin: 0;
min-height: 100vh;
/* Centers the card strip horizontally and vertically */
display: flex;
align-items: center;
justify-content: center;
/* Multi-layer conic and linear gradients simulate a 3D
grayscale panel effect. All layers live in a single
background shorthand declaration. */
background: repeating-linear-gradient(90deg, #0002 0 1px, #0000 0 5vw) -2.5vw 0 /
100% 60%,
linear-gradient(#0002, #0000 30% 70%, #0001) 50% 0% / 100% 60%,
linear-gradient(hsl(0deg 0% 70%) 0 0) 50% 0% / 5vw 60%,
linear-gradient(hsl(0deg 0% 73%) 0 0) 50% 0% / 15vw 60%,
linear-gradient(hsl(0deg 0% 76%) 0 0) 50% 0% / 25vw 60%,
linear-gradient(hsl(0deg 0% 79%) 0 0) 50% 0% / 35vw 60%,
linear-gradient(hsl(0deg 0% 82%) 0 0) 50% 0% / 45vw 60%,
linear-gradient(hsl(0deg 0% 85%) 0 0) 50% 0% / 55vw 60%,
linear-gradient(hsl(0deg 0% 88%) 0 0) 50% 0% / 65vw 60%,
linear-gradient(hsl(0deg 0% 91%) 0 0) 50% 0% / 75vw 60%,
linear-gradient(hsl(0deg 0% 94%) 0 0) 50% 0% / 85vw 60%,
linear-gradient(hsl(0deg 0% 96%) 0 0) 50% 0% / 95vw 60%,
linear-gradient(hsl(0deg 0% 98%) 0 0) 50% 0% / 105vw 60%,
radial-gradient(100vw 50vw, #0002, #0000),
conic-gradient(
from 170.5deg at 50% calc(60% - 14.8vw),
hsl(0deg 0% 70%) 19deg, #0000 0
) 0 100% / 100vw 100%,
conic-gradient(from 153deg at 50% calc(60% - 14.8vw), hsl(0deg 0% 73%) 54deg, #0000 0),
conic-gradient(from 140deg at 50% calc(60% - 14.8vw), hsl(0deg 0% 76%) 80deg, #0000 0),
conic-gradient(from 130deg at 50% calc(60% - 14.8vw), hsl(0deg 0% 79%) 100deg, #0000 0),
conic-gradient(from 123deg at 50% calc(60% - 14.8vw), hsl(0deg 0% 82%) 114deg, #0000 0),
conic-gradient(from 118.5deg at 50% calc(60% - 14.8vw), hsl(0deg 0% 85%) 123deg, #0000 0),
conic-gradient(from 114.5deg at 50% calc(60% - 14.8vw), hsl(0deg 0% 88%) 131deg, #0000 0),
conic-gradient(from 111.5deg at 50% calc(60% - 14.8vw), hsl(0deg 0% 91%) 137deg, #0000 0),
conic-gradient(from 109.2deg at 50% calc(60% - 14.8vw), hsl(0deg 0% 94%) 141.6deg, #0000 0),
conic-gradient(from 107.3deg at 50% calc(60% - 14.8vw), hsl(0deg 0% 96%) 145.3deg, #0000 0),
hsl(0deg 0% 98%);
background-repeat: no-repeat;
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
color: #111;
}
/* ============================================================
CARDS CONTAINER
Horizontal flex strip with a fixed height.
min() gives responsive width with no media query breakpoint.
============================================================ */
.cards {
display: flex;
flex-wrap: nowrap; /* Keeps all cards on one horizontal row */
width: min(1170px, 80vw); /* Responsive: 80vw wide, max 1170px */
height: 400px;
overflow: hidden;
border-radius: 1rem;
box-shadow: 0 26px 70px rgba(0, 0, 0, 0.45),
0 0 0 1px rgba(255, 255, 255, 0.5);
white-space: nowrap;
z-index: 2;
}
/* ============================================================
INDIVIDUAL CARD
Equal flex share at rest: flex 1 1 0.
Expands to flex 3 on hover — the accordion effect.
============================================================ */
.card {
flex: 1 1 0; /* Each card gets an equal width share at rest */
min-width: 0; /* Prevents any card from overflowing the container */
height: 100%;
cursor: pointer;
transition: flex 0.5s ease; /* Animates the accordion expand/collapse */
overflow: hidden;
position: relative;
will-change: flex; /* GPU layer hint for smooth flex transitions */
}
.card:hover {
flex: 3; /* Hovered card grows to 3× the base flex share */
box-shadow: 0 0 15px rgba(0, 0, 0, 0.7);
}
/* Full-surface anchor: the entire card area is clickable */
.card a {
display: block;
width: 100%;
height: 100%;
text-decoration: none;
color: inherit;
}
/* ============================================================
CARD IMAGE
Starts desaturated. Transitions to full color and 1.2× zoom.
Background gradient + shimmer animation acts as a CSS
loading skeleton until the image data arrives.
============================================================ */
.card img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
transition: transform 0.5s ease, filter 0.5s ease;
filter: grayscale(100%); /* Desaturated at rest */
will-change: transform, filter; /* GPU compositing hint for both properties */
/* Shimmer gradient: visible as a loading placeholder */
background: linear-gradient(90deg, #333 25%, #444 50%, #333 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite; /* Runs until real image covers the background */
}
/* Sweeps a lighter band left across the loading placeholder */
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
.card:hover img {
transform: scale(1.2); /* Zoom in on the hovered card */
filter: grayscale(0%); /* Switch from grayscale to full color */
}
/* ============================================================
GRADIENT OVERLAY (::after pseudo-element)
Fades in on hover to darken the lower image area.
z-index 1: bottom layer of the content stack.
pointer-events: none keeps it from blocking link clicks.
============================================================ */
.card::after {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(to top, rgba(0, 0, 0, 0.5), transparent 50%);
opacity: 0; /* Invisible at rest */
transition: opacity 0.3s ease;
z-index: 1;
pointer-events: none; /* Overlay does not intercept mouse events */
}
.card:hover::after {
opacity: 1; /* Fades in on hover */
}
/* ============================================================
CARD TITLE
Centered badge. Color-inverts on hover.
z-index 2: sits above the overlay layer.
============================================================ */
.card-title {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%); /* Perfect center alignment */
background: rgba(0, 0, 0, 0.7);
color: #fff;
padding: 10px 15px;
border-radius: 8px;
font-size: 1.1rem;
transition: background 0.3s ease, transform 0.3s ease;
z-index: 2;
}
.card:hover .card-title {
background: rgba(255, 255, 255, 0.8);
color: #000;
transform: translate(-50%, -50%) scale(1.1); /* Slight scale-up on hover */
}
/* ============================================================
CARD DESCRIPTION
Positioned at the card bottom, translated 100% downward at rest.
Slides into view on hover.
z-index 3: top layer of the content stack.
============================================================ */
.card-desc {
position: absolute;
z-index: 3;
left: 0;
right: 0;
bottom: 0;
margin: 0;
transform: translateY(100%); /* Hidden below the visible card area at rest */
transition: transform 0.3s ease;
padding: 0.5rem 1rem;
color: #fff;
font-size: 0.9rem;
background: linear-gradient(to top, rgba(0, 0, 0, 0.8), transparent);
}
.card:hover .card-desc {
transform: translateY(0); /* Slides up into the card frame on hover */
}
/* ============================================================
KEYBOARD ACCESSIBILITY
-focus-visible shows an outline only on keyboard navigation.
Mouse focus does not trigger the ring.
============================================================ */
.card a:focus-visible {
outline: 2px solid #fff;
outline-offset: 4px;
}
.card a:focus-visible img {
filter: grayscale(0%); /* Full color on keyboard focus */
}
/* ============================================================
MOBILE LAYOUT (max-width: 468px)
Cards stack vertically. All visual states stay active by
default. Mobile browsers do not fire :hover on tap — this
block compensates for that.
============================================================ */
@media (max-width: 468px) {
.cards {
flex-direction: column;
height: auto;
}
.card {
flex: none;
width: 100%;
height: 250px;
}
.card:hover {
flex: none;
box-shadow: 0 0 15px rgba(0, 0, 0, 0.7);
}
.card::after { opacity: 1; } /* Overlay always on */
.card-desc { transform: translateY(0); } /* Description always visible */
.card img { filter: grayscale(0%); } /* Full color always on */
}
/* ============================================================
TOUCH SUPPORT
-active states simulate hover feedback on tap for
touchscreens where :hover does not fire.
============================================================ */
@media (max-width: 468px) and (hover: none) {
.card:active { flex: 2; }
.card:active img { transform: scale(1.1); }
}
/* ============================================================
REDUCED MOTION
Disables all transitions and the shimmer animation for users
who have enabled "Reduce Motion" in their OS settings.
============================================================ */
@media (prefers-reduced-motion: reduce) {
.card,
.card img,
.card-title,
.card-desc,
.card::after {
transition: none;
}
.card img {
animation: none; /* Stops the shimmer loading placeholder entirely */
}
}3. The default expanded card occupies 3× the flex share of a collapsed card. Change the `flex` value in `.card:hover` to tune this ratio for your card count.
/* More dramatic expand — works well with 3 or 4 cards */
.card:hover {
flex: 4;
}
/* Tighter expand — keeps cards readable with 6 or more items */
.card:hover {
flex: 2;
}4. Update the .cards height to match your image proportions.
/* Taller strip for portrait-oriented images */
.cards {
height: 520px;
}
/* Shorter strip for a compact header gallery */
.cards {
height: 280px;
}5. All transition durations are set per-selector. Edit them directly.
/* Slower flex expand — gives a more deliberate, editorial feel */
.card {
transition: flex 0.8s ease;
}
/* Slower image zoom and color reveal */
.card img {
transition: transform 0.8s ease, filter 0.8s ease;
}
/* Faster overlay and description appear */
.card::after {
transition: opacity 0.15s ease;
}
.card-desc {
transition: transform 0.15s ease;
}FAQs:
Q: The shimmer animation keeps running after the image loads. Is this a bug?
A: It’s expected behavior. The shimmer runs as a CSS @keyframes animation on the img element’s background gradient. Once the browser paints the image, the actual pixel data covers the background layer. The animation continues underneath and becomes invisible. If the shimmer is still visible after load, either the image URL returned an error or the img element lacks width: 100%; height: 100% to fully cover its own background. Check the network tab for failed requests first.
Q: How many cards can I add to the strip?
A: The layout adapts to any count. With 8 or more cards, each collapsed card becomes very narrow and the title badge text may overflow. At that point, reduce font-size on .card-title, lower the flex: 3 expand ratio, or increase the container width in the min() function to keep the layout readable at your target count.
Q: The description overlaps the title on short cards. How do I fix it?
A: The title centers vertically at top: 50%. The description slides up from the card bottom. On short cards or with long description text, the two elements collide. Move the title to the card top by setting top: 1rem on .card-title and removing the vertical centering transform. The bottom half of the card stays clear for the description.
Alternatives:
The post Responsive Expandable Cards: CSS Accordion Slider with Hover Expand Animation appeared first on CSS Script.
Discover more from RSS Feeds Cloud
Subscribe to get the latest posts sent to your email.
