When to use it
Any card that is clickable should respond to the pointer, and a small lift is the response that reads as "this is a thing you can pick up". The specimen above shows the resting state. The lifted state is the same card with a deeper shadow and a two pixel rise, and the interesting part of this example is not either shadow but the way the site moves between them, because the obvious way is the wrong way.
How it is built
Two shadows, both painted once. The card carries the resting shadow. A ::after pseudo-element, the same size as the card and behind it, carries the lifted shadow at opacity: 0. Hover fades the pseudo-element in and nudges the card up with a transform:
.card {
position: relative;
isolation: isolate;
border-radius: 12px;
background: #ffffff;
box-shadow: 0 1px 2px rgba(27, 35, 64, 0.06); /* rest */
transition: transform 200ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.card::after {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
box-shadow: 0 8px 24px rgba(27, 35, 64, 0.12); /* lift */
opacity: 0;
z-index: -1;
pointer-events: none;
transition: opacity 200ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.card:hover,
.card:focus-within { transform: translateY(-2px); }
.card:hover::after,
.card:focus-within::after { opacity: 1; }Every hover lift on this site, the docs cards, the related links, the console panel's buttons, is this pattern with the site's shadow tokens in place of the literal values. The rest and lift states are both available in the generator: the specimen link above opens rest, and this one opens the lifted stack:
Why not transition box-shadow
Tobias Ahlin's article on animating shadows makes the point in its first line: "Animating a change of box-shadow will hurt performance." The web.dev animations guide gives the mechanism: only opacity and transform stay "on the compositing stage"; everything else triggers paint, and "anything that involves a blur (like a shadow, for example) takes longer to paint than drawing a red box". A 200 millisecond shadow transition is twelve repaints of a blurred rectangle. Multiply by every card the pointer crosses on the way down a grid, on a phone, and the lag is visible. The crossfade costs one paint per shadow at load and nothing per hover.
Notes and limits
The pseudo-element has to be behind the card and inside it at once, which is what z-index: -1 plus isolation: isolate on the card achieves; without the isolation, the pseudo-element drops behind the page background and disappears. If the card already uses ::after for something else, use ::before or a real child. Include :focus-within so keyboard users get the same lift. And do not scale the card on hover: scaling a painted shadow blurs it a second time and it goes soft in the wrong way. Two pixels of translate is plenty.
The code in four formats
Straight from the generator's readout for this state. Copy the one your project speaks; the output formats guide explains where each goes.
box-shadow:
0 1px 2px rgba(27, 35, 64, 0.06),
0 8px 24px rgba(27, 35, 64, 0.12);
border-radius: 12px;$shadow:
0 1px 2px rgba(27, 35, 64, 0.06),
0 8px 24px rgba(27, 35, 64, 0.12);
.element {
box-shadow: $shadow;
border-radius: 12px;
}<div class="shadow-[0_1px_2px_rgba(27,35,64,0.06),_0_8px_24px_rgba(27,35,64,0.12)] rounded-[12px]"></div>
/* Or name it once in your theme (Tailwind v4): */
@theme {
--shadow-custom: 0 1px 2px rgba(27, 35, 64, 0.06), 0 8px 24px rgba(27, 35, 64, 0.12);
}
/* then use class="shadow-custom" */.element
box-shadow 0 1px 2px rgba(27, 35, 64, 0.06), 0 8px 24px rgba(27, 35, 64, 0.12)
border-radius 12pxFrequently asked questions
How do I add a hover shadow to a card in CSS?
Give the card its resting shadow, put the hover shadow on a ::after pseudo-element at opacity 0 behind the card, and on :hover transition the opacity to 1 and translate the card up two pixels. Both shadows are painted once; the hover only changes opacity and transform.
Why should I not use transition: box-shadow?
Because box-shadow is painted, not composited, so a transition repaints the element and its blur on every frame. On a grid of cards on a mid-range phone that shows up as stutter and a worse Interaction to Next Paint score. Opacity and transform are handled by the GPU.
What should the rest and hover shadows be?
Rest: 0 1px 2px at 6 per cent, so the card is barely lifted. Hover: 0 8px 24px at 12 per cent, so it clearly rises. Both tinted to the page. The difference between them is what the user reads as a response.
Does the pseudo-element need z-index: -1?
Yes, with isolation: isolate or another stacking context on the card, so the pseudo-element sits behind the card's background but stays inside the card's stacking context rather than falling behind the page.