DEV Community

Cover image for Pure CSS Shine Animation & Corner Cut-Out: Shimmering Cards Without JS
Emrah G.
Emrah G.

Posted on

Pure CSS Shine Animation & Corner Cut-Out: Shimmering Cards Without JS

Shimmering cards with a diagonal shine + a rosette-friendly corner cut-out — built only with CSS.

Modern UI often needs subtle flair without sacrificing performance. In this guide we’ll combine two small but high-impact techniques:

  1. A shine animation that softly glides across the card on hover.
  2. A corner cut-out that creates space for a “Popular”/“Pro” badge.

Both are pure CSS, accessible, and production-friendly.


TL;DR

  • Animate a large, rotated overlay gradient with transform for smooth GPU-accelerated motion.
  • Create a corner cut-out using either a pseudo-element + box-shadow trick (broad compatibility) or a CSS mask (cleaner, modern).
  • Respect prefers-reduced-motion and ensure readable contrast.

Minimal HTML Skeleton

<div class="card">
  <div class="shine" aria-hidden="true"></div>
  <div class="badge">Popular</div>
  <div class="content">
    <!-- your card content -->
  </div>
  <div class="cutout" aria-hidden="true"></div>
</div>
Enter fullscreen mode Exit fullscreen mode

No frameworks needed — we’ll focus on CSS only.

The Shine Animation

The shine is a large diagonal band moving across the card. We render it as an absolutely positioned overlay, then translate it from corner to corner.

.card {
  position: relative;
  overflow: hidden;
  border-radius: 22px;
  background: linear-gradient(33deg, #dff69b 0%, #95e79b 35%, #80de8e 70%, #a9f9b2 100%);
}

/* the moving band */
.shine {
  position: absolute;
  inset: 0;
  width: 200%;
  height: 200%;
  pointer-events: none;
  opacity: 0;
  transition: opacity 300ms ease;
  background: linear-gradient(
    180deg,
    rgba(248,255,228,0) 0%,
    rgba(248,255,228,0.8) 50%,
    rgba(248,255,228,0) 100%
  );
  transform: translate(-100%, -100%) rotate(45deg);
  z-index: 1;
}

.card:hover .shine {
  opacity: 1;
  animation: shine 3s linear infinite;
}

@keyframes shine {
  from { transform: translate(-100%, -100%) rotate(45deg); }
  to   { transform: translate(200%,  200%)  rotate(45deg); }
}
Enter fullscreen mode Exit fullscreen mode

Why width/height: 200% and rotate(45deg)?

200% size ensures the band covers the card completely during the whole travel — no clipped edges.

45° creates a neat diagonal “sweep” that feels natural on rectangular cards.

Performance notes

Animate transform, not background-position. transform leverages GPU and remains smooth.

Keep the overlay non-interactive with pointer-events: none.

Consider will-change: transform only if you actually see performance hiccups.

Reduced motion

@media (prefers-reduced-motion: reduce) {
  .card:hover .shine {
    animation: none;
    opacity: .35; /* static highlight */
  }
}
Enter fullscreen mode Exit fullscreen mode

The Corner Cut-Out

We’ll cover two approaches:

A) Pseudo-element + Box-Shadow Trick (robust, easy to theme)

We fake an “erased” corner by placing a rounded square pseudo-element and pushing a white (or page background) shadow inward, so it looks like a chunk is missing.

.cutout {
  --corner-size: 32px;
  --corner-radius: 14px;
  --offset-right: 20px;
  --offset-top: 3px;
  --shadow-1: 1px;
  --shadow-2: 4px;

  position: absolute;
  top: var(--offset-top);
  right: var(--offset-right);
  width: 85px;  /* total cut-out area (helps control the composition) */
  height: 38px;
  pointer-events: none;
  background: transparent;
  overflow: hidden;
  z-index: 2; /* above the gradient bg, below content if needed */
}

.cutout::before,
.cutout::after {
  content: "";
  position: absolute;
  width: var(--corner-size);
  height: var(--corner-size);
  top: 1px;
  right: 20px;
  border-bottom-left-radius: var(--corner-radius);
  background: transparent;
}

