AAA
adrian@portfolio:~/snippets/card-shine-sweep

The premium card shine

A band of light sweeping across a card, in one pseudo-element and one gradient

try it

The metallic sweep on a premium card — a thin band of light that crosses the surface on hover — looks like a video, a sprite sheet or a shader. It is one pseudo-element with one gradient.


1. The whole effect

.card {
	position: relative;
	overflow: hidden;
}

.card::after {
	content: '';
	position: absolute;
	inset: 0;
	pointer-events: none;
	background: linear-gradient(115deg, #0000 40%, #ffffff40 50%, #0000 60%);
	translate: -100% 0;
}

.card:hover::after {
	translate: 100% 0;
	transition: translate 800ms ease;
}

Six declarations. Everything else is tuning.


2. Reading the gradient

linear-gradient(115deg, #0000 40%, #ffffff40 50%, #0000 60%)

The band is mostly nothing:

  • #0000 — transparent, from the start to 40%
  • #ffffff40 — white at 25% alpha, at exactly 50%
  • #0000 — transparent again from 60% on

So 80% of the layer is invisible and the light lives in a 20% strip. Narrow the strip (45% / 50% / 55%) and the sweep reads as a hard glint; widen it (30% / 50% / 70%) and it reads as a soft wash.

115deg tilts it. A vertical band (90deg) looks like a scanline; the tilt is what makes it read as a reflection, because real light hits a card at an angle.

Why #0000 and not transparent. They are the same colour today, but the four-digit hex says what you mean: transparent is rgb(0 0 0 / 0), pure black with no alpha, and in the days before premultiplied interpolation a gradient from white to transparent faded through grey. Modern browsers interpolate in premultiplied space and the grey is gone, but #0000 next to #ffffff40 also keeps the stop list readable: same notation, same length, one glance.


3. Why translate and not background-position

Both move the band. Only one is free.

Property What the browser does
background-position repaints the element every frame
translate moves an existing layer on the compositor

translate as its own property (not inside transform) also leaves transform free for a rotate or a scale on hover, with no transform: translateX(...) rotate(...) string to keep in sync.

The band starts at translate: -100% 0, fully parked to the left of the card, and travels to +100%, fully parked to the right. It is never half-visible at rest.


4. The three things that break it

No overflow: hidden. The band is parked outside the card; without the clip you see a white diagonal floating next to it. The clip is not decoration, it is the effect.

The return trip. Put the transition on .card::after and the band crawls back across the card when the pointer leaves, which looks like an accident. Put it on .card:hover::after instead: the hover state animates, the rest state has no transition, and the band snaps back to its parking spot invisibly.

The pseudo-element eats clicks. It covers the whole card with inset: 0, so a link or a button underneath stops responding. pointer-events: none hands the clicks back.


5. Making it real

Hover is not the only trigger, and a decoration should never be mandatory:

.card:hover::after,
.card:focus-visible::after {
	translate: 100% 0;
	transition: translate 800ms ease;
}

@media (prefers-reduced-motion: reduce) {
	.card:hover::after,
	.card:focus-visible::after {
		transition: none;
	}
}

:focus-visible gives it to keyboard users. The reduced-motion block removes the travel for anyone who asked for less movement — the card still works, it just does not shine.

On a touch screen there is no hover at all, so treat the sweep as a bonus on pointer devices rather than as the thing that tells the user the card is interactive.


6. Final result

.card {
	position: relative;
	overflow: hidden;
	border-radius: 0.75rem;
}

.card::after {
	content: '';
	position: absolute;
	inset: 0;
	pointer-events: none;
	background: linear-gradient(115deg, #0000 40%, #ffffff40 50%, #0000 60%);
	translate: -100% 0;
}

.card:hover::after,
.card:focus-visible::after {
	translate: 100% 0;
	transition: translate 800ms ease;
}

The demo above uses a lower alpha (#ffffff26) because the site runs on a dark blue ground; on a near-black card, 40 is the right amount.


✅ Simple rule: A sweep is a mostly transparent gradient that moves. Keep the movement on the compositor and the transition on the hover state, not on the element.

ADRIAN-ALVAREZ PORTFOLIO TERMINAL v3.0

(C) 2026 ADRIAN ALVAREZ. ALL RIGHTS RESERVED.

CPU 6502 @ 2.80MHz ........ OK

RAM 64K ................... OK

LOADING PROFILE ........... OK