02 / 20
All 20 in 20 Tailwind CSS Claymorphism
- 01Tailwind CSS Claymorphism Box Shadow Recipe (3-Layer Formula)
- 02Tailwind Inner Shadow & Inset Highlight for Clay UI
- 03Tailwind Border Radius Scale & Squircle Corners for Puffy Shapes
- 04Claymorphism vs Neumorphism in Tailwind CSS (Side-by-Side)
- 05Tailwind Glassmorphism + Claymorphism Hybrid Card
- 06Tailwind Clay UI Light Source & Shadow Direction System
- 07Tailwind Elevation Shadow Scale: Clay Depth Tiers
- 08Tailwind CSS v4 Claymorphism: @theme Tokens, @utility & Arbitrary Values
- 09Tailwind 3D Clay Button with Press & Squish Animation
- 10Tailwind Toggle Switch, Checkbox & Segmented Control (Clay Style)
- 11Tailwind Search Bar & Floating Label Input (Clay Inset Style)
- 12Tailwind Badge & Pill Tag Components (Clay Style)
- 13Tailwind Claymorphism Card Component (Container Query Responsive)
- 14Tailwind Modal Dialog with Popover API (Clay Floating Depth)
- 15Tailwind Pricing Table with Monthly/Yearly Toggle (Clay Style)
- 16Tailwind Mobile Bottom Navigation Bar (Clay Tab Bar)
- 17Tailwind App Icon Grid with 3D Hover Tilt (Clay Tiles)
- 18Tailwind Toast Notification & Alert Components (Clay Style)
- 19Tailwind Dark Mode Claymorphism: Shadow Recalibration with light-dark()
- 20Tailwind Animation Performance: GPU Transform vs Box-Shadow (Live FPS Meter)
Tailwind Inner Shadow & Inset Highlight for Clay UI
Isolate the two inset layers and the material becomes obvious: highlight alone looks inflated, shade alone looks hollow, together they look like clay. The comparison balls use v4's dedicated inset-shadow-* utilities, and the hero sphere orbits its light with two @property-registered lengths — a genuinely animatable inset shadow. New: a pause button, because an infinite animation needs one.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="tcl-02 w-full min-h-svh grid place-items-center bg-tcl02-bg bg-[radial-gradient(80%_60%_at_78%_4%,var(--color-tcl02-glow),transparent_62%)] px-5 py-14 font-tcl02-sans text-tcl02-ink" aria-label="Inner shadow and inset highlight demo">
<div class="grid w-full justify-items-center gap-[clamp(32px,5vw,56px)]">
<div class="group/hero grid w-full max-w-[880px] items-center justify-items-center gap-[clamp(24px,4vw,48px)] text-center min-[820px]:grid-cols-[auto_1fr] min-[820px]:text-left">
<div aria-hidden="true" class="aspect-square w-[clamp(168px,24vw,210px)] animate-tcl02-orbit rounded-full bg-tcl02-clay [box-shadow:0_26px_44px_-14px_var(--color-tcl02-shade),inset_var(--tcl02-lx)_var(--tcl02-ly)_30px_-8px_var(--color-tcl02-light),inset_calc(var(--tcl02-lx)*-1)_calc(var(--tcl02-ly)*-1)_30px_-8px_var(--color-tcl02-shade)] group-has-[:checked]/hero:[animation-play-state:paused] motion-reduce:animate-none"></div>
<div class="grid max-w-[46ch] justify-items-center gap-3.5 min-[820px]:justify-items-start">
<h2 class="font-tcl02-display text-[clamp(26px,3.6vw,36px)] font-extrabold leading-[1.08] tracking-[-0.03em] text-balance">Two insets make the material</h2>
<p class="text-[15px] leading-[1.65] text-tcl02-mut text-pretty">The highlight is the lit face, the shade is the far wall. Keep them diametrically opposed and the object holds one light source — even while the light orbits.</p>
<div class="flex flex-wrap items-center gap-2.5">
<label class="inline-flex min-h-11 cursor-pointer items-center gap-2.5 rounded-full bg-tcl02-soft px-4.5 text-[13.5px] font-semibold shadow-tcl02-pill transition-transform hover:-translate-y-0.5 has-[:focus-visible]:outline-3 has-[:focus-visible]:outline-offset-3 has-[:focus-visible]:outline-tcl02-ink">
<input type="checkbox" class="peer sr-only">
<svg class="size-3.5 peer-checked:hidden" viewBox="0 0 16 16" aria-hidden="true"><rect x="3.5" y="2.5" width="3" height="11" rx="1" fill="currentColor"/><rect x="9.5" y="2.5" width="3" height="11" rx="1" fill="currentColor"/></svg>
<svg class="hidden size-3.5 peer-checked:block" viewBox="0 0 16 16" aria-hidden="true"><path d="M4.5 2.8v10.4a.6.6 0 0 0 .9.5l8.2-5.2a.6.6 0 0 0 0-1L5.4 2.3a.6.6 0 0 0-.9.5z" fill="currentColor"/></svg>
<span class="peer-checked:hidden">Pause light</span><span class="hidden peer-checked:inline">Play light</span>
</label>
<p class="rounded-full bg-tcl02-soft px-4 py-2.5 font-tcl02-mono text-[11.5px] shadow-tcl02-pill">@property <length> → animatable inset</p>
</div>
</div>
</div>
<ul class="grid w-full max-w-[780px] grid-cols-[repeat(auto-fit,minmax(160px,1fr))] gap-[clamp(16px,3vw,28px)]">
<li class="grid justify-items-center gap-2 rounded-[34px] bg-tcl02-soft px-4.5 pt-7 pb-6 text-center shadow-tcl02-card transition-transform duration-300 ease-[cubic-bezier(.34,1.3,.5,1)] hover:-translate-y-1.5 motion-reduce:transition-none">
<span aria-hidden="true" class="mb-2 size-[84px] rounded-full bg-tcl02-clay inset-shadow-tcl02-light"></span>
<b class="font-tcl02-display text-[15px] font-bold tracking-[-0.01em]">Highlight only</b>
<span class="text-[12.5px] text-tcl02-mut">inflated · no thickness</span>
</li>
<li class="grid justify-items-center gap-2 rounded-[34px] bg-tcl02-soft px-4.5 pt-7 pb-6 text-center shadow-tcl02-card transition-transform duration-300 ease-[cubic-bezier(.34,1.3,.5,1)] hover:-translate-y-1.5 motion-reduce:transition-none">
<span aria-hidden="true" class="mb-2 size-[84px] rounded-full bg-tcl02-clay inset-shadow-tcl02-dark"></span>
<b class="font-tcl02-display text-[15px] font-bold tracking-[-0.01em]">Shade only</b>
<span class="text-[12.5px] text-tcl02-mut">hollow · reads as a bowl</span>
</li>
<li class="grid justify-items-center gap-2 rounded-[34px] bg-tcl02-soft px-4.5 pt-7 pb-6 text-center shadow-tcl02-card ring-2 ring-tcl02-clay/60 transition-transform duration-300 ease-[cubic-bezier(.34,1.3,.5,1)] hover:-translate-y-1.5 motion-reduce:transition-none">
<span aria-hidden="true" class="mb-2 size-[84px] rounded-full bg-tcl02-clay inset-shadow-tcl02-both shadow-tcl02-ball"></span>
<b class="font-tcl02-display text-[15px] font-bold tracking-[-0.01em]">Both layers</b>
<span class="text-[12.5px] text-tcl02-mut">clay · pressable mass</span>
</li>
</ul>
</div>
</section><section class="tcl-02 w-full min-h-svh grid place-items-center bg-tcl02-bg bg-[radial-gradient(80%_60%_at_78%_4%,var(--color-tcl02-glow),transparent_62%)] px-5 py-14 font-tcl02-sans text-tcl02-ink" aria-label="Inner shadow and inset highlight demo">
<div class="grid w-full justify-items-center gap-[clamp(32px,5vw,56px)]">
<div class="group/hero grid w-full max-w-[880px] items-center justify-items-center gap-[clamp(24px,4vw,48px)] text-center min-[820px]:grid-cols-[auto_1fr] min-[820px]:text-left">
<div aria-hidden="true" class="aspect-square w-[clamp(168px,24vw,210px)] animate-tcl02-orbit rounded-full bg-tcl02-clay [box-shadow:0_26px_44px_-14px_var(--color-tcl02-shade),inset_var(--tcl02-lx)_var(--tcl02-ly)_30px_-8px_var(--color-tcl02-light),inset_calc(var(--tcl02-lx)*-1)_calc(var(--tcl02-ly)*-1)_30px_-8px_var(--color-tcl02-shade)] group-has-[:checked]/hero:[animation-play-state:paused] motion-reduce:animate-none"></div>
<div class="grid max-w-[46ch] justify-items-center gap-3.5 min-[820px]:justify-items-start">
<h2 class="font-tcl02-display text-[clamp(26px,3.6vw,36px)] font-extrabold leading-[1.08] tracking-[-0.03em] text-balance">Two insets make the material</h2>
<p class="text-[15px] leading-[1.65] text-tcl02-mut text-pretty">The highlight is the lit face, the shade is the far wall. Keep them diametrically opposed and the object holds one light source — even while the light orbits.</p>
<div class="flex flex-wrap items-center gap-2.5">
<label class="inline-flex min-h-11 cursor-pointer items-center gap-2.5 rounded-full bg-tcl02-soft px-4.5 text-[13.5px] font-semibold shadow-tcl02-pill transition-transform hover:-translate-y-0.5 has-[:focus-visible]:outline-3 has-[:focus-visible]:outline-offset-3 has-[:focus-visible]:outline-tcl02-ink">
<input type="checkbox" class="peer sr-only">
<svg class="size-3.5 peer-checked:hidden" viewBox="0 0 16 16" aria-hidden="true"><rect x="3.5" y="2.5" width="3" height="11" rx="1" fill="currentColor"/><rect x="9.5" y="2.5" width="3" height="11" rx="1" fill="currentColor"/></svg>
<svg class="hidden size-3.5 peer-checked:block" viewBox="0 0 16 16" aria-hidden="true"><path d="M4.5 2.8v10.4a.6.6 0 0 0 .9.5l8.2-5.2a.6.6 0 0 0 0-1L5.4 2.3a.6.6 0 0 0-.9.5z" fill="currentColor"/></svg>
<span class="peer-checked:hidden">Pause light</span><span class="hidden peer-checked:inline">Play light</span>
</label>
<p class="rounded-full bg-tcl02-soft px-4 py-2.5 font-tcl02-mono text-[11.5px] shadow-tcl02-pill">@property <length> → animatable inset</p>
</div>
</div>
</div>
<ul class="grid w-full max-w-[780px] grid-cols-[repeat(auto-fit,minmax(160px,1fr))] gap-[clamp(16px,3vw,28px)]">
<li class="grid justify-items-center gap-2 rounded-[34px] bg-tcl02-soft px-4.5 pt-7 pb-6 text-center shadow-tcl02-card transition-transform duration-300 ease-[cubic-bezier(.34,1.3,.5,1)] hover:-translate-y-1.5 motion-reduce:transition-none">
<span aria-hidden="true" class="mb-2 size-[84px] rounded-full bg-tcl02-clay inset-shadow-tcl02-light"></span>
<b class="font-tcl02-display text-[15px] font-bold tracking-[-0.01em]">Highlight only</b>
<span class="text-[12.5px] text-tcl02-mut">inflated · no thickness</span>
</li>
<li class="grid justify-items-center gap-2 rounded-[34px] bg-tcl02-soft px-4.5 pt-7 pb-6 text-center shadow-tcl02-card transition-transform duration-300 ease-[cubic-bezier(.34,1.3,.5,1)] hover:-translate-y-1.5 motion-reduce:transition-none">
<span aria-hidden="true" class="mb-2 size-[84px] rounded-full bg-tcl02-clay inset-shadow-tcl02-dark"></span>
<b class="font-tcl02-display text-[15px] font-bold tracking-[-0.01em]">Shade only</b>
<span class="text-[12.5px] text-tcl02-mut">hollow · reads as a bowl</span>
</li>
<li class="grid justify-items-center gap-2 rounded-[34px] bg-tcl02-soft px-4.5 pt-7 pb-6 text-center shadow-tcl02-card ring-2 ring-tcl02-clay/60 transition-transform duration-300 ease-[cubic-bezier(.34,1.3,.5,1)] hover:-translate-y-1.5 motion-reduce:transition-none">
<span aria-hidden="true" class="mb-2 size-[84px] rounded-full bg-tcl02-clay inset-shadow-tcl02-both shadow-tcl02-ball"></span>
<b class="font-tcl02-display text-[15px] font-bold tracking-[-0.01em]">Both layers</b>
<span class="text-[12.5px] text-tcl02-mut">clay · pressable mass</span>
</li>
</ul>
</div>
</section>@import "tailwindcss";
@property --tcl02-lx {
syntax: '<length>';
inherits: false;
initial-value: 0px;
}
@property --tcl02-ly {
syntax: '<length>';
inherits: false;
initial-value: 20px;
}
@theme {
--color-tcl02-bg: #e8e9ff;
--color-tcl02-bg: oklch(0.94 0.03 285);
--color-tcl02-glow: #faf2ff;
--color-tcl02-glow: oklch(0.975 0.035 300);
--color-tcl02-clay: #bfb2f9;
--color-tcl02-clay: oklch(0.8 0.1 292);
--color-tcl02-shade: color-mix(in oklch, var(--color-tcl02-clay) 45%, oklch(0.38 0.13 290));
--color-tcl02-light: #fef9fff2;
--color-tcl02-light: oklch(0.99 0.02 300 / 0.95);
--color-tcl02-ink: #2a2344;
--color-tcl02-ink: oklch(0.28 0.06 292);
--color-tcl02-mut: #595277;
--color-tcl02-mut: oklch(0.46 0.06 292);
--color-tcl02-soft: color-mix(in oklch, var(--color-tcl02-bg) 84%, white);
--font-tcl02-display: 'Bricolage Grotesque', system-ui, sans-serif;
--font-tcl02-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--font-tcl02-mono: 'JetBrains Mono', ui-monospace, monospace;
--inset-shadow-tcl02-light: inset 0 20px 28px -8px var(--color-tcl02-light);
--inset-shadow-tcl02-dark: inset 0 -20px 28px -8px var(--color-tcl02-shade);
--inset-shadow-tcl02-both: inset 0 20px 28px -8px var(--color-tcl02-light), inset 0 -20px 28px -8px var(--color-tcl02-shade);
--shadow-tcl02-ball: 0 12px 20px -10px var(--color-tcl02-shade);
--shadow-tcl02-card: 0 18px 34px -18px var(--color-tcl02-shade), inset 0 -10px 18px -12px var(--color-tcl02-shade), inset 0 12px 18px -10px var(--color-tcl02-light);
--shadow-tcl02-pill: inset 0 -6px 12px -8px var(--color-tcl02-shade), inset 0 8px 12px -6px var(--color-tcl02-light);
--animate-tcl02-orbit: tcl02-orbit 9s linear infinite;
@keyframes tcl02-orbit {
0%,
100% {
--tcl02-lx: 0px;
--tcl02-ly: 20px;
}
25% {
--tcl02-lx: 20px;
--tcl02-ly: 0px;
}
50% {
--tcl02-lx: 0px;
--tcl02-ly: -20px;
}
75% {
--tcl02-lx: -20px;
--tcl02-ly: 0px;
}
}
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's utilities, so the
stage is sized before the browser build compiles. */
.tcl-02 {
width: 100%;
min-height: 100svh;
}@import "tailwindcss";
@property --tcl02-lx {
syntax: '<length>';
inherits: false;
initial-value: 0px;
}
@property --tcl02-ly {
syntax: '<length>';
inherits: false;
initial-value: 20px;
}
@theme {
--color-tcl02-bg: #e8e9ff;
--color-tcl02-bg: oklch(0.94 0.03 285);
--color-tcl02-glow: #faf2ff;
--color-tcl02-glow: oklch(0.975 0.035 300);
--color-tcl02-clay: #bfb2f9;
--color-tcl02-clay: oklch(0.8 0.1 292);
--color-tcl02-shade: color-mix(in oklch, var(--color-tcl02-clay) 45%, oklch(0.38 0.13 290));
--color-tcl02-light: #fef9fff2;
--color-tcl02-light: oklch(0.99 0.02 300 / 0.95);
--color-tcl02-ink: #2a2344;
--color-tcl02-ink: oklch(0.28 0.06 292);
--color-tcl02-mut: #595277;
--color-tcl02-mut: oklch(0.46 0.06 292);
--color-tcl02-soft: color-mix(in oklch, var(--color-tcl02-bg) 84%, white);
--font-tcl02-display: 'Bricolage Grotesque', system-ui, sans-serif;
--font-tcl02-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--font-tcl02-mono: 'JetBrains Mono', ui-monospace, monospace;
--inset-shadow-tcl02-light: inset 0 20px 28px -8px var(--color-tcl02-light);
--inset-shadow-tcl02-dark: inset 0 -20px 28px -8px var(--color-tcl02-shade);
--inset-shadow-tcl02-both: inset 0 20px 28px -8px var(--color-tcl02-light), inset 0 -20px 28px -8px var(--color-tcl02-shade);
--shadow-tcl02-ball: 0 12px 20px -10px var(--color-tcl02-shade);
--shadow-tcl02-card: 0 18px 34px -18px var(--color-tcl02-shade), inset 0 -10px 18px -12px var(--color-tcl02-shade), inset 0 12px 18px -10px var(--color-tcl02-light);
--shadow-tcl02-pill: inset 0 -6px 12px -8px var(--color-tcl02-shade), inset 0 8px 12px -6px var(--color-tcl02-light);
--animate-tcl02-orbit: tcl02-orbit 9s linear infinite;
@keyframes tcl02-orbit {
0%,
100% {
--tcl02-lx: 0px;
--tcl02-ly: 20px;
}
25% {
--tcl02-lx: 20px;
--tcl02-ly: 0px;
}
50% {
--tcl02-lx: 0px;
--tcl02-ly: -20px;
}
75% {
--tcl02-lx: -20px;
--tcl02-ly: 0px;
}
}
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's utilities, so the
stage is sized before the browser build compiles. */
.tcl-02 {
width: 100%;
min-height: 100svh;
}Setup — do this once per project
Skip this if your project already runs Tailwind v4.
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>Compiles in the browser — perfect for a CodePen or a quick test, not for production.
npm install tailwindcss @tailwindcss/vite
npm install tailwindcss @tailwindcss/vite// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})Any v4 setup works — Vite, PostCSS or the CLI. Nothing in this demo is bundler-specific.
@import "tailwindcss";
@property --tcl02-lx {
syntax: '<length>';
inherits: false;
initial-value: 0px;
}
@property --tcl02-ly {
syntax: '<length>';
inherits: false;
initial-value: 20px;
}
@theme {
--color-tcl02-bg: #e8e9ff;
--color-tcl02-bg: oklch(0.94 0.03 285);
--color-tcl02-glow: #faf2ff;
--color-tcl02-glow: oklch(0.975 0.035 300);
--color-tcl02-clay: #bfb2f9;
--color-tcl02-clay: oklch(0.8 0.1 292);
--color-tcl02-shade: color-mix(in oklch, var(--color-tcl02-clay) 45%, oklch(0.38 0.13 290));
--color-tcl02-light: #fef9fff2;
--color-tcl02-light: oklch(0.99 0.02 300 / 0.95);
--color-tcl02-ink: #2a2344;
--color-tcl02-ink: oklch(0.28 0.06 292);
--color-tcl02-mut: #595277;
--color-tcl02-mut: oklch(0.46 0.06 292);
--color-tcl02-soft: color-mix(in oklch, var(--color-tcl02-bg) 84%, white);
--font-tcl02-display: 'Bricolage Grotesque', system-ui, sans-serif;
--font-tcl02-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--font-tcl02-mono: 'JetBrains Mono', ui-monospace, monospace;
--inset-shadow-tcl02-light: inset 0 20px 28px -8px var(--color-tcl02-light);
--inset-shadow-tcl02-dark: inset 0 -20px 28px -8px var(--color-tcl02-shade);
--inset-shadow-tcl02-both: inset 0 20px 28px -8px var(--color-tcl02-light), inset 0 -20px 28px -8px var(--color-tcl02-shade);
--shadow-tcl02-ball: 0 12px 20px -10px var(--color-tcl02-shade);
--shadow-tcl02-card: 0 18px 34px -18px var(--color-tcl02-shade), inset 0 -10px 18px -12px var(--color-tcl02-shade), inset 0 12px 18px -10px var(--color-tcl02-light);
--shadow-tcl02-pill: inset 0 -6px 12px -8px var(--color-tcl02-shade), inset 0 8px 12px -6px var(--color-tcl02-light);
--animate-tcl02-orbit: tcl02-orbit 9s linear infinite;
@keyframes tcl02-orbit {
0%,
100% {
--tcl02-lx: 0px;
--tcl02-ly: 20px;
}
25% {
--tcl02-lx: 20px;
--tcl02-ly: 0px;
}
50% {
--tcl02-lx: 0px;
--tcl02-ly: -20px;
}
75% {
--tcl02-lx: -20px;
--tcl02-ly: 0px;
}
}
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's utilities, so the
stage is sized before the browser build compiles. */
.tcl-02 {
width: 100%;
min-height: 100svh;
}@import "tailwindcss";
@property --tcl02-lx {
syntax: '<length>';
inherits: false;
initial-value: 0px;
}
@property --tcl02-ly {
syntax: '<length>';
inherits: false;
initial-value: 20px;
}
@theme {
--color-tcl02-bg: #e8e9ff;
--color-tcl02-bg: oklch(0.94 0.03 285);
--color-tcl02-glow: #faf2ff;
--color-tcl02-glow: oklch(0.975 0.035 300);
--color-tcl02-clay: #bfb2f9;
--color-tcl02-clay: oklch(0.8 0.1 292);
--color-tcl02-shade: color-mix(in oklch, var(--color-tcl02-clay) 45%, oklch(0.38 0.13 290));
--color-tcl02-light: #fef9fff2;
--color-tcl02-light: oklch(0.99 0.02 300 / 0.95);
--color-tcl02-ink: #2a2344;
--color-tcl02-ink: oklch(0.28 0.06 292);
--color-tcl02-mut: #595277;
--color-tcl02-mut: oklch(0.46 0.06 292);
--color-tcl02-soft: color-mix(in oklch, var(--color-tcl02-bg) 84%, white);
--font-tcl02-display: 'Bricolage Grotesque', system-ui, sans-serif;
--font-tcl02-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--font-tcl02-mono: 'JetBrains Mono', ui-monospace, monospace;
--inset-shadow-tcl02-light: inset 0 20px 28px -8px var(--color-tcl02-light);
--inset-shadow-tcl02-dark: inset 0 -20px 28px -8px var(--color-tcl02-shade);
--inset-shadow-tcl02-both: inset 0 20px 28px -8px var(--color-tcl02-light), inset 0 -20px 28px -8px var(--color-tcl02-shade);
--shadow-tcl02-ball: 0 12px 20px -10px var(--color-tcl02-shade);
--shadow-tcl02-card: 0 18px 34px -18px var(--color-tcl02-shade), inset 0 -10px 18px -12px var(--color-tcl02-shade), inset 0 12px 18px -10px var(--color-tcl02-light);
--shadow-tcl02-pill: inset 0 -6px 12px -8px var(--color-tcl02-shade), inset 0 8px 12px -6px var(--color-tcl02-light);
--animate-tcl02-orbit: tcl02-orbit 9s linear infinite;
@keyframes tcl02-orbit {
0%,
100% {
--tcl02-lx: 0px;
--tcl02-ly: 20px;
}
25% {
--tcl02-lx: 20px;
--tcl02-ly: 0px;
}
50% {
--tcl02-lx: 0px;
--tcl02-ly: -20px;
}
75% {
--tcl02-lx: -20px;
--tcl02-ly: 0px;
}
}
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's utilities, so the
stage is sized before the browser build compiles. */
.tcl-02 {
width: 100%;
min-height: 100svh;
}<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,500..800&family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600;700&display=swap">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,500..800&family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600;700&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- The three
.ball--*modifiers became v4'sinset-shadow-*scale — the first place a clay port benefits from v4's split between outer and inset shadows. @propertystays in CSS (there is no utility for an at-rule); the keyframes moved into@themebehind--animate-tcl02-orbit.- UI upgrade from the CSS original: a Pause / Play light toggle (a hidden checkbox read by
group-has-[:checked]/hero:) — infinite motion now has an off switch, per WCAG 2.2.2. - UI upgrade: the "Both layers" card carries a clay ring so the winning recipe is marked, and muted copy rose to 5.4:1.
Here's a working CSS demo 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: Tailwind Inner Shadow & Inset Highlight for Clay UI
Source: https://codefronts.com/design-styles/tailwind-css-claymorphism/tailwind-inner-shadow-inset-highlight-clay-ui/
Isolate the two inset layers and the material becomes obvious: highlight alone looks inflated, shade alone looks hollow, together they look like clay. The comparison balls use v4's dedicated inset-shadow-* utilities, and the hero sphere orbits its light with two @property-registered lengths — a genuinely animatable inset shadow. New: a pause button, because an infinite animation needs one.
## HTML
```html
<section class="tcl-02 w-full min-h-svh grid place-items-center bg-tcl02-bg bg-[radial-gradient(80%_60%_at_78%_4%,var(--color-tcl02-glow),transparent_62%)] px-5 py-14 font-tcl02-sans text-tcl02-ink" aria-label="Inner shadow and inset highlight demo">
<div class="grid w-full justify-items-center gap-[clamp(32px,5vw,56px)]">
<div class="group/hero grid w-full max-w-[880px] items-center justify-items-center gap-[clamp(24px,4vw,48px)] text-center min-[820px]:grid-cols-[auto_1fr] min-[820px]:text-left">
<div aria-hidden="true" class="aspect-square w-[clamp(168px,24vw,210px)] animate-tcl02-orbit rounded-full bg-tcl02-clay [box-shadow:0_26px_44px_-14px_var(--color-tcl02-shade),inset_var(--tcl02-lx)_var(--tcl02-ly)_30px_-8px_var(--color-tcl02-light),inset_calc(var(--tcl02-lx)*-1)_calc(var(--tcl02-ly)*-1)_30px_-8px_var(--color-tcl02-shade)] group-has-[:checked]/hero:[animation-play-state:paused] motion-reduce:animate-none"></div>
<div class="grid max-w-[46ch] justify-items-center gap-3.5 min-[820px]:justify-items-start">
<h2 class="font-tcl02-display text-[clamp(26px,3.6vw,36px)] font-extrabold leading-[1.08] tracking-[-0.03em] text-balance">Two insets make the material</h2>
<p class="text-[15px] leading-[1.65] text-tcl02-mut text-pretty">The highlight is the lit face, the shade is the far wall. Keep them diametrically opposed and the object holds one light source — even while the light orbits.</p>
<div class="flex flex-wrap items-center gap-2.5">
<label class="inline-flex min-h-11 cursor-pointer items-center gap-2.5 rounded-full bg-tcl02-soft px-4.5 text-[13.5px] font-semibold shadow-tcl02-pill transition-transform hover:-translate-y-0.5 has-[:focus-visible]:outline-3 has-[:focus-visible]:outline-offset-3 has-[:focus-visible]:outline-tcl02-ink">
<input type="checkbox" class="peer sr-only">
<svg class="size-3.5 peer-checked:hidden" viewBox="0 0 16 16" aria-hidden="true"><rect x="3.5" y="2.5" width="3" height="11" rx="1" fill="currentColor"/><rect x="9.5" y="2.5" width="3" height="11" rx="1" fill="currentColor"/></svg>
<svg class="hidden size-3.5 peer-checked:block" viewBox="0 0 16 16" aria-hidden="true"><path d="M4.5 2.8v10.4a.6.6 0 0 0 .9.5l8.2-5.2a.6.6 0 0 0 0-1L5.4 2.3a.6.6 0 0 0-.9.5z" fill="currentColor"/></svg>
<span class="peer-checked:hidden">Pause light</span><span class="hidden peer-checked:inline">Play light</span>
</label>
<p class="rounded-full bg-tcl02-soft px-4 py-2.5 font-tcl02-mono text-[11.5px] shadow-tcl02-pill">@property <length> → animatable inset</p>
</div>
</div>
</div>
<ul class="grid w-full max-w-[780px] grid-cols-[repeat(auto-fit,minmax(160px,1fr))] gap-[clamp(16px,3vw,28px)]">
<li class="grid justify-items-center gap-2 rounded-[34px] bg-tcl02-soft px-4.5 pt-7 pb-6 text-center shadow-tcl02-card transition-transform duration-300 ease-[cubic-bezier(.34,1.3,.5,1)] hover:-translate-y-1.5 motion-reduce:transition-none">
<span aria-hidden="true" class="mb-2 size-[84px] rounded-full bg-tcl02-clay inset-shadow-tcl02-light"></span>
<b class="font-tcl02-display text-[15px] font-bold tracking-[-0.01em]">Highlight only</b>
<span class="text-[12.5px] text-tcl02-mut">inflated · no thickness</span>
</li>
<li class="grid justify-items-center gap-2 rounded-[34px] bg-tcl02-soft px-4.5 pt-7 pb-6 text-center shadow-tcl02-card transition-transform duration-300 ease-[cubic-bezier(.34,1.3,.5,1)] hover:-translate-y-1.5 motion-reduce:transition-none">
<span aria-hidden="true" class="mb-2 size-[84px] rounded-full bg-tcl02-clay inset-shadow-tcl02-dark"></span>
<b class="font-tcl02-display text-[15px] font-bold tracking-[-0.01em]">Shade only</b>
<span class="text-[12.5px] text-tcl02-mut">hollow · reads as a bowl</span>
</li>
<li class="grid justify-items-center gap-2 rounded-[34px] bg-tcl02-soft px-4.5 pt-7 pb-6 text-center shadow-tcl02-card ring-2 ring-tcl02-clay/60 transition-transform duration-300 ease-[cubic-bezier(.34,1.3,.5,1)] hover:-translate-y-1.5 motion-reduce:transition-none">
<span aria-hidden="true" class="mb-2 size-[84px] rounded-full bg-tcl02-clay inset-shadow-tcl02-both shadow-tcl02-ball"></span>
<b class="font-tcl02-display text-[15px] font-bold tracking-[-0.01em]">Both layers</b>
<span class="text-[12.5px] text-tcl02-mut">clay · pressable mass</span>
</li>
</ul>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@property --tcl02-lx {
syntax: '<length>';
inherits: false;
initial-value: 0px;
}
@property --tcl02-ly {
syntax: '<length>';
inherits: false;
initial-value: 20px;
}
@theme {
--color-tcl02-bg: #e8e9ff;
--color-tcl02-bg: oklch(0.94 0.03 285);
--color-tcl02-glow: #faf2ff;
--color-tcl02-glow: oklch(0.975 0.035 300);
--color-tcl02-clay: #bfb2f9;
--color-tcl02-clay: oklch(0.8 0.1 292);
--color-tcl02-shade: color-mix(in oklch, var(--color-tcl02-clay) 45%, oklch(0.38 0.13 290));
--color-tcl02-light: #fef9fff2;
--color-tcl02-light: oklch(0.99 0.02 300 / 0.95);
--color-tcl02-ink: #2a2344;
--color-tcl02-ink: oklch(0.28 0.06 292);
--color-tcl02-mut: #595277;
--color-tcl02-mut: oklch(0.46 0.06 292);
--color-tcl02-soft: color-mix(in oklch, var(--color-tcl02-bg) 84%, white);
--font-tcl02-display: 'Bricolage Grotesque', system-ui, sans-serif;
--font-tcl02-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--font-tcl02-mono: 'JetBrains Mono', ui-monospace, monospace;
--inset-shadow-tcl02-light: inset 0 20px 28px -8px var(--color-tcl02-light);
--inset-shadow-tcl02-dark: inset 0 -20px 28px -8px var(--color-tcl02-shade);
--inset-shadow-tcl02-both: inset 0 20px 28px -8px var(--color-tcl02-light), inset 0 -20px 28px -8px var(--color-tcl02-shade);
--shadow-tcl02-ball: 0 12px 20px -10px var(--color-tcl02-shade);
--shadow-tcl02-card: 0 18px 34px -18px var(--color-tcl02-shade), inset 0 -10px 18px -12px var(--color-tcl02-shade), inset 0 12px 18px -10px var(--color-tcl02-light);
--shadow-tcl02-pill: inset 0 -6px 12px -8px var(--color-tcl02-shade), inset 0 8px 12px -6px var(--color-tcl02-light);
--animate-tcl02-orbit: tcl02-orbit 9s linear infinite;
@keyframes tcl02-orbit {
0%,
100% {
--tcl02-lx: 0px;
--tcl02-ly: 20px;
}
25% {
--tcl02-lx: 20px;
--tcl02-ly: 0px;
}
50% {
--tcl02-lx: 0px;
--tcl02-ly: -20px;
}
75% {
--tcl02-lx: -20px;
--tcl02-ly: 0px;
}
}
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's utilities, so the
stage is sized before the browser build compiles. */
.tcl-02 {
width: 100%;
min-height: 100svh;
}
```Here's a working CSS demo 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: Tailwind Inner Shadow & Inset Highlight for Clay UI
Source: https://codefronts.com/design-styles/tailwind-css-claymorphism/tailwind-inner-shadow-inset-highlight-clay-ui/
Isolate the two inset layers and the material becomes obvious: highlight alone looks inflated, shade alone looks hollow, together they look like clay. The comparison balls use v4's dedicated inset-shadow-* utilities, and the hero sphere orbits its light with two @property-registered lengths — a genuinely animatable inset shadow. New: a pause button, because an infinite animation needs one.
## HTML
```html
<section class="tcl-02 w-full min-h-svh grid place-items-center bg-tcl02-bg bg-[radial-gradient(80%_60%_at_78%_4%,var(--color-tcl02-glow),transparent_62%)] px-5 py-14 font-tcl02-sans text-tcl02-ink" aria-label="Inner shadow and inset highlight demo">
<div class="grid w-full justify-items-center gap-[clamp(32px,5vw,56px)]">
<div class="group/hero grid w-full max-w-[880px] items-center justify-items-center gap-[clamp(24px,4vw,48px)] text-center min-[820px]:grid-cols-[auto_1fr] min-[820px]:text-left">
<div aria-hidden="true" class="aspect-square w-[clamp(168px,24vw,210px)] animate-tcl02-orbit rounded-full bg-tcl02-clay [box-shadow:0_26px_44px_-14px_var(--color-tcl02-shade),inset_var(--tcl02-lx)_var(--tcl02-ly)_30px_-8px_var(--color-tcl02-light),inset_calc(var(--tcl02-lx)*-1)_calc(var(--tcl02-ly)*-1)_30px_-8px_var(--color-tcl02-shade)] group-has-[:checked]/hero:[animation-play-state:paused] motion-reduce:animate-none"></div>
<div class="grid max-w-[46ch] justify-items-center gap-3.5 min-[820px]:justify-items-start">
<h2 class="font-tcl02-display text-[clamp(26px,3.6vw,36px)] font-extrabold leading-[1.08] tracking-[-0.03em] text-balance">Two insets make the material</h2>
<p class="text-[15px] leading-[1.65] text-tcl02-mut text-pretty">The highlight is the lit face, the shade is the far wall. Keep them diametrically opposed and the object holds one light source — even while the light orbits.</p>
<div class="flex flex-wrap items-center gap-2.5">
<label class="inline-flex min-h-11 cursor-pointer items-center gap-2.5 rounded-full bg-tcl02-soft px-4.5 text-[13.5px] font-semibold shadow-tcl02-pill transition-transform hover:-translate-y-0.5 has-[:focus-visible]:outline-3 has-[:focus-visible]:outline-offset-3 has-[:focus-visible]:outline-tcl02-ink">
<input type="checkbox" class="peer sr-only">
<svg class="size-3.5 peer-checked:hidden" viewBox="0 0 16 16" aria-hidden="true"><rect x="3.5" y="2.5" width="3" height="11" rx="1" fill="currentColor"/><rect x="9.5" y="2.5" width="3" height="11" rx="1" fill="currentColor"/></svg>
<svg class="hidden size-3.5 peer-checked:block" viewBox="0 0 16 16" aria-hidden="true"><path d="M4.5 2.8v10.4a.6.6 0 0 0 .9.5l8.2-5.2a.6.6 0 0 0 0-1L5.4 2.3a.6.6 0 0 0-.9.5z" fill="currentColor"/></svg>
<span class="peer-checked:hidden">Pause light</span><span class="hidden peer-checked:inline">Play light</span>
</label>
<p class="rounded-full bg-tcl02-soft px-4 py-2.5 font-tcl02-mono text-[11.5px] shadow-tcl02-pill">@property <length> → animatable inset</p>
</div>
</div>
</div>
<ul class="grid w-full max-w-[780px] grid-cols-[repeat(auto-fit,minmax(160px,1fr))] gap-[clamp(16px,3vw,28px)]">
<li class="grid justify-items-center gap-2 rounded-[34px] bg-tcl02-soft px-4.5 pt-7 pb-6 text-center shadow-tcl02-card transition-transform duration-300 ease-[cubic-bezier(.34,1.3,.5,1)] hover:-translate-y-1.5 motion-reduce:transition-none">
<span aria-hidden="true" class="mb-2 size-[84px] rounded-full bg-tcl02-clay inset-shadow-tcl02-light"></span>
<b class="font-tcl02-display text-[15px] font-bold tracking-[-0.01em]">Highlight only</b>
<span class="text-[12.5px] text-tcl02-mut">inflated · no thickness</span>
</li>
<li class="grid justify-items-center gap-2 rounded-[34px] bg-tcl02-soft px-4.5 pt-7 pb-6 text-center shadow-tcl02-card transition-transform duration-300 ease-[cubic-bezier(.34,1.3,.5,1)] hover:-translate-y-1.5 motion-reduce:transition-none">
<span aria-hidden="true" class="mb-2 size-[84px] rounded-full bg-tcl02-clay inset-shadow-tcl02-dark"></span>
<b class="font-tcl02-display text-[15px] font-bold tracking-[-0.01em]">Shade only</b>
<span class="text-[12.5px] text-tcl02-mut">hollow · reads as a bowl</span>
</li>
<li class="grid justify-items-center gap-2 rounded-[34px] bg-tcl02-soft px-4.5 pt-7 pb-6 text-center shadow-tcl02-card ring-2 ring-tcl02-clay/60 transition-transform duration-300 ease-[cubic-bezier(.34,1.3,.5,1)] hover:-translate-y-1.5 motion-reduce:transition-none">
<span aria-hidden="true" class="mb-2 size-[84px] rounded-full bg-tcl02-clay inset-shadow-tcl02-both shadow-tcl02-ball"></span>
<b class="font-tcl02-display text-[15px] font-bold tracking-[-0.01em]">Both layers</b>
<span class="text-[12.5px] text-tcl02-mut">clay · pressable mass</span>
</li>
</ul>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@property --tcl02-lx {
syntax: '<length>';
inherits: false;
initial-value: 0px;
}
@property --tcl02-ly {
syntax: '<length>';
inherits: false;
initial-value: 20px;
}
@theme {
--color-tcl02-bg: #e8e9ff;
--color-tcl02-bg: oklch(0.94 0.03 285);
--color-tcl02-glow: #faf2ff;
--color-tcl02-glow: oklch(0.975 0.035 300);
--color-tcl02-clay: #bfb2f9;
--color-tcl02-clay: oklch(0.8 0.1 292);
--color-tcl02-shade: color-mix(in oklch, var(--color-tcl02-clay) 45%, oklch(0.38 0.13 290));
--color-tcl02-light: #fef9fff2;
--color-tcl02-light: oklch(0.99 0.02 300 / 0.95);
--color-tcl02-ink: #2a2344;
--color-tcl02-ink: oklch(0.28 0.06 292);
--color-tcl02-mut: #595277;
--color-tcl02-mut: oklch(0.46 0.06 292);
--color-tcl02-soft: color-mix(in oklch, var(--color-tcl02-bg) 84%, white);
--font-tcl02-display: 'Bricolage Grotesque', system-ui, sans-serif;
--font-tcl02-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--font-tcl02-mono: 'JetBrains Mono', ui-monospace, monospace;
--inset-shadow-tcl02-light: inset 0 20px 28px -8px var(--color-tcl02-light);
--inset-shadow-tcl02-dark: inset 0 -20px 28px -8px var(--color-tcl02-shade);
--inset-shadow-tcl02-both: inset 0 20px 28px -8px var(--color-tcl02-light), inset 0 -20px 28px -8px var(--color-tcl02-shade);
--shadow-tcl02-ball: 0 12px 20px -10px var(--color-tcl02-shade);
--shadow-tcl02-card: 0 18px 34px -18px var(--color-tcl02-shade), inset 0 -10px 18px -12px var(--color-tcl02-shade), inset 0 12px 18px -10px var(--color-tcl02-light);
--shadow-tcl02-pill: inset 0 -6px 12px -8px var(--color-tcl02-shade), inset 0 8px 12px -6px var(--color-tcl02-light);
--animate-tcl02-orbit: tcl02-orbit 9s linear infinite;
@keyframes tcl02-orbit {
0%,
100% {
--tcl02-lx: 0px;
--tcl02-ly: 20px;
}
25% {
--tcl02-lx: 20px;
--tcl02-ly: 0px;
}
50% {
--tcl02-lx: 0px;
--tcl02-ly: -20px;
}
75% {
--tcl02-lx: -20px;
--tcl02-ly: 0px;
}
}
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's utilities, so the
stage is sized before the browser build compiles. */
.tcl-02 {
width: 100%;
min-height: 100svh;
}
```How this works
v4 splits the shadow in two. shadow-* writes --tw-shadow and inset-shadow-* writes --tw-inset-shadow; the final box-shadow joins both. So the third ball is literally the first two combined:
<span class="inset-shadow-tcl02-light"> highlight only
<span class="inset-shadow-tcl02-dark"> shade only
<span class="inset-shadow-tcl02-both shadow-tcl02-ball">The orbit needs @property. Keyframes can only tween a custom property the browser knows is a <length>. Registered, --tcl02-lx and --tcl02-ly glide around the circle while the shade offsets are their negation, so light and shadow stay diametrically opposed.
Pause without JS. A visually hidden checkbox lives in the hero; group-has-[:checked]/hero: pauses the sphere.
Make it yours
- Faster light: change
9sin--animate-tcl02-orbit. - Wider orbit: raise the
20pxstops in the keyframes; keep blur (30px) larger than the offset or the edge hardens. - Different hue: edit
--color-tcl02-clay; shade and sheen derive from it.
Gotchas — read before shipping
inset-shadow-*is v4-only; in v3 you must hand-write the whole list inshadow-[inset_…].- Only one
inset-shadow-*per element — combining light and dark needs one token with both layers (--inset-shadow-tcl02-both). - Unregistered custom properties animate discretely; without
@propertythe sphere's light jumps between four positions.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
@property is Chrome 85 / Safari 16.4 / Firefox 128 — the same as Tailwind v4's floor.