.cutout::before { box-shadow: -1px 1px white; }
.cutout::after  { box-shadow: -4px 5px white; }
Enter fullscreen mode Exit fullscreen mode

If your page background isn’t white, replace white with a variable that matches the actual outer background.

B) Real Hole with CSS Mask (clean, modern)

Masks let you truly subtract pixels so the page background shows through.

.card {
  --r: 14px; /* radius */
  -webkit-mask: radial-gradient(
    circle var(--r) at right var(--r) top var(--r),
    transparent calc(var(--r) - 0.5px),
    #000       calc(var(--r) + 0.5px)
  );
  mask: radial-gradient(
    circle var(--r) at right var(--r) top var(--r),
    transparent calc(var(--r) - 0.5px),
    #000       calc(var(--r) + 0.5px)
  );
}
Enter fullscreen mode Exit fullscreen mode

The tiny 0.5px buffer helps anti-aliasing on different pixel densities. Test on Safari; older versions might need the -webkit- prefix only.

Layering: Z-Index & Hit Testing

Keep the content above backgrounds (z-index > shine/cut-out).

The shine should be visually above the background but not clickable.

For the badge, place it near the cut-out and ensure it sits above the shine (e.g., z-index: 3).

.badge {
  position: absolute;
  top: 10px;
  right: 20px;
  font: 600 12px/1 system-ui, sans-serif;
  text-transform: uppercase;
  z-index: 3;
}
.content { position: relative; z-index: 2; }
Enter fullscreen mode Exit fullscreen mode

Theming with CSS Variables

Centralize constants for easy design tweaks:

:root {
  --radius: 22px;
  --badge-h: 35px;
  --badge-w: 95px;

  /* cut-out */
  --corner-size: 32px;
  --corner-radius: 14px;

  /* shine */
  --shine-angle: 45deg;
  --shine-duration: 3s;
}
Enter fullscreen mode Exit fullscreen mode

Common Pitfalls & Fixes

Band “tears” at edges: Ensure overlay is bigger than the card (200%) and fully traverses the diagonal.

Gradient banding: Use 3+ color stops or add a subtle translucent white layer (as in the examples) to smooth it out.

Wrong cut-out color: For the pseudo-element trick, make the shadow color match the actual page background.

Mask aliasing in Safari: Use a tiny ±0.5px tolerance around the radius.

Copy-Paste Snippets

Minimal Shine

.card { position: relative; overflow: hidden; border-radius: 22px; }
.shine {
  position: absolute; inset: 0; width: 200%; height: 200%;
  background: linear-gradient(180deg, transparent, rgba(255,255,255,.8), transparent);
  transform: translate(-100%,-100%) rotate(45deg);
  opacity: 0; pointer-events: none;
}
.card:hover .shine { opacity: 1; animation: shine 3s linear infinite; }
@keyframes shine {
  from { transform: translate(-100%,-100%) rotate(45deg); }
  to   { transform: translate(200%,200%)  rotate(45deg); }
}
Enter fullscreen mode Exit fullscreen mode

Minimal Cut-Out (pseudo-element)

.cutout{position:absolute;top:3px;right:40px;width:85px;height:38px;pointer-events:none}
.cutout::before,.cutout::after{
  content:"";position:absolute;top:1px;right:20px;width:32px;height:32px;border-bottom-left-radius:14px;background:transparent
}
.cutout::before{box-shadow:-1px 1px white}
.cutout::after{box-shadow:-4px 5px white}
Enter fullscreen mode Exit fullscreen mode

Minimal Cut-Out (mask)

.card{
  --r:14px;
  -webkit-mask:radial-gradient(circle var(--r) at right var(--r) top var(--r),
    transparent calc(var(--r) - .5px), #000 calc(var(--r) + .5px));
          mask:radial-gradient(circle var(--r) at right var(--r) top var(--r),
    transparent calc(var(--r) - .5px), #000 calc(var(--r) + .5px));
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)