30 CSS Shake Animations18 / 30
CSS Card Shake Effect on Hover
Card-scale shake in two temperaments: a polaroid that jiggles like it was just picked off a desk (rotation around its center, shadow deepening as it lifts), and a glass feature tile that shivers in 3D — rotateY jitter inside a perspective parent with a specular highlight racing across on the same clock.
Published
The code
<div class="shk-18-group">
<link href="https://fonts.googleapis.com/css2?family=Caveat:wght@500;600&family=Manrope:wght@500..800&display=swap" rel="stylesheet">
<section class="shk-18a" aria-label="Polaroid photos that jiggle on hover">
<div class="shk-18a__desk">
<figure class="shk-18a__pol" style="--r:-2.5deg">
<img class="shk-18a__img" src="https://picsum.photos/id/1025/360/300" alt="A pug wrapped in a blanket" loading="lazy">
<figcaption class="shk-18a__cap">office manager, probably</figcaption>
</figure>
<figure class="shk-18a__pol" style="--r:1.8deg">
<img class="shk-18a__img" src="https://picsum.photos/id/1015/360/300" alt="A river winding through mountains" loading="lazy">
<figcaption class="shk-18a__cap">offsite, day 2</figcaption>
</figure>
<figure class="shk-18a__pol" style="--r:3.2deg">
<img class="shk-18a__img" src="https://picsum.photos/id/1011/360/300" alt="A person canoeing on a lake" loading="lazy">
<figcaption class="shk-18a__cap">shipping v1 felt like this</figcaption>
</figure>
</div>
<p class="shk-18a__hint" aria-hidden="true">hover — transition lifts, animation jiggles; separate properties, zero conflict</p>
</section>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400..700&display=swap" rel="stylesheet">
<section class="shk-18b" aria-label="Glass feature tile with 3D shiver on hover">
<div class="shk-18b__scene">
<a class="shk-18b__tile" href="#" onclick="return false">
<p class="shk-18b__chip">New · Motion engine</p>
<h2 class="shk-18b__title">Shake, physically.</h2>
<p class="shk-18b__sub">Spring-based micro-interactions, now in the design kit.</p>
<span class="shk-18b__go" aria-hidden="true">Explore →</span>
</a>
</div>
<p class="shk-18b__hint" aria-hidden="true">hover — rotateY jitters ±2° inside a 900px perspective while the highlight sweeps on the same clock</p>
</section>
</div><div class="shk-18-group">
<link href="https://fonts.googleapis.com/css2?family=Caveat:wght@500;600&family=Manrope:wght@500..800&display=swap" rel="stylesheet">
<section class="shk-18a" aria-label="Polaroid photos that jiggle on hover">
<div class="shk-18a__desk">
<figure class="shk-18a__pol" style="--r:-2.5deg">
<img class="shk-18a__img" src="https://picsum.photos/id/1025/360/300" alt="A pug wrapped in a blanket" loading="lazy">
<figcaption class="shk-18a__cap">office manager, probably</figcaption>
</figure>
<figure class="shk-18a__pol" style="--r:1.8deg">
<img class="shk-18a__img" src="https://picsum.photos/id/1015/360/300" alt="A river winding through mountains" loading="lazy">
<figcaption class="shk-18a__cap">offsite, day 2</figcaption>
</figure>
<figure class="shk-18a__pol" style="--r:3.2deg">
<img class="shk-18a__img" src="https://picsum.photos/id/1011/360/300" alt="A person canoeing on a lake" loading="lazy">
<figcaption class="shk-18a__cap">shipping v1 felt like this</figcaption>
</figure>
</div>
<p class="shk-18a__hint" aria-hidden="true">hover — transition lifts, animation jiggles; separate properties, zero conflict</p>
</section>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400..700&display=swap" rel="stylesheet">
<section class="shk-18b" aria-label="Glass feature tile with 3D shiver on hover">
<div class="shk-18b__scene">
<a class="shk-18b__tile" href="#" onclick="return false">
<p class="shk-18b__chip">New · Motion engine</p>
<h2 class="shk-18b__title">Shake, physically.</h2>
<p class="shk-18b__sub">Spring-based micro-interactions, now in the design kit.</p>
<span class="shk-18b__go" aria-hidden="true">Explore →</span>
</a>
</div>
<p class="shk-18b__hint" aria-hidden="true">hover — rotateY jitters ±2° inside a 900px perspective while the highlight sweeps on the same clock</p>
</section>
</div>.shk-18-group {
display: flex;
flex-wrap: wrap;
align-items: stretch;
width: 100%;
}
.shk-18-group > section {
flex: 1 1 480px;
min-width: min(100%,360px);
}
.shk-18a,
.shk-18a *,
.shk-18a *::before,
.shk-18a *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.shk-18a {
width: 100%;
min-height: 100vh;
display: grid;
place-content: center;
gap: 26px;
padding: 48px 24px;
background: #c9b99a linear-gradient(180deg,rgba(255,255,255,.16),rgba(94,74,42,.18));
font-family: 'Manrope','Segoe UI',system-ui,sans-serif;
}
.shk-18a__desk {
display: flex;
flex-wrap: wrap;
gap: 26px;
justify-content: center;
}
.shk-18a__pol {
background: #fdfcf8;
padding: 12px 12px 14px;
border-radius: 4px;
rotate: var(--r,0deg);
box-shadow: 0 12px 24px -12px rgba(50,38,20,.55);
cursor: grab;
transition: translate .25s,box-shadow .25s,scale .25s;
}
.shk-18a__pol:hover {
translate: 0 -8px;
scale: 1.03;
box-shadow: 0 34px 50px -22px rgba(50,38,20,.6);
animation: shk-18a-jiggle .8s ease-in-out .1s infinite;
}
.shk-18a__pol:active {
cursor: grabbing;
}
.shk-18a__img {
display: block;
width: min(230px,62vw);
aspect-ratio: 6/5;
object-fit: cover;
filter: saturate(1.05) contrast(1.02);
}
.shk-18a__cap {
margin-top: 10px;
text-align: center;
font-family: 'Caveat',cursive;
font-size: 19px;
color: #4a3a22;
}
.shk-18a__hint {
text-align: center;
font-size: 12px;
color: rgba(58,44,22,.6);
}
@keyframes shk-18a-jiggle {
0%,
100% {
rotate: calc(var(--r,0deg) - 1.4deg);
}
50% {
rotate: calc(var(--r,0deg) + 1.4deg);
}
}
@media (prefers-reduced-motion: reduce) {
.shk-18a__pol:hover {
animation: none;
}
}
.shk-18b,
.shk-18b *,
.shk-18b *::before,
.shk-18b *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.shk-18b {
width: 100%;
min-height: 100vh;
display: grid;
place-content: center;
gap: 22px;
padding: 48px 24px;
background: radial-gradient(120% 120% at 20% 0%,#16203a,#0b0f1d 60%);
font-family: 'Space Grotesk','Segoe UI',system-ui,sans-serif;
}
.shk-18b__scene {
perspective: 900px;
}
.shk-18b__tile {
display: block;
width: min(400px,88vw);
padding: 32px;
border-radius: 22px;
text-decoration: none;
background: linear-gradient(115deg,rgba(255,255,255,.12),rgba(255,255,255,.04) 40%,rgba(255,255,255,.14) 50%,rgba(255,255,255,.04) 60%);
background-size: 250% 100%;
background-position: 100% 0;
border: 1px solid rgba(170,200,255,.22);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
box-shadow: 0 40px 80px -40px rgba(0,0,0,.9),inset 0 1px 0 rgba(255,255,255,.12);
transition: border-color .25s;
}
.shk-18b__tile:hover {
border-color: rgba(170,200,255,.45);
animation: shk-18b-shiver 1.2s ease-in-out infinite,shk-18b-sheen 1.2s ease-in-out infinite;
}
.shk-18b__tile:focus-visible {
outline: 2px solid #9fc0ff;
outline-offset: 4px;
}
.shk-18b__chip {
display: inline-block;
font-size: 11px;
font-weight: 700;
letter-spacing: .1em;
text-transform: uppercase;
color: oklch(0.8 0.11 250);
background: oklch(0.8 0.11 250 / .12);
border: 1px solid oklch(0.8 0.11 250 / .3);
padding: 5px 11px;
border-radius: 999px;
}
.shk-18b__title {
margin-top: 16px;
font-size: 30px;
font-weight: 700;
letter-spacing: -.02em;
color: #eef3ff;
}
.shk-18b__sub {
margin-top: 9px;
font-size: 14px;
line-height: 1.6;
color: rgba(222,232,252,.6);
}
.shk-18b__go {
display: inline-block;
margin-top: 20px;
font-size: 14px;
font-weight: 700;
color: #9fc0ff;
}
.shk-18b__hint {
text-align: center;
font-size: 12px;
color: rgba(222,232,252,.4);
max-width: 440px;
justify-self: center;
}
@keyframes shk-18b-shiver {
0%,
100% {
transform: rotateY(-2deg) rotateX(.6deg);
}
25% {
transform: rotateY(2deg) rotateX(-.4deg);
}
50% {
transform: rotateY(-1.4deg) rotateX(.5deg);
}
75% {
transform: rotateY(1.6deg) rotateX(-.6deg);
}
}
@keyframes shk-18b-sheen {
0% {
background-position: 100% 0;
}
55%,
100% {
background-position: 0% 0;
}
}
@media (prefers-reduced-motion: reduce) {
.shk-18b__tile:hover {
animation: none;
}
}.shk-18-group {
display: flex;
flex-wrap: wrap;
align-items: stretch;
width: 100%;
}
.shk-18-group > section {
flex: 1 1 480px;
min-width: min(100%,360px);
}
.shk-18a,
.shk-18a *,
.shk-18a *::before,
.shk-18a *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.shk-18a {
width: 100%;
min-height: 100vh;
display: grid;
place-content: center;
gap: 26px;
padding: 48px 24px;
background: #c9b99a linear-gradient(180deg,rgba(255,255,255,.16),rgba(94,74,42,.18));
font-family: 'Manrope','Segoe UI',system-ui,sans-serif;
}
.shk-18a__desk {
display: flex;
flex-wrap: wrap;
gap: 26px;
justify-content: center;
}
.shk-18a__pol {
background: #fdfcf8;
padding: 12px 12px 14px;
border-radius: 4px;
rotate: var(--r,0deg);
box-shadow: 0 12px 24px -12px rgba(50,38,20,.55);
cursor: grab;
transition: translate .25s,box-shadow .25s,scale .25s;
}
.shk-18a__pol:hover {
translate: 0 -8px;
scale: 1.03;
box-shadow: 0 34px 50px -22px rgba(50,38,20,.6);
animation: shk-18a-jiggle .8s ease-in-out .1s infinite;
}
.shk-18a__pol:active {
cursor: grabbing;
}
.shk-18a__img {
display: block;
width: min(230px,62vw);
aspect-ratio: 6/5;
object-fit: cover;
filter: saturate(1.05) contrast(1.02);
}
.shk-18a__cap {
margin-top: 10px;
text-align: center;
font-family: 'Caveat',cursive;
font-size: 19px;
color: #4a3a22;
}
.shk-18a__hint {
text-align: center;
font-size: 12px;
color: rgba(58,44,22,.6);
}
@keyframes shk-18a-jiggle {
0%,
100% {
rotate: calc(var(--r,0deg) - 1.4deg);
}
50% {
rotate: calc(var(--r,0deg) + 1.4deg);
}
}
@media (prefers-reduced-motion: reduce) {
.shk-18a__pol:hover {
animation: none;
}
}
.shk-18b,
.shk-18b *,
.shk-18b *::before,
.shk-18b *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.shk-18b {
width: 100%;
min-height: 100vh;
display: grid;
place-content: center;
gap: 22px;
padding: 48px 24px;
background: radial-gradient(120% 120% at 20% 0%,#16203a,#0b0f1d 60%);
font-family: 'Space Grotesk','Segoe UI',system-ui,sans-serif;
}
.shk-18b__scene {
perspective: 900px;
}
.shk-18b__tile {
display: block;
width: min(400px,88vw);
padding: 32px;
border-radius: 22px;
text-decoration: none;
background: linear-gradient(115deg,rgba(255,255,255,.12),rgba(255,255,255,.04) 40%,rgba(255,255,255,.14) 50%,rgba(255,255,255,.04) 60%);
background-size: 250% 100%;
background-position: 100% 0;
border: 1px solid rgba(170,200,255,.22);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
box-shadow: 0 40px 80px -40px rgba(0,0,0,.9),inset 0 1px 0 rgba(255,255,255,.12);
transition: border-color .25s;
}
.shk-18b__tile:hover {
border-color: rgba(170,200,255,.45);
animation: shk-18b-shiver 1.2s ease-in-out infinite,shk-18b-sheen 1.2s ease-in-out infinite;
}
.shk-18b__tile:focus-visible {
outline: 2px solid #9fc0ff;
outline-offset: 4px;
}
.shk-18b__chip {
display: inline-block;
font-size: 11px;
font-weight: 700;
letter-spacing: .1em;
text-transform: uppercase;
color: oklch(0.8 0.11 250);
background: oklch(0.8 0.11 250 / .12);
border: 1px solid oklch(0.8 0.11 250 / .3);
padding: 5px 11px;
border-radius: 999px;
}
.shk-18b__title {
margin-top: 16px;
font-size: 30px;
font-weight: 700;
letter-spacing: -.02em;
color: #eef3ff;
}
.shk-18b__sub {
margin-top: 9px;
font-size: 14px;
line-height: 1.6;
color: rgba(222,232,252,.6);
}
.shk-18b__go {
display: inline-block;
margin-top: 20px;
font-size: 14px;
font-weight: 700;
color: #9fc0ff;
}
.shk-18b__hint {
text-align: center;
font-size: 12px;
color: rgba(222,232,252,.4);
max-width: 440px;
justify-self: center;
}
@keyframes shk-18b-shiver {
0%,
100% {
transform: rotateY(-2deg) rotateX(.6deg);
}
25% {
transform: rotateY(2deg) rotateX(-.4deg);
}
50% {
transform: rotateY(-1.4deg) rotateX(.5deg);
}
75% {
transform: rotateY(1.6deg) rotateX(-.6deg);
}
}
@keyframes shk-18b-sheen {
0% {
background-position: 100% 0;
}
55%,
100% {
background-position: 0% 0;
}
}
@media (prefers-reduced-motion: reduce) {
.shk-18b__tile:hover {
animation: none;
}
}/* No JavaScript — each polaroid keeps its resting pose in --r; the hover jiggle swings ±1.4° AROUND that pose via calc(), while translate/scale/shadow ride a separate transition. */ /* No JavaScript — perspective on the PARENT gives the jitter a vanishing point; rotateY/rotateX shiver and the background-position sheen share one 1.2s clock so light and motion feel like a single material. */
/* No JavaScript — each polaroid keeps its resting pose in --r; the hover jiggle swings ±1.4° AROUND that pose via calc(), while translate/scale/shadow ride a separate transition. */
/* No JavaScript — perspective on the PARENT gives the jitter a vanishing point; rotateY/rotateX shiver and the background-position sheen share one 1.2s clock so light and motion feel like a single material. */Here's a working CSS Shake Animation from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: CSS Card Shake Effect on Hover
Source: https://codefronts.com/motion/css-shake-animation/css-card-shake-effect-on-hover/
Card-scale shake in two temperaments: a polaroid that jiggles like it was just picked off a desk (rotation around its center, shadow deepening as it lifts), and a glass feature tile that shivers in 3D — rotateY jitter inside a perspective parent with a specular highlight racing across on the same clock.
## HTML
```html
<div class="shk-18-group">
<link href="https://fonts.googleapis.com/css2?family=Caveat:wght@500;600&family=Manrope:wght@500..800&display=swap" rel="stylesheet">
<section class="shk-18a" aria-label="Polaroid photos that jiggle on hover">
<div class="shk-18a__desk">
<figure class="shk-18a__pol" style="--r:-2.5deg">
<img class="shk-18a__img" src="https://picsum.photos/id/1025/360/300" alt="A pug wrapped in a blanket" loading="lazy">
<figcaption class="shk-18a__cap">office manager, probably</figcaption>
</figure>
<figure class="shk-18a__pol" style="--r:1.8deg">
<img class="shk-18a__img" src="https://picsum.photos/id/1015/360/300" alt="A river winding through mountains" loading="lazy">
<figcaption class="shk-18a__cap">offsite, day 2</figcaption>
</figure>
<figure class="shk-18a__pol" style="--r:3.2deg">
<img class="shk-18a__img" src="https://picsum.photos/id/1011/360/300" alt="A person canoeing on a lake" loading="lazy">
<figcaption class="shk-18a__cap">shipping v1 felt like this</figcaption>
</figure>
</div>
<p class="shk-18a__hint" aria-hidden="true">hover — transition lifts, animation jiggles; separate properties, zero conflict</p>
</section>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400..700&display=swap" rel="stylesheet">
<section class="shk-18b" aria-label="Glass feature tile with 3D shiver on hover">
<div class="shk-18b__scene">
<a class="shk-18b__tile" href="#" onclick="return false">
<p class="shk-18b__chip">New · Motion engine</p>
<h2 class="shk-18b__title">Shake, physically.</h2>
<p class="shk-18b__sub">Spring-based micro-interactions, now in the design kit.</p>
<span class="shk-18b__go" aria-hidden="true">Explore →</span>
</a>
</div>
<p class="shk-18b__hint" aria-hidden="true">hover — rotateY jitters ±2° inside a 900px perspective while the highlight sweeps on the same clock</p>
</section>
</div>
```
## CSS
```css
.shk-18-group {
display: flex;
flex-wrap: wrap;
align-items: stretch;
width: 100%;
}
.shk-18-group > section {
flex: 1 1 480px;
min-width: min(100%,360px);
}
.shk-18a,
.shk-18a *,
.shk-18a *::before,
.shk-18a *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.shk-18a {
width: 100%;
min-height: 100vh;
display: grid;
place-content: center;
gap: 26px;
padding: 48px 24px;
background: #c9b99a linear-gradient(180deg,rgba(255,255,255,.16),rgba(94,74,42,.18));
font-family: 'Manrope','Segoe UI',system-ui,sans-serif;
}
.shk-18a__desk {
display: flex;
flex-wrap: wrap;
gap: 26px;
justify-content: center;
}
.shk-18a__pol {
background: #fdfcf8;
padding: 12px 12px 14px;
border-radius: 4px;
rotate: var(--r,0deg);
box-shadow: 0 12px 24px -12px rgba(50,38,20,.55);
cursor: grab;
transition: translate .25s,box-shadow .25s,scale .25s;
}
.shk-18a__pol:hover {
translate: 0 -8px;
scale: 1.03;
box-shadow: 0 34px 50px -22px rgba(50,38,20,.6);
animation: shk-18a-jiggle .8s ease-in-out .1s infinite;
}
.shk-18a__pol:active {
cursor: grabbing;
}
.shk-18a__img {
display: block;
width: min(230px,62vw);
aspect-ratio: 6/5;
object-fit: cover;
filter: saturate(1.05) contrast(1.02);
}
.shk-18a__cap {
margin-top: 10px;
text-align: center;
font-family: 'Caveat',cursive;
font-size: 19px;
color: #4a3a22;
}
.shk-18a__hint {
text-align: center;
font-size: 12px;
color: rgba(58,44,22,.6);
}
@keyframes shk-18a-jiggle {
0%,
100% {
rotate: calc(var(--r,0deg) - 1.4deg);
}
50% {
rotate: calc(var(--r,0deg) + 1.4deg);
}
}
@media (prefers-reduced-motion: reduce) {
.shk-18a__pol:hover {
animation: none;
}
}
.shk-18b,
.shk-18b *,
.shk-18b *::before,
.shk-18b *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.shk-18b {
width: 100%;
min-height: 100vh;
display: grid;
place-content: center;
gap: 22px;
padding: 48px 24px;
background: radial-gradient(120% 120% at 20% 0%,#16203a,#0b0f1d 60%);
font-family: 'Space Grotesk','Segoe UI',system-ui,sans-serif;
}
.shk-18b__scene {
perspective: 900px;
}
.shk-18b__tile {
display: block;
width: min(400px,88vw);
padding: 32px;
border-radius: 22px;
text-decoration: none;
background: linear-gradient(115deg,rgba(255,255,255,.12),rgba(255,255,255,.04) 40%,rgba(255,255,255,.14) 50%,rgba(255,255,255,.04) 60%);
background-size: 250% 100%;
background-position: 100% 0;
border: 1px solid rgba(170,200,255,.22);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
box-shadow: 0 40px 80px -40px rgba(0,0,0,.9),inset 0 1px 0 rgba(255,255,255,.12);
transition: border-color .25s;
}
.shk-18b__tile:hover {
border-color: rgba(170,200,255,.45);
animation: shk-18b-shiver 1.2s ease-in-out infinite,shk-18b-sheen 1.2s ease-in-out infinite;
}
.shk-18b__tile:focus-visible {
outline: 2px solid #9fc0ff;
outline-offset: 4px;
}
.shk-18b__chip {
display: inline-block;
font-size: 11px;
font-weight: 700;
letter-spacing: .1em;
text-transform: uppercase;
color: oklch(0.8 0.11 250);
background: oklch(0.8 0.11 250 / .12);
border: 1px solid oklch(0.8 0.11 250 / .3);
padding: 5px 11px;
border-radius: 999px;
}
.shk-18b__title {
margin-top: 16px;
font-size: 30px;
font-weight: 700;
letter-spacing: -.02em;
color: #eef3ff;
}
.shk-18b__sub {
margin-top: 9px;
font-size: 14px;
line-height: 1.6;
color: rgba(222,232,252,.6);
}
.shk-18b__go {
display: inline-block;
margin-top: 20px;
font-size: 14px;
font-weight: 700;
color: #9fc0ff;
}
.shk-18b__hint {
text-align: center;
font-size: 12px;
color: rgba(222,232,252,.4);
max-width: 440px;
justify-self: center;
}
@keyframes shk-18b-shiver {
0%,
100% {
transform: rotateY(-2deg) rotateX(.6deg);
}
25% {
transform: rotateY(2deg) rotateX(-.4deg);
}
50% {
transform: rotateY(-1.4deg) rotateX(.5deg);
}
75% {
transform: rotateY(1.6deg) rotateX(-.6deg);
}
}
@keyframes shk-18b-sheen {
0% {
background-position: 100% 0;
}
55%,
100% {
background-position: 0% 0;
}
}
@media (prefers-reduced-motion: reduce) {
.shk-18b__tile:hover {
animation: none;
}
}
```
## JavaScript
```js
/* No JavaScript — each polaroid keeps its resting pose in --r; the hover jiggle swings ±1.4° AROUND that pose via calc(), while translate/scale/shadow ride a separate transition. */
/* No JavaScript — perspective on the PARENT gives the jitter a vanishing point; rotateY/rotateX shiver and the background-position sheen share one 1.2s clock so light and motion feel like a single material. */
```Here's a working CSS Shake Animation from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: CSS Card Shake Effect on Hover
Source: https://codefronts.com/motion/css-shake-animation/css-card-shake-effect-on-hover/
Card-scale shake in two temperaments: a polaroid that jiggles like it was just picked off a desk (rotation around its center, shadow deepening as it lifts), and a glass feature tile that shivers in 3D — rotateY jitter inside a perspective parent with a specular highlight racing across on the same clock.
## HTML
```html
<div class="shk-18-group">
<link href="https://fonts.googleapis.com/css2?family=Caveat:wght@500;600&family=Manrope:wght@500..800&display=swap" rel="stylesheet">
<section class="shk-18a" aria-label="Polaroid photos that jiggle on hover">
<div class="shk-18a__desk">
<figure class="shk-18a__pol" style="--r:-2.5deg">
<img class="shk-18a__img" src="https://picsum.photos/id/1025/360/300" alt="A pug wrapped in a blanket" loading="lazy">
<figcaption class="shk-18a__cap">office manager, probably</figcaption>
</figure>
<figure class="shk-18a__pol" style="--r:1.8deg">
<img class="shk-18a__img" src="https://picsum.photos/id/1015/360/300" alt="A river winding through mountains" loading="lazy">
<figcaption class="shk-18a__cap">offsite, day 2</figcaption>
</figure>
<figure class="shk-18a__pol" style="--r:3.2deg">
<img class="shk-18a__img" src="https://picsum.photos/id/1011/360/300" alt="A person canoeing on a lake" loading="lazy">
<figcaption class="shk-18a__cap">shipping v1 felt like this</figcaption>
</figure>
</div>
<p class="shk-18a__hint" aria-hidden="true">hover — transition lifts, animation jiggles; separate properties, zero conflict</p>
</section>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400..700&display=swap" rel="stylesheet">
<section class="shk-18b" aria-label="Glass feature tile with 3D shiver on hover">
<div class="shk-18b__scene">
<a class="shk-18b__tile" href="#" onclick="return false">
<p class="shk-18b__chip">New · Motion engine</p>
<h2 class="shk-18b__title">Shake, physically.</h2>
<p class="shk-18b__sub">Spring-based micro-interactions, now in the design kit.</p>
<span class="shk-18b__go" aria-hidden="true">Explore →</span>
</a>
</div>
<p class="shk-18b__hint" aria-hidden="true">hover — rotateY jitters ±2° inside a 900px perspective while the highlight sweeps on the same clock</p>
</section>
</div>
```
## CSS
```css
.shk-18-group {
display: flex;
flex-wrap: wrap;
align-items: stretch;
width: 100%;
}
.shk-18-group > section {
flex: 1 1 480px;
min-width: min(100%,360px);
}
.shk-18a,
.shk-18a *,
.shk-18a *::before,
.shk-18a *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.shk-18a {
width: 100%;
min-height: 100vh;
display: grid;
place-content: center;
gap: 26px;
padding: 48px 24px;
background: #c9b99a linear-gradient(180deg,rgba(255,255,255,.16),rgba(94,74,42,.18));
font-family: 'Manrope','Segoe UI',system-ui,sans-serif;
}
.shk-18a__desk {
display: flex;
flex-wrap: wrap;
gap: 26px;
justify-content: center;
}
.shk-18a__pol {
background: #fdfcf8;
padding: 12px 12px 14px;
border-radius: 4px;
rotate: var(--r,0deg);
box-shadow: 0 12px 24px -12px rgba(50,38,20,.55);
cursor: grab;
transition: translate .25s,box-shadow .25s,scale .25s;
}
.shk-18a__pol:hover {
translate: 0 -8px;
scale: 1.03;
box-shadow: 0 34px 50px -22px rgba(50,38,20,.6);
animation: shk-18a-jiggle .8s ease-in-out .1s infinite;
}
.shk-18a__pol:active {
cursor: grabbing;
}
.shk-18a__img {
display: block;
width: min(230px,62vw);
aspect-ratio: 6/5;
object-fit: cover;
filter: saturate(1.05) contrast(1.02);
}
.shk-18a__cap {
margin-top: 10px;
text-align: center;
font-family: 'Caveat',cursive;
font-size: 19px;
color: #4a3a22;
}
.shk-18a__hint {
text-align: center;
font-size: 12px;
color: rgba(58,44,22,.6);
}
@keyframes shk-18a-jiggle {
0%,
100% {
rotate: calc(var(--r,0deg) - 1.4deg);
}
50% {
rotate: calc(var(--r,0deg) + 1.4deg);
}
}
@media (prefers-reduced-motion: reduce) {
.shk-18a__pol:hover {
animation: none;
}
}
.shk-18b,
.shk-18b *,
.shk-18b *::before,
.shk-18b *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.shk-18b {
width: 100%;
min-height: 100vh;
display: grid;
place-content: center;
gap: 22px;
padding: 48px 24px;
background: radial-gradient(120% 120% at 20% 0%,#16203a,#0b0f1d 60%);
font-family: 'Space Grotesk','Segoe UI',system-ui,sans-serif;
}
.shk-18b__scene {
perspective: 900px;
}
.shk-18b__tile {
display: block;
width: min(400px,88vw);
padding: 32px;
border-radius: 22px;
text-decoration: none;
background: linear-gradient(115deg,rgba(255,255,255,.12),rgba(255,255,255,.04) 40%,rgba(255,255,255,.14) 50%,rgba(255,255,255,.04) 60%);
background-size: 250% 100%;
background-position: 100% 0;
border: 1px solid rgba(170,200,255,.22);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
box-shadow: 0 40px 80px -40px rgba(0,0,0,.9),inset 0 1px 0 rgba(255,255,255,.12);
transition: border-color .25s;
}
.shk-18b__tile:hover {
border-color: rgba(170,200,255,.45);
animation: shk-18b-shiver 1.2s ease-in-out infinite,shk-18b-sheen 1.2s ease-in-out infinite;
}
.shk-18b__tile:focus-visible {
outline: 2px solid #9fc0ff;
outline-offset: 4px;
}
.shk-18b__chip {
display: inline-block;
font-size: 11px;
font-weight: 700;
letter-spacing: .1em;
text-transform: uppercase;
color: oklch(0.8 0.11 250);
background: oklch(0.8 0.11 250 / .12);
border: 1px solid oklch(0.8 0.11 250 / .3);
padding: 5px 11px;
border-radius: 999px;
}
.shk-18b__title {
margin-top: 16px;
font-size: 30px;
font-weight: 700;
letter-spacing: -.02em;
color: #eef3ff;
}
.shk-18b__sub {
margin-top: 9px;
font-size: 14px;
line-height: 1.6;
color: rgba(222,232,252,.6);
}
.shk-18b__go {
display: inline-block;
margin-top: 20px;
font-size: 14px;
font-weight: 700;
color: #9fc0ff;
}
.shk-18b__hint {
text-align: center;
font-size: 12px;
color: rgba(222,232,252,.4);
max-width: 440px;
justify-self: center;
}
@keyframes shk-18b-shiver {
0%,
100% {
transform: rotateY(-2deg) rotateX(.6deg);
}
25% {
transform: rotateY(2deg) rotateX(-.4deg);
}
50% {
transform: rotateY(-1.4deg) rotateX(.5deg);
}
75% {
transform: rotateY(1.6deg) rotateX(-.6deg);
}
}
@keyframes shk-18b-sheen {
0% {
background-position: 100% 0;
}
55%,
100% {
background-position: 0% 0;
}
}
@media (prefers-reduced-motion: reduce) {
.shk-18b__tile:hover {
animation: none;
}
}
```
## JavaScript
```js
/* No JavaScript — each polaroid keeps its resting pose in --r; the hover jiggle swings ±1.4° AROUND that pose via calc(), while translate/scale/shadow ride a separate transition. */
/* No JavaScript — perspective on the PARENT gives the jitter a vanishing point; rotateY/rotateX shiver and the background-position sheen share one 1.2s clock so light and motion feel like a single material. */
```How this works
Cards are big, so hover-shake needs restraint plus physics. The polaroid composes two channels: a transition owns the pickup (lift + scale + shadow), an animation owns the jiggle, on the same element via individual properties so they don't fight:
.card{transition:translate .25s,box-shadow .25s}
.card:hover{translate:0 -6px;
animation:jiggle .8s ease-in-out .1s infinite;
box-shadow:0 30px 50px -20px rgba(0,0,0,.45)}
@keyframes jiggle{0%,100%{rotate:-1.4deg}50%{rotate:1.4deg}}Because translate and rotate are separate properties now, the transition animates one while the keyframes animate the other — no transform tug-of-war. The 3D variant wraps the tile in perspective:900px and jitters rotateY ±2°; a background-position sweep synced to the same duration reads as light rolling over glass.
Make it yours
- Rotation stays ≤1.5° for photo cards, ≤2.5° for 3D tiles — card size amplifies perceived motion.
- Start polaroids with a resting rotate (-1° to 2° each, alternating) so the grid feels hand-placed before any hover.
- Speed sets mood: .8s jiggle = mischievous; .4s = nervous; 1.4s = dreamy float.
- For the 3D tile, put perspective on the PARENT so multiple tiles share one vanishing point.
Gotchas — read before shipping
- Without individual transform properties, transition and animation both writing 'transform' cancel each other — the #1 reason this effect 'randomly breaks'.
- Jiggling changes the hover hit-area edge; padding-compensate or a fast pointer can flicker in/out of :hover at the card edge.
- backdrop-filter + 3D transforms can drop to software rendering on old GPUs — test with devtools layers panel.
- Set transform-style only where needed; promiscuous preserve-3d breaks z-index stacking inside the card.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 15.4+ | 113+ | 111+ |
Floor set by oklch(), backdrop-filter as this demo is written. The core technique itself goes back further — Chrome 104+.
The channel-split trick requires individual transform properties (translate/rotate) — Chrome 104, Safari 14.1, Firefox 72. Older browsers: fall back to animating a wrapper for lift and the card for jiggle.