20 / 20
All 20 in 20 CSS Claymorphism
- 01Claymorphism Shadow Recipe Reference Build
- 02Inner Highlight and Inner Shadow Layering
- 03Border Radius Scale for Puffy Shapes
- 04Claymorphism vs Neumorphism Side by Side
- 05Claymorphism with Glassmorphism Hybrid
- 06Light Source Consistency Across a Clay Layout
- 07Clay Depth Tiers for Visual Hierarchy
- 08Tailwind CSS Claymorphism Implementation
- 09Clay Button Press and Squish States
- 10Clay Toggle Switch and Checkbox Set
- 11Clay Input Field and Search Bar
- 12Clay Badge & Pill Tag System
- 13Claymorphism Card Component
- 14Clay Modal Dialog with Floating Depth
- 15Clay Pricing Table with Raised Popular Tier
- 16Clay Bottom Tab Bar for Mobile
- 17Clay Icon Tiles and App Grid
- 18Clay Toast & Floating Alert Bubbles
- 19Clay Dark Mode Shadow Recalibration
- 20Clay Animation Performance & Transform Optimization
Clay Animation Performance & Transform Optimization
Measure it, do not guess. Twenty-four clay tiles animate under a live FPS meter in two modes: the naive one animates box-shadow blur directly, the optimized one pre-renders both shadow states and cross-fades a pseudo-element while transform does the motion. The frame counter and long-frame tally are real, and the gap is visible on screen.
Untitled UI Pro: a 10,000+ component Figma UI kit · from $129 one-time
Untitled UI · Affiliate link
The code
<section class="cl-20" aria-label="Clay animation performance and transform optimization demo">
<div class="cl-20__stage">
<div class="cl-20__panel" data-mode="fast">
<header class="cl-20__head">
<div>
<h2 class="cl-20__h">Measure the shadow tax</h2>
<p class="cl-20__lede">24 tiles, one continuous animation. Switch modes and watch the meter — same visual result, very different cost.</p>
</div>
<div class="cl-20__meter" role="status" aria-live="off">
<p class="cl-20__fps"><b class="cl-20__fpsVal">60</b> fps</p>
<p class="cl-20__jank">long frames: <b class="cl-20__jankVal">0</b></p>
<span class="cl-20__graph" aria-hidden="true"></span>
</div>
</header>
<div class="cl-20__modes" role="group" aria-label="Animation mode">
<button class="cl-20__mode is-on" type="button" data-set="fast" aria-pressed="true">Optimized · transform + opacity</button>
<button class="cl-20__mode" type="button" data-set="slow" aria-pressed="false">Naive · animating box-shadow</button>
</div>
<div class="cl-20__grid"></div>
<p class="cl-20__hint">Optimized mode pre-renders both shadow states and cross-fades them; motion is <code>translate</code> + <code>scale</code> only.</p>
</div>
<p class="cl-20__chip" aria-hidden="true">shadow = paint · transform/opacity = composite · rAF meter, real numbers</p>
</div>
</section><section class="cl-20" aria-label="Clay animation performance and transform optimization demo">
<div class="cl-20__stage">
<div class="cl-20__panel" data-mode="fast">
<header class="cl-20__head">
<div>
<h2 class="cl-20__h">Measure the shadow tax</h2>
<p class="cl-20__lede">24 tiles, one continuous animation. Switch modes and watch the meter — same visual result, very different cost.</p>
</div>
<div class="cl-20__meter" role="status" aria-live="off">
<p class="cl-20__fps"><b class="cl-20__fpsVal">60</b> fps</p>
<p class="cl-20__jank">long frames: <b class="cl-20__jankVal">0</b></p>
<span class="cl-20__graph" aria-hidden="true"></span>
</div>
</header>
<div class="cl-20__modes" role="group" aria-label="Animation mode">
<button class="cl-20__mode is-on" type="button" data-set="fast" aria-pressed="true">Optimized · transform + opacity</button>
<button class="cl-20__mode" type="button" data-set="slow" aria-pressed="false">Naive · animating box-shadow</button>
</div>
<div class="cl-20__grid"></div>
<p class="cl-20__hint">Optimized mode pre-renders both shadow states and cross-fades them; motion is <code>translate</code> + <code>scale</code> only.</p>
</div>
<p class="cl-20__chip" aria-hidden="true">shadow = paint · transform/opacity = composite · rAF meter, real numbers</p>
</div>
</section>.cl-20,
.cl-20 *,
.cl-20 *::before,
.cl-20 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.cl-20 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
background: var(--cl-20-bg);
--cl-20-bg: oklch(0.94 0.026 135);
--cl-20-clay: oklch(0.88 0.07 135);
--cl-20-accent: oklch(0.8 0.14 140);
--cl-20-shade: oklch(0.53 0.09 138/.5);
--cl-20-light: oklch(1 0.015 135/.95);
--cl-20-ink: oklch(0.27 0.05 138);
--cl-20-mut: oklch(0.5 0.04 138);
font-family: system-ui,-apple-system,'Segoe UI',sans-serif;
color: var(--cl-20-ink);
}
.cl-20__stage {
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
align-content: center;
gap: 18px;
padding: clamp(18px,4vw,52px);
}
.cl-20__panel {
width: min(100%,900px);
display: grid;
gap: clamp(16px,2.6vw,24px);
padding: clamp(20px,3.2vw,34px);
border-radius: 44px;
background: color-mix(in oklch,var(--cl-20-bg) 93%,white);
box-shadow: 0 30px 58px -24px var(--cl-20-shade),inset 0 -16px 28px -16px var(--cl-20-shade),inset 0 18px 28px -12px var(--cl-20-light);
}
.cl-20__head {
display: flex;
flex-wrap: wrap;
gap: 18px;
align-items: center;
justify-content: space-between;
}
.cl-20__h {
font-size: clamp(21px,3.2vw,30px);
font-weight: 780;
letter-spacing: -.022em;
}
.cl-20__lede {
margin-top: 6px;
max-width: 46ch;
font-size: 14px;
line-height: 1.55;
color: var(--cl-20-mut);
text-wrap: pretty;
}
.cl-20__meter {
display: grid;
gap: 4px;
min-width: 150px;
padding: 14px 18px;
border-radius: 26px;
background: color-mix(in oklch,var(--cl-20-bg) 96%,oklch(0.66 0.06 138));
box-shadow: inset 0 10px 18px -8px var(--cl-20-shade),inset 0 -7px 13px -10px var(--cl-20-light);
}
.cl-20__fps {
font-size: 13px;
color: var(--cl-20-mut);
font-variant-numeric: tabular-nums;
}
.cl-20__fps b {
font-size: 30px;
font-weight: 790;
letter-spacing: -.03em;
color: var(--cl-20-ink);
}
.cl-20__jank {
font-size: 11.5px;
color: var(--cl-20-mut);
font-variant-numeric: tabular-nums;
}
.cl-20__jank b {
color: oklch(0.55 0.17 25);
font-weight: 740;
}
.cl-20__graph {
display: flex;
align-items: flex-end;
gap: 2px;
height: 26px;
margin-top: 4px;
}
.cl-20__graph i {
flex: 1;
min-width: 2px;
border-radius: 2px;
background: var(--cl-20-accent);
height: 100%;
transform-origin: bottom;
scale: 1 var(--cl-20-s,.1);
}
.cl-20__modes {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.cl-20__mode {
flex: 1;
min-width: 200px;
min-height: 52px;
padding: 0 20px;
border: 0;
border-radius: 24px;
font: inherit;
font-size: 13.5px;
font-weight: 660;
color: var(--cl-20-mut);
cursor: pointer;
background: color-mix(in oklch,var(--cl-20-bg) 92%,white);
box-shadow: 0 12px 20px -14px var(--cl-20-shade),inset 0 -8px 14px -10px var(--cl-20-shade),inset 0 10px 14px -8px var(--cl-20-light);
transition: color .25s,background .25s,translate .18s,scale .18s cubic-bezier(.2,.8,.3,1.4);
}
.cl-20__mode:hover {
translate: 0 -3px;
color: var(--cl-20-ink);
}
.cl-20__mode:active {
scale: 1 .94;
translate: 0 2px;
}
.cl-20__mode:focus-visible {
outline: 3px solid var(--cl-20-ink);
outline-offset: 3px;
}
.cl-20__mode.is-on {
color: oklch(0.24 0.07 140);
background: var(--cl-20-accent);
box-shadow: 0 16px 26px -12px var(--cl-20-shade),inset 0 -10px 16px -9px var(--cl-20-shade),inset 0 12px 16px -7px var(--cl-20-light);
}
.cl-20__grid {
display: grid;
grid-template-columns: repeat(auto-fit,minmax(64px,1fr));
gap: clamp(8px,1.6vw,14px);
padding: clamp(12px,2vw,18px);
border-radius: 34px;
background: color-mix(in oklch,var(--cl-20-bg) 96%,oklch(0.7 0.05 138));
box-shadow: inset 0 12px 22px -10px var(--cl-20-shade),inset 0 -9px 16px -12px var(--cl-20-light);
}
.cl-20__tile {
position: relative;
aspect-ratio: 1;
border-radius: 22px;
contain: paint;
background: var(--cl-20-clay);
box-shadow: 0 10px 18px -8px var(--cl-20-shade),inset 0 -8px 13px -8px var(--cl-20-shade),inset 0 10px 13px -6px var(--cl-20-light);
}
.cl-20__tile::after {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
opacity: 0;
box-shadow: 0 24px 34px -10px var(--cl-20-shade),inset 0 -10px 16px -8px var(--cl-20-shade),inset 0 14px 18px -6px var(--cl-20-light);
}
.cl-20__panel[data-mode="fast"] .cl-20__tile {
animation: cl-20-lift 1.6s ease-in-out infinite alternate;
animation-delay: calc(var(--cl-20-i) * 55ms);
will-change: transform;
}
.cl-20__panel[data-mode="fast"] .cl-20__tile::after {
animation: cl-20-fade 1.6s ease-in-out infinite alternate;
animation-delay: calc(var(--cl-20-i) * 55ms);
}
.cl-20__panel[data-mode="slow"] .cl-20__tile {
animation: cl-20-shadow 1.6s ease-in-out infinite alternate;
animation-delay: calc(var(--cl-20-i) * 55ms);
}
@keyframes cl-20-lift {
from {
translate: 0 0;
scale: 1;
}
to {
translate: 0 -9px;
scale: 1.05;
}
}
@keyframes cl-20-fade {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes cl-20-shadow {
from {
box-shadow: 0 10px 18px -8px var(--cl-20-shade),inset 0 -8px 13px -8px var(--cl-20-shade),inset 0 10px 13px -6px var(--cl-20-light);
}
to {
box-shadow: 0 34px 56px -8px var(--cl-20-shade),inset 0 -18px 30px -8px var(--cl-20-shade),inset 0 24px 34px -6px var(--cl-20-light);
}
}
.cl-20__hint {
font-size: 12.5px;
line-height: 1.55;
color: var(--cl-20-mut);
text-align: center;
text-wrap: pretty;
}
.cl-20__hint code {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 12px;
}
.cl-20__chip {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
color: var(--cl-20-mut);
padding: 9px 16px;
border-radius: 99px;
text-align: center;
background: oklch(1 0 0/.6);
box-shadow: inset 0 -4px 10px -8px var(--cl-20-shade),inset 0 6px 10px -6px oklch(1 0 0);
}
@media (prefers-reduced-motion: reduce) {
.cl-20__tile,
.cl-20__tile::after {
animation: none !important;
}
.cl-20 * {
transition-duration: .01ms !important;
}
}.cl-20,
.cl-20 *,
.cl-20 *::before,
.cl-20 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.cl-20 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
background: var(--cl-20-bg);
--cl-20-bg: oklch(0.94 0.026 135);
--cl-20-clay: oklch(0.88 0.07 135);
--cl-20-accent: oklch(0.8 0.14 140);
--cl-20-shade: oklch(0.53 0.09 138/.5);
--cl-20-light: oklch(1 0.015 135/.95);
--cl-20-ink: oklch(0.27 0.05 138);
--cl-20-mut: oklch(0.5 0.04 138);
font-family: system-ui,-apple-system,'Segoe UI',sans-serif;
color: var(--cl-20-ink);
}
.cl-20__stage {
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
align-content: center;
gap: 18px;
padding: clamp(18px,4vw,52px);
}
.cl-20__panel {
width: min(100%,900px);
display: grid;
gap: clamp(16px,2.6vw,24px);
padding: clamp(20px,3.2vw,34px);
border-radius: 44px;
background: color-mix(in oklch,var(--cl-20-bg) 93%,white);
box-shadow: 0 30px 58px -24px var(--cl-20-shade),inset 0 -16px 28px -16px var(--cl-20-shade),inset 0 18px 28px -12px var(--cl-20-light);
}
.cl-20__head {
display: flex;
flex-wrap: wrap;
gap: 18px;
align-items: center;
justify-content: space-between;
}
.cl-20__h {
font-size: clamp(21px,3.2vw,30px);
font-weight: 780;
letter-spacing: -.022em;
}
.cl-20__lede {
margin-top: 6px;
max-width: 46ch;
font-size: 14px;
line-height: 1.55;
color: var(--cl-20-mut);
text-wrap: pretty;
}
.cl-20__meter {
display: grid;
gap: 4px;
min-width: 150px;
padding: 14px 18px;
border-radius: 26px;
background: color-mix(in oklch,var(--cl-20-bg) 96%,oklch(0.66 0.06 138));
box-shadow: inset 0 10px 18px -8px var(--cl-20-shade),inset 0 -7px 13px -10px var(--cl-20-light);
}
.cl-20__fps {
font-size: 13px;
color: var(--cl-20-mut);
font-variant-numeric: tabular-nums;
}
.cl-20__fps b {
font-size: 30px;
font-weight: 790;
letter-spacing: -.03em;
color: var(--cl-20-ink);
}
.cl-20__jank {
font-size: 11.5px;
color: var(--cl-20-mut);
font-variant-numeric: tabular-nums;
}
.cl-20__jank b {
color: oklch(0.55 0.17 25);
font-weight: 740;
}
.cl-20__graph {
display: flex;
align-items: flex-end;
gap: 2px;
height: 26px;
margin-top: 4px;
}
.cl-20__graph i {
flex: 1;
min-width: 2px;
border-radius: 2px;
background: var(--cl-20-accent);
height: 100%;
transform-origin: bottom;
scale: 1 var(--cl-20-s,.1);
}
.cl-20__modes {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.cl-20__mode {
flex: 1;
min-width: 200px;
min-height: 52px;
padding: 0 20px;
border: 0;
border-radius: 24px;
font: inherit;
font-size: 13.5px;
font-weight: 660;
color: var(--cl-20-mut);
cursor: pointer;
background: color-mix(in oklch,var(--cl-20-bg) 92%,white);
box-shadow: 0 12px 20px -14px var(--cl-20-shade),inset 0 -8px 14px -10px var(--cl-20-shade),inset 0 10px 14px -8px var(--cl-20-light);
transition: color .25s,background .25s,translate .18s,scale .18s cubic-bezier(.2,.8,.3,1.4);
}
.cl-20__mode:hover {
translate: 0 -3px;
color: var(--cl-20-ink);
}
.cl-20__mode:active {
scale: 1 .94;
translate: 0 2px;
}
.cl-20__mode:focus-visible {
outline: 3px solid var(--cl-20-ink);
outline-offset: 3px;
}
.cl-20__mode.is-on {
color: oklch(0.24 0.07 140);
background: var(--cl-20-accent);
box-shadow: 0 16px 26px -12px var(--cl-20-shade),inset 0 -10px 16px -9px var(--cl-20-shade),inset 0 12px 16px -7px var(--cl-20-light);
}
.cl-20__grid {
display: grid;
grid-template-columns: repeat(auto-fit,minmax(64px,1fr));
gap: clamp(8px,1.6vw,14px);
padding: clamp(12px,2vw,18px);
border-radius: 34px;
background: color-mix(in oklch,var(--cl-20-bg) 96%,oklch(0.7 0.05 138));
box-shadow: inset 0 12px 22px -10px var(--cl-20-shade),inset 0 -9px 16px -12px var(--cl-20-light);
}
.cl-20__tile {
position: relative;
aspect-ratio: 1;
border-radius: 22px;
contain: paint;
background: var(--cl-20-clay);
box-shadow: 0 10px 18px -8px var(--cl-20-shade),inset 0 -8px 13px -8px var(--cl-20-shade),inset 0 10px 13px -6px var(--cl-20-light);
}
.cl-20__tile::after {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
opacity: 0;
box-shadow: 0 24px 34px -10px var(--cl-20-shade),inset 0 -10px 16px -8px var(--cl-20-shade),inset 0 14px 18px -6px var(--cl-20-light);
}
.cl-20__panel[data-mode="fast"] .cl-20__tile {
animation: cl-20-lift 1.6s ease-in-out infinite alternate;
animation-delay: calc(var(--cl-20-i) * 55ms);
will-change: transform;
}
.cl-20__panel[data-mode="fast"] .cl-20__tile::after {
animation: cl-20-fade 1.6s ease-in-out infinite alternate;
animation-delay: calc(var(--cl-20-i) * 55ms);
}
.cl-20__panel[data-mode="slow"] .cl-20__tile {
animation: cl-20-shadow 1.6s ease-in-out infinite alternate;
animation-delay: calc(var(--cl-20-i) * 55ms);
}
@keyframes cl-20-lift {
from {
translate: 0 0;
scale: 1;
}
to {
translate: 0 -9px;
scale: 1.05;
}
}
@keyframes cl-20-fade {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes cl-20-shadow {
from {
box-shadow: 0 10px 18px -8px var(--cl-20-shade),inset 0 -8px 13px -8px var(--cl-20-shade),inset 0 10px 13px -6px var(--cl-20-light);
}
to {
box-shadow: 0 34px 56px -8px var(--cl-20-shade),inset 0 -18px 30px -8px var(--cl-20-shade),inset 0 24px 34px -6px var(--cl-20-light);
}
}
.cl-20__hint {
font-size: 12.5px;
line-height: 1.55;
color: var(--cl-20-mut);
text-align: center;
text-wrap: pretty;
}
.cl-20__hint code {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 12px;
}
.cl-20__chip {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
color: var(--cl-20-mut);
padding: 9px 16px;
border-radius: 99px;
text-align: center;
background: oklch(1 0 0/.6);
box-shadow: inset 0 -4px 10px -8px var(--cl-20-shade),inset 0 6px 10px -6px oklch(1 0 0);
}
@media (prefers-reduced-motion: reduce) {
.cl-20__tile,
.cl-20__tile::after {
animation: none !important;
}
.cl-20 * {
transition-duration: .01ms !important;
}
}(() => {
document.querySelectorAll('.cl-20__panel').forEach((panel) => {
if (panel.dataset.wired) return;
panel.dataset.wired = '1';
const grid = panel.querySelector('.cl-20__grid');
const graph = panel.querySelector('.cl-20__graph');
const fpsVal = panel.querySelector('.cl-20__fpsVal');
const jankVal = panel.querySelector('.cl-20__jankVal');
const TILES = 24, BARS = 28;
for (let i = 0; i < TILES; i++) {
const t = document.createElement('div');
t.className = 'cl-20__tile';
t.style.setProperty('--cl-20-i', String(i));
grid.appendChild(t);
}
const bars = [];
for (let i = 0; i < BARS; i++) {
const b = document.createElement('i');
graph.appendChild(b);
bars.push(b);
}
panel.querySelectorAll('.cl-20__mode').forEach((btn) => {
btn.addEventListener('click', () => {
panel.dataset.mode = btn.dataset.set;
jank = 0;
jankVal.textContent = '0';
panel.querySelectorAll('.cl-20__mode').forEach((b) => {
const on = b === btn;
b.classList.toggle('is-on', on);
b.setAttribute('aria-pressed', String(on));
});
});
});
let frames = 0, jank = 0, last = performance.now(), mark = last, cursor = 0;
const tick = (now) => {
const dt = now - last;
last = now;
frames++;
if (dt > 24) { jank++; jankVal.textContent = String(jank); }
if (now - mark >= 500) {
const fps = Math.round((frames * 1000) / (now - mark));
fpsVal.textContent = String(fps);
bars[cursor % BARS].style.setProperty('--cl-20-s', String(Math.max(0.06, Math.min(1, fps / 60))));
cursor++;
frames = 0;
mark = now;
}
requestAnimationFrame(tick);
};
requestAnimationFrame(tick);
});
})();(() => {
document.querySelectorAll('.cl-20__panel').forEach((panel) => {
if (panel.dataset.wired) return;
panel.dataset.wired = '1';
const grid = panel.querySelector('.cl-20__grid');
const graph = panel.querySelector('.cl-20__graph');
const fpsVal = panel.querySelector('.cl-20__fpsVal');
const jankVal = panel.querySelector('.cl-20__jankVal');
const TILES = 24, BARS = 28;
for (let i = 0; i < TILES; i++) {
const t = document.createElement('div');
t.className = 'cl-20__tile';
t.style.setProperty('--cl-20-i', String(i));
grid.appendChild(t);
}
const bars = [];
for (let i = 0; i < BARS; i++) {
const b = document.createElement('i');
graph.appendChild(b);
bars.push(b);
}
panel.querySelectorAll('.cl-20__mode').forEach((btn) => {
btn.addEventListener('click', () => {
panel.dataset.mode = btn.dataset.set;
jank = 0;
jankVal.textContent = '0';
panel.querySelectorAll('.cl-20__mode').forEach((b) => {
const on = b === btn;
b.classList.toggle('is-on', on);
b.setAttribute('aria-pressed', String(on));
});
});
});
let frames = 0, jank = 0, last = performance.now(), mark = last, cursor = 0;
const tick = (now) => {
const dt = now - last;
last = now;
frames++;
if (dt > 24) { jank++; jankVal.textContent = String(jank); }
if (now - mark >= 500) {
const fps = Math.round((frames * 1000) / (now - mark));
fpsVal.textContent = String(fps);
bars[cursor % BARS].style.setProperty('--cl-20-s', String(Math.max(0.06, Math.min(1, fps / 60))));
cursor++;
frames = 0;
mark = now;
}
requestAnimationFrame(tick);
};
requestAnimationFrame(tick);
});
})();Here's a working CSS Claymorphism 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: Clay Animation Performance & Transform Optimization
Source: https://codefronts.com/design-styles/css-claymorphism/clay-animation-performance-transforms/
Measure it, do not guess. Twenty-four clay tiles animate under a live FPS meter in two modes: the naive one animates box-shadow blur directly, the optimized one pre-renders both shadow states and cross-fades a pseudo-element while transform does the motion. The frame counter and long-frame tally are real, and the gap is visible on screen.
## HTML
```html
<section class="cl-20" aria-label="Clay animation performance and transform optimization demo">
<div class="cl-20__stage">
<div class="cl-20__panel" data-mode="fast">
<header class="cl-20__head">
<div>
<h2 class="cl-20__h">Measure the shadow tax</h2>
<p class="cl-20__lede">24 tiles, one continuous animation. Switch modes and watch the meter — same visual result, very different cost.</p>
</div>
<div class="cl-20__meter" role="status" aria-live="off">
<p class="cl-20__fps"><b class="cl-20__fpsVal">60</b> fps</p>
<p class="cl-20__jank">long frames: <b class="cl-20__jankVal">0</b></p>
<span class="cl-20__graph" aria-hidden="true"></span>
</div>
</header>
<div class="cl-20__modes" role="group" aria-label="Animation mode">
<button class="cl-20__mode is-on" type="button" data-set="fast" aria-pressed="true">Optimized · transform + opacity</button>
<button class="cl-20__mode" type="button" data-set="slow" aria-pressed="false">Naive · animating box-shadow</button>
</div>
<div class="cl-20__grid"></div>
<p class="cl-20__hint">Optimized mode pre-renders both shadow states and cross-fades them; motion is <code>translate</code> + <code>scale</code> only.</p>
</div>
<p class="cl-20__chip" aria-hidden="true">shadow = paint · transform/opacity = composite · rAF meter, real numbers</p>
</div>
</section>
```
## CSS
```css
.cl-20,
.cl-20 *,
.cl-20 *::before,
.cl-20 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.cl-20 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
background: var(--cl-20-bg);
--cl-20-bg: oklch(0.94 0.026 135);
--cl-20-clay: oklch(0.88 0.07 135);
--cl-20-accent: oklch(0.8 0.14 140);
--cl-20-shade: oklch(0.53 0.09 138/.5);
--cl-20-light: oklch(1 0.015 135/.95);
--cl-20-ink: oklch(0.27 0.05 138);
--cl-20-mut: oklch(0.5 0.04 138);
font-family: system-ui,-apple-system,'Segoe UI',sans-serif;
color: var(--cl-20-ink);
}
.cl-20__stage {
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
align-content: center;
gap: 18px;
padding: clamp(18px,4vw,52px);
}
.cl-20__panel {
width: min(100%,900px);
display: grid;
gap: clamp(16px,2.6vw,24px);
padding: clamp(20px,3.2vw,34px);
border-radius: 44px;
background: color-mix(in oklch,var(--cl-20-bg) 93%,white);
box-shadow: 0 30px 58px -24px var(--cl-20-shade),inset 0 -16px 28px -16px var(--cl-20-shade),inset 0 18px 28px -12px var(--cl-20-light);
}
.cl-20__head {
display: flex;
flex-wrap: wrap;
gap: 18px;
align-items: center;
justify-content: space-between;
}
.cl-20__h {
font-size: clamp(21px,3.2vw,30px);
font-weight: 780;
letter-spacing: -.022em;
}
.cl-20__lede {
margin-top: 6px;
max-width: 46ch;
font-size: 14px;
line-height: 1.55;
color: var(--cl-20-mut);
text-wrap: pretty;
}
.cl-20__meter {
display: grid;
gap: 4px;
min-width: 150px;
padding: 14px 18px;
border-radius: 26px;
background: color-mix(in oklch,var(--cl-20-bg) 96%,oklch(0.66 0.06 138));
box-shadow: inset 0 10px 18px -8px var(--cl-20-shade),inset 0 -7px 13px -10px var(--cl-20-light);
}
.cl-20__fps {
font-size: 13px;
color: var(--cl-20-mut);
font-variant-numeric: tabular-nums;
}
.cl-20__fps b {
font-size: 30px;
font-weight: 790;
letter-spacing: -.03em;
color: var(--cl-20-ink);
}
.cl-20__jank {
font-size: 11.5px;
color: var(--cl-20-mut);
font-variant-numeric: tabular-nums;
}
.cl-20__jank b {
color: oklch(0.55 0.17 25);
font-weight: 740;
}
.cl-20__graph {
display: flex;
align-items: flex-end;
gap: 2px;
height: 26px;
margin-top: 4px;
}
.cl-20__graph i {
flex: 1;
min-width: 2px;
border-radius: 2px;
background: var(--cl-20-accent);
height: 100%;
transform-origin: bottom;
scale: 1 var(--cl-20-s,.1);
}
.cl-20__modes {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.cl-20__mode {
flex: 1;
min-width: 200px;
min-height: 52px;
padding: 0 20px;
border: 0;
border-radius: 24px;
font: inherit;
font-size: 13.5px;
font-weight: 660;
color: var(--cl-20-mut);
cursor: pointer;
background: color-mix(in oklch,var(--cl-20-bg) 92%,white);
box-shadow: 0 12px 20px -14px var(--cl-20-shade),inset 0 -8px 14px -10px var(--cl-20-shade),inset 0 10px 14px -8px var(--cl-20-light);
transition: color .25s,background .25s,translate .18s,scale .18s cubic-bezier(.2,.8,.3,1.4);
}
.cl-20__mode:hover {
translate: 0 -3px;
color: var(--cl-20-ink);
}
.cl-20__mode:active {
scale: 1 .94;
translate: 0 2px;
}
.cl-20__mode:focus-visible {
outline: 3px solid var(--cl-20-ink);
outline-offset: 3px;
}
.cl-20__mode.is-on {
color: oklch(0.24 0.07 140);
background: var(--cl-20-accent);
box-shadow: 0 16px 26px -12px var(--cl-20-shade),inset 0 -10px 16px -9px var(--cl-20-shade),inset 0 12px 16px -7px var(--cl-20-light);
}
.cl-20__grid {
display: grid;
grid-template-columns: repeat(auto-fit,minmax(64px,1fr));
gap: clamp(8px,1.6vw,14px);
padding: clamp(12px,2vw,18px);
border-radius: 34px;
background: color-mix(in oklch,var(--cl-20-bg) 96%,oklch(0.7 0.05 138));
box-shadow: inset 0 12px 22px -10px var(--cl-20-shade),inset 0 -9px 16px -12px var(--cl-20-light);
}
.cl-20__tile {
position: relative;
aspect-ratio: 1;
border-radius: 22px;
contain: paint;
background: var(--cl-20-clay);
box-shadow: 0 10px 18px -8px var(--cl-20-shade),inset 0 -8px 13px -8px var(--cl-20-shade),inset 0 10px 13px -6px var(--cl-20-light);
}
.cl-20__tile::after {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
opacity: 0;
box-shadow: 0 24px 34px -10px var(--cl-20-shade),inset 0 -10px 16px -8px var(--cl-20-shade),inset 0 14px 18px -6px var(--cl-20-light);
}
.cl-20__panel[data-mode="fast"] .cl-20__tile {
animation: cl-20-lift 1.6s ease-in-out infinite alternate;
animation-delay: calc(var(--cl-20-i) * 55ms);
will-change: transform;
}
.cl-20__panel[data-mode="fast"] .cl-20__tile::after {
animation: cl-20-fade 1.6s ease-in-out infinite alternate;
animation-delay: calc(var(--cl-20-i) * 55ms);
}
.cl-20__panel[data-mode="slow"] .cl-20__tile {
animation: cl-20-shadow 1.6s ease-in-out infinite alternate;
animation-delay: calc(var(--cl-20-i) * 55ms);
}
@keyframes cl-20-lift {
from {
translate: 0 0;
scale: 1;
}
to {
translate: 0 -9px;
scale: 1.05;
}
}
@keyframes cl-20-fade {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes cl-20-shadow {
from {
box-shadow: 0 10px 18px -8px var(--cl-20-shade),inset 0 -8px 13px -8px var(--cl-20-shade),inset 0 10px 13px -6px var(--cl-20-light);
}
to {
box-shadow: 0 34px 56px -8px var(--cl-20-shade),inset 0 -18px 30px -8px var(--cl-20-shade),inset 0 24px 34px -6px var(--cl-20-light);
}
}
.cl-20__hint {
font-size: 12.5px;
line-height: 1.55;
color: var(--cl-20-mut);
text-align: center;
text-wrap: pretty;
}
.cl-20__hint code {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 12px;
}
.cl-20__chip {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
color: var(--cl-20-mut);
padding: 9px 16px;
border-radius: 99px;
text-align: center;
background: oklch(1 0 0/.6);
box-shadow: inset 0 -4px 10px -8px var(--cl-20-shade),inset 0 6px 10px -6px oklch(1 0 0);
}
@media (prefers-reduced-motion: reduce) {
.cl-20__tile,
.cl-20__tile::after {
animation: none !important;
}
.cl-20 * {
transition-duration: .01ms !important;
}
}
```
## JavaScript
```js
(() => {
document.querySelectorAll('.cl-20__panel').forEach((panel) => {
if (panel.dataset.wired) return;
panel.dataset.wired = '1';
const grid = panel.querySelector('.cl-20__grid');
const graph = panel.querySelector('.cl-20__graph');
const fpsVal = panel.querySelector('.cl-20__fpsVal');
const jankVal = panel.querySelector('.cl-20__jankVal');
const TILES = 24, BARS = 28;
for (let i = 0; i < TILES; i++) {
const t = document.createElement('div');
t.className = 'cl-20__tile';
t.style.setProperty('--cl-20-i', String(i));
grid.appendChild(t);
}
const bars = [];
for (let i = 0; i < BARS; i++) {
const b = document.createElement('i');
graph.appendChild(b);
bars.push(b);
}
panel.querySelectorAll('.cl-20__mode').forEach((btn) => {
btn.addEventListener('click', () => {
panel.dataset.mode = btn.dataset.set;
jank = 0;
jankVal.textContent = '0';
panel.querySelectorAll('.cl-20__mode').forEach((b) => {
const on = b === btn;
b.classList.toggle('is-on', on);
b.setAttribute('aria-pressed', String(on));
});
});
});
let frames = 0, jank = 0, last = performance.now(), mark = last, cursor = 0;
const tick = (now) => {
const dt = now - last;
last = now;
frames++;
if (dt > 24) { jank++; jankVal.textContent = String(jank); }
if (now - mark >= 500) {
const fps = Math.round((frames * 1000) / (now - mark));
fpsVal.textContent = String(fps);
bars[cursor % BARS].style.setProperty('--cl-20-s', String(Math.max(0.06, Math.min(1, fps / 60))));
cursor++;
frames = 0;
mark = now;
}
requestAnimationFrame(tick);
};
requestAnimationFrame(tick);
});
})();
```Here's a working CSS Claymorphism 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: Clay Animation Performance & Transform Optimization
Source: https://codefronts.com/design-styles/css-claymorphism/clay-animation-performance-transforms/
Measure it, do not guess. Twenty-four clay tiles animate under a live FPS meter in two modes: the naive one animates box-shadow blur directly, the optimized one pre-renders both shadow states and cross-fades a pseudo-element while transform does the motion. The frame counter and long-frame tally are real, and the gap is visible on screen.
## HTML
```html
<section class="cl-20" aria-label="Clay animation performance and transform optimization demo">
<div class="cl-20__stage">
<div class="cl-20__panel" data-mode="fast">
<header class="cl-20__head">
<div>
<h2 class="cl-20__h">Measure the shadow tax</h2>
<p class="cl-20__lede">24 tiles, one continuous animation. Switch modes and watch the meter — same visual result, very different cost.</p>
</div>
<div class="cl-20__meter" role="status" aria-live="off">
<p class="cl-20__fps"><b class="cl-20__fpsVal">60</b> fps</p>
<p class="cl-20__jank">long frames: <b class="cl-20__jankVal">0</b></p>
<span class="cl-20__graph" aria-hidden="true"></span>
</div>
</header>
<div class="cl-20__modes" role="group" aria-label="Animation mode">
<button class="cl-20__mode is-on" type="button" data-set="fast" aria-pressed="true">Optimized · transform + opacity</button>
<button class="cl-20__mode" type="button" data-set="slow" aria-pressed="false">Naive · animating box-shadow</button>
</div>
<div class="cl-20__grid"></div>
<p class="cl-20__hint">Optimized mode pre-renders both shadow states and cross-fades them; motion is <code>translate</code> + <code>scale</code> only.</p>
</div>
<p class="cl-20__chip" aria-hidden="true">shadow = paint · transform/opacity = composite · rAF meter, real numbers</p>
</div>
</section>
```
## CSS
```css
.cl-20,
.cl-20 *,
.cl-20 *::before,
.cl-20 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.cl-20 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
background: var(--cl-20-bg);
--cl-20-bg: oklch(0.94 0.026 135);
--cl-20-clay: oklch(0.88 0.07 135);
--cl-20-accent: oklch(0.8 0.14 140);
--cl-20-shade: oklch(0.53 0.09 138/.5);
--cl-20-light: oklch(1 0.015 135/.95);
--cl-20-ink: oklch(0.27 0.05 138);
--cl-20-mut: oklch(0.5 0.04 138);
font-family: system-ui,-apple-system,'Segoe UI',sans-serif;
color: var(--cl-20-ink);
}
.cl-20__stage {
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
align-content: center;
gap: 18px;
padding: clamp(18px,4vw,52px);
}
.cl-20__panel {
width: min(100%,900px);
display: grid;
gap: clamp(16px,2.6vw,24px);
padding: clamp(20px,3.2vw,34px);
border-radius: 44px;
background: color-mix(in oklch,var(--cl-20-bg) 93%,white);
box-shadow: 0 30px 58px -24px var(--cl-20-shade),inset 0 -16px 28px -16px var(--cl-20-shade),inset 0 18px 28px -12px var(--cl-20-light);
}
.cl-20__head {
display: flex;
flex-wrap: wrap;
gap: 18px;
align-items: center;
justify-content: space-between;
}
.cl-20__h {
font-size: clamp(21px,3.2vw,30px);
font-weight: 780;
letter-spacing: -.022em;
}
.cl-20__lede {
margin-top: 6px;
max-width: 46ch;
font-size: 14px;
line-height: 1.55;
color: var(--cl-20-mut);
text-wrap: pretty;
}
.cl-20__meter {
display: grid;
gap: 4px;
min-width: 150px;
padding: 14px 18px;
border-radius: 26px;
background: color-mix(in oklch,var(--cl-20-bg) 96%,oklch(0.66 0.06 138));
box-shadow: inset 0 10px 18px -8px var(--cl-20-shade),inset 0 -7px 13px -10px var(--cl-20-light);
}
.cl-20__fps {
font-size: 13px;
color: var(--cl-20-mut);
font-variant-numeric: tabular-nums;
}
.cl-20__fps b {
font-size: 30px;
font-weight: 790;
letter-spacing: -.03em;
color: var(--cl-20-ink);
}
.cl-20__jank {
font-size: 11.5px;
color: var(--cl-20-mut);
font-variant-numeric: tabular-nums;
}
.cl-20__jank b {
color: oklch(0.55 0.17 25);
font-weight: 740;
}
.cl-20__graph {
display: flex;
align-items: flex-end;
gap: 2px;
height: 26px;
margin-top: 4px;
}
.cl-20__graph i {
flex: 1;
min-width: 2px;
border-radius: 2px;
background: var(--cl-20-accent);
height: 100%;
transform-origin: bottom;
scale: 1 var(--cl-20-s,.1);
}
.cl-20__modes {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.cl-20__mode {
flex: 1;
min-width: 200px;
min-height: 52px;
padding: 0 20px;
border: 0;
border-radius: 24px;
font: inherit;
font-size: 13.5px;
font-weight: 660;
color: var(--cl-20-mut);
cursor: pointer;
background: color-mix(in oklch,var(--cl-20-bg) 92%,white);
box-shadow: 0 12px 20px -14px var(--cl-20-shade),inset 0 -8px 14px -10px var(--cl-20-shade),inset 0 10px 14px -8px var(--cl-20-light);
transition: color .25s,background .25s,translate .18s,scale .18s cubic-bezier(.2,.8,.3,1.4);
}
.cl-20__mode:hover {
translate: 0 -3px;
color: var(--cl-20-ink);
}
.cl-20__mode:active {
scale: 1 .94;
translate: 0 2px;
}
.cl-20__mode:focus-visible {
outline: 3px solid var(--cl-20-ink);
outline-offset: 3px;
}
.cl-20__mode.is-on {
color: oklch(0.24 0.07 140);
background: var(--cl-20-accent);
box-shadow: 0 16px 26px -12px var(--cl-20-shade),inset 0 -10px 16px -9px var(--cl-20-shade),inset 0 12px 16px -7px var(--cl-20-light);
}
.cl-20__grid {
display: grid;
grid-template-columns: repeat(auto-fit,minmax(64px,1fr));
gap: clamp(8px,1.6vw,14px);
padding: clamp(12px,2vw,18px);
border-radius: 34px;
background: color-mix(in oklch,var(--cl-20-bg) 96%,oklch(0.7 0.05 138));
box-shadow: inset 0 12px 22px -10px var(--cl-20-shade),inset 0 -9px 16px -12px var(--cl-20-light);
}
.cl-20__tile {
position: relative;
aspect-ratio: 1;
border-radius: 22px;
contain: paint;
background: var(--cl-20-clay);
box-shadow: 0 10px 18px -8px var(--cl-20-shade),inset 0 -8px 13px -8px var(--cl-20-shade),inset 0 10px 13px -6px var(--cl-20-light);
}
.cl-20__tile::after {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
opacity: 0;
box-shadow: 0 24px 34px -10px var(--cl-20-shade),inset 0 -10px 16px -8px var(--cl-20-shade),inset 0 14px 18px -6px var(--cl-20-light);
}
.cl-20__panel[data-mode="fast"] .cl-20__tile {
animation: cl-20-lift 1.6s ease-in-out infinite alternate;
animation-delay: calc(var(--cl-20-i) * 55ms);
will-change: transform;
}
.cl-20__panel[data-mode="fast"] .cl-20__tile::after {
animation: cl-20-fade 1.6s ease-in-out infinite alternate;
animation-delay: calc(var(--cl-20-i) * 55ms);
}
.cl-20__panel[data-mode="slow"] .cl-20__tile {
animation: cl-20-shadow 1.6s ease-in-out infinite alternate;
animation-delay: calc(var(--cl-20-i) * 55ms);
}
@keyframes cl-20-lift {
from {
translate: 0 0;
scale: 1;
}
to {
translate: 0 -9px;
scale: 1.05;
}
}
@keyframes cl-20-fade {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes cl-20-shadow {
from {
box-shadow: 0 10px 18px -8px var(--cl-20-shade),inset 0 -8px 13px -8px var(--cl-20-shade),inset 0 10px 13px -6px var(--cl-20-light);
}
to {
box-shadow: 0 34px 56px -8px var(--cl-20-shade),inset 0 -18px 30px -8px var(--cl-20-shade),inset 0 24px 34px -6px var(--cl-20-light);
}
}
.cl-20__hint {
font-size: 12.5px;
line-height: 1.55;
color: var(--cl-20-mut);
text-align: center;
text-wrap: pretty;
}
.cl-20__hint code {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 12px;
}
.cl-20__chip {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
color: var(--cl-20-mut);
padding: 9px 16px;
border-radius: 99px;
text-align: center;
background: oklch(1 0 0/.6);
box-shadow: inset 0 -4px 10px -8px var(--cl-20-shade),inset 0 6px 10px -6px oklch(1 0 0);
}
@media (prefers-reduced-motion: reduce) {
.cl-20__tile,
.cl-20__tile::after {
animation: none !important;
}
.cl-20 * {
transition-duration: .01ms !important;
}
}
```
## JavaScript
```js
(() => {
document.querySelectorAll('.cl-20__panel').forEach((panel) => {
if (panel.dataset.wired) return;
panel.dataset.wired = '1';
const grid = panel.querySelector('.cl-20__grid');
const graph = panel.querySelector('.cl-20__graph');
const fpsVal = panel.querySelector('.cl-20__fpsVal');
const jankVal = panel.querySelector('.cl-20__jankVal');
const TILES = 24, BARS = 28;
for (let i = 0; i < TILES; i++) {
const t = document.createElement('div');
t.className = 'cl-20__tile';
t.style.setProperty('--cl-20-i', String(i));
grid.appendChild(t);
}
const bars = [];
for (let i = 0; i < BARS; i++) {
const b = document.createElement('i');
graph.appendChild(b);
bars.push(b);
}
panel.querySelectorAll('.cl-20__mode').forEach((btn) => {
btn.addEventListener('click', () => {
panel.dataset.mode = btn.dataset.set;
jank = 0;
jankVal.textContent = '0';
panel.querySelectorAll('.cl-20__mode').forEach((b) => {
const on = b === btn;
b.classList.toggle('is-on', on);
b.setAttribute('aria-pressed', String(on));
});
});
});
let frames = 0, jank = 0, last = performance.now(), mark = last, cursor = 0;
const tick = (now) => {
const dt = now - last;
last = now;
frames++;
if (dt > 24) { jank++; jankVal.textContent = String(jank); }
if (now - mark >= 500) {
const fps = Math.round((frames * 1000) / (now - mark));
fpsVal.textContent = String(fps);
bars[cursor % BARS].style.setProperty('--cl-20-s', String(Math.max(0.06, Math.min(1, fps / 60))));
cursor++;
frames = 0;
mark = now;
}
requestAnimationFrame(tick);
};
requestAnimationFrame(tick);
});
})();
```How this works
box-shadow is a paint property. Animating its blur forces the browser to re-rasterize a large blurred region every frame, for every element — on the main thread, at full layer size. transform and opacity are composited: the pixels already exist and the compositor just moves or blends them.
So the fix is to never interpolate a shadow. Pre-render both states and cross-fade them:
.tile{ box-shadow: var(--clay-rest); } /* rasterized once */
.tile::after{ /* the raised shadow, ready */
content:''; position:absolute; inset:0; border-radius:inherit;
box-shadow: var(--clay-raised); opacity:0;
transition: opacity .3s; /* composited blend */
}
.tile:hover{ translate:0 -8px; scale:1.04 } /* composited motion */
.tile:hover::after{ opacity:1 }Both shadows are painted once into their own layers; hover only changes an opacity and a transform. The visual result is identical to animating the shadow, at a fraction of the cost.
The rest of the checklist, all demonstrated here:
/* promote only while it matters */
.tile:hover{ will-change: transform }
/* isolate paint so a repaint cannot escape the tile */
.tile{ contain: paint }
/* one animation driver, many elements: animate a parent, not 24 children */The meter itself is 12 lines of requestAnimationFrame that count frames per second and tally frames longer than 24 ms — a proxy for the jank a user actually feels. Toggle modes while it runs.
Make it yours
- Add a third mode using
filter:drop-shadow()— cheaper than box-shadow on some GPUs, and it follows non-rectangular shapes; measure before adopting. - For scroll-driven clay motion use
animation-timeline:view(), which runs off the main thread entirely. - Reduce shadow area, not just count: a 40px blur on a 200px tile is ~4x the pixels of a 20px blur. Halving the blur is the single biggest win after removing interpolation.
- Above ~40 animated clay objects, flatten to one shared shadow on a container and animate the container instead.
Gotchas — read before shipping
- Do not leave
will-changeon permanently — every promoted element costs GPU memory, and 24 permanent layers is worse than the problem you were fixing. - filter:blur() and backdrop-filter on the same subtree as an animating shadow forces repeated re-composites; never combine them in a hot path.
- Animating
box-shadowbetween different LAYER COUNTS does not interpolate at all — it snaps. Keep the count fixed and change colors (demo 01). - The FPS number is device-dependent: on a 120 Hz display expect 120 in optimized mode. Compare the two modes on the SAME device, not against an absolute target.
- prefers-reduced-motion is not a performance switch — honour it for accessibility regardless of how fast your animation is.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 114+ | 17.5+ | 121+ | 114+ |
Floor set by oklch(), color-mix(), text-wrap as this demo is written. The core technique itself goes back further — Chrome 111+.
requestAnimationFrame, will-change and contain are universal; oklch()/color-mix() theming needs Chrome 111 / Safari 16.2 / Firefox 113. The optimization technique itself works in every engine.