Entry 04
Shadow change that fades and does not repaint
Use it on cards that lift on hover, most of all in a grid of many cards or with a large blur.
Code
.card {
position: relative;
isolation: isolate;
}
.card::before,
.card::after {
content: "";
position: absolute;
inset: 0;
z-index: -1;
border-radius: inherit;
pointer-events: none;
}
.card::before {
box-shadow:
0 1px 4px oklch(0.3 0.05 250 / 0.1),
0 2px 10px oklch(0.3 0.05 250 / 0.08);
}
.card::after {
box-shadow:
0 4px 8px oklch(0.3 0.05 250 / 0.06),
0 12px 32px oklch(0.3 0.05 250 / 0.16);
opacity: 0;
}
@media (hover: hover) and (pointer: fine) {
.card:hover::before { opacity: 0; }
.card:hover::after { opacity: 1; }
}
@media (prefers-reduced-motion: no-preference) {
.card::before,
.card::after { transition: opacity 0.3s var(--ease-out, ease-out); }
}Why it works, the rules and the credits
Why it works
- A
box-shadowtransition repaints the shadow on every frame. Each shadow here is painted once, and the transition changes onlyopacity, which the compositor handles. In a Chrome trace of one card it took 12 paint events, against about 78 for a plainbox-shadowtransition. - The two shadows cross-fade, so the end state is exactly the raised shadow and not the sum of both.
isolation: isolategives the card its own stacking context.z-index: -1then puts the pseudo-elements above the card's background and below its content.
Rules
- The card needs its own background. The pseudo-elements have none.
- It uses both pseudo-elements of the card. Hit area larger than the visual uses
::afterfor the hit area and Whole card clickable from one link uses it for the link, so do not combine them on one element. - For a single small element, transition
box-shadowdirectly. The saving is real but small per card.
Support
Every browser.
Credits
Borrowed from: Stripe, on its resource cards · Tobias Ahlin, How to animate box-shadow, for the older form of the idea