25 CSS Background Animations18 / 25
Morphing Organic Blob Animation
Two morphing techniques side by side, because they solve different problems. The big shapes use the eight-value border-radius trick — cheap, GPU-friendly, infinitely loopable. The precise foreground shape uses SVG <animate> path morphing, which can hit exact silhouettes the radius trick can never make. Both run without a line of JavaScript.
Published Updated
The code
<section class="bga-18" aria-label="Morphing organic blob background demo">
<div class="bga-18__wash" aria-hidden="true"></div>
<div class="bga-18__blobs" aria-hidden="true">
<span class="bga-18__blob" style="--bga-18-t:22s;--bga-18-x:12%;--bga-18-y:16%;--bga-18-s:44vmin;--bga-18-h:340"></span>
<span class="bga-18__blob" style="--bga-18-t:30s;--bga-18-x:70%;--bga-18-y:24%;--bga-18-s:36vmin;--bga-18-h:212"></span>
<span class="bga-18__blob" style="--bga-18-t:26s;--bga-18-x:56%;--bga-18-y:74%;--bga-18-s:50vmin;--bga-18-h:38"></span>
</div>
<svg class="bga-18__svg" viewBox="0 0 200 200" aria-hidden="true" focusable="false" preserveAspectRatio="xMidYMid meet">
<defs>
<linearGradient id="bga-18-g" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="oklch(0.78 0.19 12)"/>
<stop offset="1" stop-color="oklch(0.7 0.2 300)"/>
</linearGradient>
</defs>
<path fill="url(#bga-18-g)" d="M45,-58C58,-47,68,-33,71,-17C74,-1,70,17,60,32C50,47,34,59,15,66C-4,73,-25,75,-42,66C-59,57,-72,37,-75,16C-78,-5,-71,-27,-58,-42C-45,-57,-26,-65,-7,-63C12,-61,32,-69,45,-58Z" transform="translate(100 100)">
<animate attributeName="d" dur="18s" repeatCount="indefinite" calcMode="spline"
keyTimes="0;0.33;0.66;1" keySplines="0.45 0 0.55 1;0.45 0 0.55 1;0.45 0 0.55 1"
values="M45,-58C58,-47,68,-33,71,-17C74,-1,70,17,60,32C50,47,34,59,15,66C-4,73,-25,75,-42,66C-59,57,-72,37,-75,16C-78,-5,-71,-27,-58,-42C-45,-57,-26,-65,-7,-63C12,-61,32,-69,45,-58Z;
M53,-64C67,-52,73,-31,74,-11C75,9,71,29,59,44C47,59,27,69,5,71C-17,73,-41,67,-57,53C-73,39,-81,17,-78,-4C-75,-25,-61,-45,-45,-57C-29,-69,-11,-73,7,-74C25,-75,39,-76,53,-64Z;
M39,-51C52,-40,64,-29,70,-14C76,1,76,19,68,33C60,47,44,57,26,64C8,71,-12,75,-31,69C-50,63,-68,47,-74,28C-80,9,-74,-13,-63,-30C-52,-47,-36,-59,-19,-64C-2,-69,17,-62,39,-51Z;
M45,-58C58,-47,68,-33,71,-17C74,-1,70,17,60,32C50,47,34,59,15,66C-4,73,-25,75,-42,66C-59,57,-72,37,-75,16C-78,-5,-71,-27,-58,-42C-45,-57,-26,-65,-7,-63C12,-61,32,-69,45,-58Z"/>
</path>
</svg>
<div class="bga-18__stage">
<div class="bga-18__card">
<p class="bga-18__eyebrow">Two techniques · 18</p>
<h1 class="bga-18__title">Radii for cheap.<br>Paths for precise.</h1>
<p class="bga-18__sub">Eight-value <code>border-radius</code> morphs anything convex almost for free. SVG <code>d</code> interpolation hits exact silhouettes — as long as every keyframe has the same command count.</p>
<div class="bga-18__split">
<div><b>A</b><span>border-radius<br>62% 38% 46% 54% / 58% 42% 58% 42%</span></div>
<div><b>B</b><span>SMIL animate d<br>4 keyframes · spline easing</span></div>
</div>
</div>
</div>
</section><section class="bga-18" aria-label="Morphing organic blob background demo">
<div class="bga-18__wash" aria-hidden="true"></div>
<div class="bga-18__blobs" aria-hidden="true">
<span class="bga-18__blob" style="--bga-18-t:22s;--bga-18-x:12%;--bga-18-y:16%;--bga-18-s:44vmin;--bga-18-h:340"></span>
<span class="bga-18__blob" style="--bga-18-t:30s;--bga-18-x:70%;--bga-18-y:24%;--bga-18-s:36vmin;--bga-18-h:212"></span>
<span class="bga-18__blob" style="--bga-18-t:26s;--bga-18-x:56%;--bga-18-y:74%;--bga-18-s:50vmin;--bga-18-h:38"></span>
</div>
<svg class="bga-18__svg" viewBox="0 0 200 200" aria-hidden="true" focusable="false" preserveAspectRatio="xMidYMid meet">
<defs>
<linearGradient id="bga-18-g" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="oklch(0.78 0.19 12)"/>
<stop offset="1" stop-color="oklch(0.7 0.2 300)"/>
</linearGradient>
</defs>
<path fill="url(#bga-18-g)" d="M45,-58C58,-47,68,-33,71,-17C74,-1,70,17,60,32C50,47,34,59,15,66C-4,73,-25,75,-42,66C-59,57,-72,37,-75,16C-78,-5,-71,-27,-58,-42C-45,-57,-26,-65,-7,-63C12,-61,32,-69,45,-58Z" transform="translate(100 100)">
<animate attributeName="d" dur="18s" repeatCount="indefinite" calcMode="spline"
keyTimes="0;0.33;0.66;1" keySplines="0.45 0 0.55 1;0.45 0 0.55 1;0.45 0 0.55 1"
values="M45,-58C58,-47,68,-33,71,-17C74,-1,70,17,60,32C50,47,34,59,15,66C-4,73,-25,75,-42,66C-59,57,-72,37,-75,16C-78,-5,-71,-27,-58,-42C-45,-57,-26,-65,-7,-63C12,-61,32,-69,45,-58Z;
M53,-64C67,-52,73,-31,74,-11C75,9,71,29,59,44C47,59,27,69,5,71C-17,73,-41,67,-57,53C-73,39,-81,17,-78,-4C-75,-25,-61,-45,-45,-57C-29,-69,-11,-73,7,-74C25,-75,39,-76,53,-64Z;
M39,-51C52,-40,64,-29,70,-14C76,1,76,19,68,33C60,47,44,57,26,64C8,71,-12,75,-31,69C-50,63,-68,47,-74,28C-80,9,-74,-13,-63,-30C-52,-47,-36,-59,-19,-64C-2,-69,17,-62,39,-51Z;
M45,-58C58,-47,68,-33,71,-17C74,-1,70,17,60,32C50,47,34,59,15,66C-4,73,-25,75,-42,66C-59,57,-72,37,-75,16C-78,-5,-71,-27,-58,-42C-45,-57,-26,-65,-7,-63C12,-61,32,-69,45,-58Z"/>
</path>
</svg>
<div class="bga-18__stage">
<div class="bga-18__card">
<p class="bga-18__eyebrow">Two techniques · 18</p>
<h1 class="bga-18__title">Radii for cheap.<br>Paths for precise.</h1>
<p class="bga-18__sub">Eight-value <code>border-radius</code> morphs anything convex almost for free. SVG <code>d</code> interpolation hits exact silhouettes — as long as every keyframe has the same command count.</p>
<div class="bga-18__split">
<div><b>A</b><span>border-radius<br>62% 38% 46% 54% / 58% 42% 58% 42%</span></div>
<div><b>B</b><span>SMIL animate d<br>4 keyframes · spline easing</span></div>
</div>
</div>
</div>
</section>.bga-18,
.bga-18 *,
.bga-18 *::before,
.bga-18 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.bga-18 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
position: relative;
overflow: hidden;
isolation: isolate;
--bga-18-bg: oklch(0.97 0.008 300);
--bga-18-ink: oklch(0.22 0.03 300);
background: var(--bga-18-bg);
color: var(--bga-18-ink);
font-family: ui-sans-serif,system-ui,-apple-system,'Segoe UI',sans-serif;
-webkit-font-smoothing: antialiased;
}
.bga-18__wash {
position: absolute;
inset: 0;
background: radial-gradient(120% 100% at 50% 0%,oklch(0.99 0.01 320),oklch(0.93 0.02 290) 80%);
}
.bga-18__blobs {
position: absolute;
inset: 0;
filter: blur(26px) saturate(1.2);
opacity: .72;
}
.bga-18__blob {
position: absolute;
left: var(--bga-18-x);
top: var(--bga-18-y);
width: var(--bga-18-s);
aspect-ratio: 1;
translate: -50% -50%;
background: conic-gradient(from 140deg,
oklch(0.8 0.17 var(--bga-18-h)),
oklch(0.86 0.12 calc(var(--bga-18-h) + 40)),
oklch(0.74 0.19 calc(var(--bga-18-h) - 30)),
oklch(0.8 0.17 var(--bga-18-h)));
mix-blend-mode: multiply;
animation: bga-18-morph var(--bga-18-t) ease-in-out infinite, bga-18-turn calc(var(--bga-18-t) * 2.3) linear infinite;
}
@keyframes bga-18-morph {
0% {
border-radius: 62% 38% 46% 54% / 58% 42% 58% 42%;
}
34% {
border-radius: 38% 62% 63% 37% / 41% 44% 56% 59%;
}
67% {
border-radius: 45% 55% 37% 63% / 63% 57% 43% 37%;
}
100% {
border-radius: 62% 38% 46% 54% / 58% 42% 58% 42%;
}
}
@keyframes bga-18-turn {
to {
rotate: 360deg;
}
}
.bga-18__svg {
position: absolute;
left: 50%;
top: 50%;
translate: -50% -50%;
width: min(74vmin,660px);
height: min(74vmin,660px);
opacity: .2;
filter: blur(.4px);
}
.bga-18__stage {
position: relative;
z-index: 3;
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
align-content: center;
padding: clamp(22px,5vw,68px);
}
.bga-18__card {
width: min(100%,660px);
display: grid;
gap: 16px;
justify-items: center;
text-align: center;
padding: clamp(26px,3.8vw,48px);
border-radius: 30px;
background: oklch(1 0 0/.66);
border: 1px solid oklch(1 0 0/.9);
backdrop-filter: blur(24px) saturate(1.6);
-webkit-backdrop-filter: blur(24px) saturate(1.6);
box-shadow: 0 40px 90px -50px oklch(0.3 0.06 300/.5),inset 0 1px 0 oklch(1 0 0);
}
.bga-18__eyebrow {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
letter-spacing: .24em;
text-transform: uppercase;
color: oklch(0.58 0.19 340);
}
.bga-18__title {
font-size: clamp(27px,5.4vw,54px);
font-weight: 640;
line-height: 1.02;
letter-spacing: -.05em;
text-wrap: balance;
}
.bga-18__sub {
max-width: 50ch;
font-size: clamp(13.5px,1.3vw,16px);
line-height: 1.6;
color: color-mix(in oklab,var(--bga-18-ink) 78%,transparent);
text-wrap: pretty;
}
.bga-18__sub code {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: .88em;
padding: 1px 5px;
border-radius: 5px;
background: oklch(0.3 0.04 300/.09);
}
.bga-18__split {
display: flex;
flex-wrap: wrap;
gap: 10px;
justify-content: center;
margin-top: 6px;
}
.bga-18__split div {
display: flex;
gap: 11px;
align-items: flex-start;
text-align: left;
min-width: 210px;
padding: 13px 16px;
border-radius: 16px;
background: oklch(0.3 0.04 300/.05);
border: 1px solid oklch(0.3 0.04 300/.1);
}
.bga-18__split b {
display: grid;
place-items: center;
width: 24px;
height: 24px;
flex: none;
border-radius: 8px;
font-size: 12px;
background: oklch(0.58 0.19 340);
color: oklch(0.99 0 0);
}
.bga-18__split span {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 10.5px;
line-height: 1.5;
color: color-mix(in oklab,var(--bga-18-ink) 62%,transparent);
}
@media (prefers-reduced-motion: reduce) {
.bga-18__blob {
animation: none;
}
}.bga-18,
.bga-18 *,
.bga-18 *::before,
.bga-18 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.bga-18 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
position: relative;
overflow: hidden;
isolation: isolate;
--bga-18-bg: oklch(0.97 0.008 300);
--bga-18-ink: oklch(0.22 0.03 300);
background: var(--bga-18-bg);
color: var(--bga-18-ink);
font-family: ui-sans-serif,system-ui,-apple-system,'Segoe UI',sans-serif;
-webkit-font-smoothing: antialiased;
}
.bga-18__wash {
position: absolute;
inset: 0;
background: radial-gradient(120% 100% at 50% 0%,oklch(0.99 0.01 320),oklch(0.93 0.02 290) 80%);
}
.bga-18__blobs {
position: absolute;
inset: 0;
filter: blur(26px) saturate(1.2);
opacity: .72;
}
.bga-18__blob {
position: absolute;
left: var(--bga-18-x);
top: var(--bga-18-y);
width: var(--bga-18-s);
aspect-ratio: 1;
translate: -50% -50%;
background: conic-gradient(from 140deg,
oklch(0.8 0.17 var(--bga-18-h)),
oklch(0.86 0.12 calc(var(--bga-18-h) + 40)),
oklch(0.74 0.19 calc(var(--bga-18-h) - 30)),
oklch(0.8 0.17 var(--bga-18-h)));
mix-blend-mode: multiply;
animation: bga-18-morph var(--bga-18-t) ease-in-out infinite, bga-18-turn calc(var(--bga-18-t) * 2.3) linear infinite;
}
@keyframes bga-18-morph {
0% {
border-radius: 62% 38% 46% 54% / 58% 42% 58% 42%;
}
34% {
border-radius: 38% 62% 63% 37% / 41% 44% 56% 59%;
}
67% {
border-radius: 45% 55% 37% 63% / 63% 57% 43% 37%;
}
100% {
border-radius: 62% 38% 46% 54% / 58% 42% 58% 42%;
}
}
@keyframes bga-18-turn {
to {
rotate: 360deg;
}
}
.bga-18__svg {
position: absolute;
left: 50%;
top: 50%;
translate: -50% -50%;
width: min(74vmin,660px);
height: min(74vmin,660px);
opacity: .2;
filter: blur(.4px);
}
.bga-18__stage {
position: relative;
z-index: 3;
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
align-content: center;
padding: clamp(22px,5vw,68px);
}
.bga-18__card {
width: min(100%,660px);
display: grid;
gap: 16px;
justify-items: center;
text-align: center;
padding: clamp(26px,3.8vw,48px);
border-radius: 30px;
background: oklch(1 0 0/.66);
border: 1px solid oklch(1 0 0/.9);
backdrop-filter: blur(24px) saturate(1.6);
-webkit-backdrop-filter: blur(24px) saturate(1.6);
box-shadow: 0 40px 90px -50px oklch(0.3 0.06 300/.5),inset 0 1px 0 oklch(1 0 0);
}
.bga-18__eyebrow {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
letter-spacing: .24em;
text-transform: uppercase;
color: oklch(0.58 0.19 340);
}
.bga-18__title {
font-size: clamp(27px,5.4vw,54px);
font-weight: 640;
line-height: 1.02;
letter-spacing: -.05em;
text-wrap: balance;
}
.bga-18__sub {
max-width: 50ch;
font-size: clamp(13.5px,1.3vw,16px);
line-height: 1.6;
color: color-mix(in oklab,var(--bga-18-ink) 78%,transparent);
text-wrap: pretty;
}
.bga-18__sub code {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: .88em;
padding: 1px 5px;
border-radius: 5px;
background: oklch(0.3 0.04 300/.09);
}
.bga-18__split {
display: flex;
flex-wrap: wrap;
gap: 10px;
justify-content: center;
margin-top: 6px;
}
.bga-18__split div {
display: flex;
gap: 11px;
align-items: flex-start;
text-align: left;
min-width: 210px;
padding: 13px 16px;
border-radius: 16px;
background: oklch(0.3 0.04 300/.05);
border: 1px solid oklch(0.3 0.04 300/.1);
}
.bga-18__split b {
display: grid;
place-items: center;
width: 24px;
height: 24px;
flex: none;
border-radius: 8px;
font-size: 12px;
background: oklch(0.58 0.19 340);
color: oklch(0.99 0 0);
}
.bga-18__split span {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 10.5px;
line-height: 1.5;
color: color-mix(in oklab,var(--bga-18-ink) 62%,transparent);
}
@media (prefers-reduced-motion: reduce) {
.bga-18__blob {
animation: none;
}
}Here's a working CSS Background 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: Morphing Organic Blob Animation
Source: https://codefronts.com/motion/css-background-animations/animated-blob-morphing-background/
Two morphing techniques side by side, because they solve different problems. The big shapes use the eight-value border-radius trick — cheap, GPU-friendly, infinitely loopable. The precise foreground shape uses SVG <animate> path morphing, which can hit exact silhouettes the radius trick can never make. Both run without a line of JavaScript.
## HTML
```html
<section class="bga-18" aria-label="Morphing organic blob background demo">
<div class="bga-18__wash" aria-hidden="true"></div>
<div class="bga-18__blobs" aria-hidden="true">
<span class="bga-18__blob" style="--bga-18-t:22s;--bga-18-x:12%;--bga-18-y:16%;--bga-18-s:44vmin;--bga-18-h:340"></span>
<span class="bga-18__blob" style="--bga-18-t:30s;--bga-18-x:70%;--bga-18-y:24%;--bga-18-s:36vmin;--bga-18-h:212"></span>
<span class="bga-18__blob" style="--bga-18-t:26s;--bga-18-x:56%;--bga-18-y:74%;--bga-18-s:50vmin;--bga-18-h:38"></span>
</div>
<svg class="bga-18__svg" viewBox="0 0 200 200" aria-hidden="true" focusable="false" preserveAspectRatio="xMidYMid meet">
<defs>
<linearGradient id="bga-18-g" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="oklch(0.78 0.19 12)"/>
<stop offset="1" stop-color="oklch(0.7 0.2 300)"/>
</linearGradient>
</defs>
<path fill="url(#bga-18-g)" d="M45,-58C58,-47,68,-33,71,-17C74,-1,70,17,60,32C50,47,34,59,15,66C-4,73,-25,75,-42,66C-59,57,-72,37,-75,16C-78,-5,-71,-27,-58,-42C-45,-57,-26,-65,-7,-63C12,-61,32,-69,45,-58Z" transform="translate(100 100)">
<animate attributeName="d" dur="18s" repeatCount="indefinite" calcMode="spline"
keyTimes="0;0.33;0.66;1" keySplines="0.45 0 0.55 1;0.45 0 0.55 1;0.45 0 0.55 1"
values="M45,-58C58,-47,68,-33,71,-17C74,-1,70,17,60,32C50,47,34,59,15,66C-4,73,-25,75,-42,66C-59,57,-72,37,-75,16C-78,-5,-71,-27,-58,-42C-45,-57,-26,-65,-7,-63C12,-61,32,-69,45,-58Z;
M53,-64C67,-52,73,-31,74,-11C75,9,71,29,59,44C47,59,27,69,5,71C-17,73,-41,67,-57,53C-73,39,-81,17,-78,-4C-75,-25,-61,-45,-45,-57C-29,-69,-11,-73,7,-74C25,-75,39,-76,53,-64Z;
M39,-51C52,-40,64,-29,70,-14C76,1,76,19,68,33C60,47,44,57,26,64C8,71,-12,75,-31,69C-50,63,-68,47,-74,28C-80,9,-74,-13,-63,-30C-52,-47,-36,-59,-19,-64C-2,-69,17,-62,39,-51Z;
M45,-58C58,-47,68,-33,71,-17C74,-1,70,17,60,32C50,47,34,59,15,66C-4,73,-25,75,-42,66C-59,57,-72,37,-75,16C-78,-5,-71,-27,-58,-42C-45,-57,-26,-65,-7,-63C12,-61,32,-69,45,-58Z"/>
</path>
</svg>
<div class="bga-18__stage">
<div class="bga-18__card">
<p class="bga-18__eyebrow">Two techniques · 18</p>
<h1 class="bga-18__title">Radii for cheap.<br>Paths for precise.</h1>
<p class="bga-18__sub">Eight-value <code>border-radius</code> morphs anything convex almost for free. SVG <code>d</code> interpolation hits exact silhouettes — as long as every keyframe has the same command count.</p>
<div class="bga-18__split">
<div><b>A</b><span>border-radius<br>62% 38% 46% 54% / 58% 42% 58% 42%</span></div>
<div><b>B</b><span>SMIL animate d<br>4 keyframes · spline easing</span></div>
</div>
</div>
</div>
</section>
```
## CSS
```css
.bga-18,
.bga-18 *,
.bga-18 *::before,
.bga-18 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.bga-18 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
position: relative;
overflow: hidden;
isolation: isolate;
--bga-18-bg: oklch(0.97 0.008 300);
--bga-18-ink: oklch(0.22 0.03 300);
background: var(--bga-18-bg);
color: var(--bga-18-ink);
font-family: ui-sans-serif,system-ui,-apple-system,'Segoe UI',sans-serif;
-webkit-font-smoothing: antialiased;
}
.bga-18__wash {
position: absolute;
inset: 0;
background: radial-gradient(120% 100% at 50% 0%,oklch(0.99 0.01 320),oklch(0.93 0.02 290) 80%);
}
.bga-18__blobs {
position: absolute;
inset: 0;
filter: blur(26px) saturate(1.2);
opacity: .72;
}
.bga-18__blob {
position: absolute;
left: var(--bga-18-x);
top: var(--bga-18-y);
width: var(--bga-18-s);
aspect-ratio: 1;
translate: -50% -50%;
background: conic-gradient(from 140deg,
oklch(0.8 0.17 var(--bga-18-h)),
oklch(0.86 0.12 calc(var(--bga-18-h) + 40)),
oklch(0.74 0.19 calc(var(--bga-18-h) - 30)),
oklch(0.8 0.17 var(--bga-18-h)));
mix-blend-mode: multiply;
animation: bga-18-morph var(--bga-18-t) ease-in-out infinite, bga-18-turn calc(var(--bga-18-t) * 2.3) linear infinite;
}
@keyframes bga-18-morph {
0% {
border-radius: 62% 38% 46% 54% / 58% 42% 58% 42%;
}
34% {
border-radius: 38% 62% 63% 37% / 41% 44% 56% 59%;
}
67% {
border-radius: 45% 55% 37% 63% / 63% 57% 43% 37%;
}
100% {
border-radius: 62% 38% 46% 54% / 58% 42% 58% 42%;
}
}
@keyframes bga-18-turn {
to {
rotate: 360deg;
}
}
.bga-18__svg {
position: absolute;
left: 50%;
top: 50%;
translate: -50% -50%;
width: min(74vmin,660px);
height: min(74vmin,660px);
opacity: .2;
filter: blur(.4px);
}
.bga-18__stage {
position: relative;
z-index: 3;
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
align-content: center;
padding: clamp(22px,5vw,68px);
}
.bga-18__card {
width: min(100%,660px);
display: grid;
gap: 16px;
justify-items: center;
text-align: center;
padding: clamp(26px,3.8vw,48px);
border-radius: 30px;
background: oklch(1 0 0/.66);
border: 1px solid oklch(1 0 0/.9);
backdrop-filter: blur(24px) saturate(1.6);
-webkit-backdrop-filter: blur(24px) saturate(1.6);
box-shadow: 0 40px 90px -50px oklch(0.3 0.06 300/.5),inset 0 1px 0 oklch(1 0 0);
}
.bga-18__eyebrow {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
letter-spacing: .24em;
text-transform: uppercase;
color: oklch(0.58 0.19 340);
}
.bga-18__title {
font-size: clamp(27px,5.4vw,54px);
font-weight: 640;
line-height: 1.02;
letter-spacing: -.05em;
text-wrap: balance;
}
.bga-18__sub {
max-width: 50ch;
font-size: clamp(13.5px,1.3vw,16px);
line-height: 1.6;
color: color-mix(in oklab,var(--bga-18-ink) 78%,transparent);
text-wrap: pretty;
}
.bga-18__sub code {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: .88em;
padding: 1px 5px;
border-radius: 5px;
background: oklch(0.3 0.04 300/.09);
}
.bga-18__split {
display: flex;
flex-wrap: wrap;
gap: 10px;
justify-content: center;
margin-top: 6px;
}
.bga-18__split div {
display: flex;
gap: 11px;
align-items: flex-start;
text-align: left;
min-width: 210px;
padding: 13px 16px;
border-radius: 16px;
background: oklch(0.3 0.04 300/.05);
border: 1px solid oklch(0.3 0.04 300/.1);
}
.bga-18__split b {
display: grid;
place-items: center;
width: 24px;
height: 24px;
flex: none;
border-radius: 8px;
font-size: 12px;
background: oklch(0.58 0.19 340);
color: oklch(0.99 0 0);
}
.bga-18__split span {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 10.5px;
line-height: 1.5;
color: color-mix(in oklab,var(--bga-18-ink) 62%,transparent);
}
@media (prefers-reduced-motion: reduce) {
.bga-18__blob {
animation: none;
}
}
```Here's a working CSS Background 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: Morphing Organic Blob Animation
Source: https://codefronts.com/motion/css-background-animations/animated-blob-morphing-background/
Two morphing techniques side by side, because they solve different problems. The big shapes use the eight-value border-radius trick — cheap, GPU-friendly, infinitely loopable. The precise foreground shape uses SVG <animate> path morphing, which can hit exact silhouettes the radius trick can never make. Both run without a line of JavaScript.
## HTML
```html
<section class="bga-18" aria-label="Morphing organic blob background demo">
<div class="bga-18__wash" aria-hidden="true"></div>
<div class="bga-18__blobs" aria-hidden="true">
<span class="bga-18__blob" style="--bga-18-t:22s;--bga-18-x:12%;--bga-18-y:16%;--bga-18-s:44vmin;--bga-18-h:340"></span>
<span class="bga-18__blob" style="--bga-18-t:30s;--bga-18-x:70%;--bga-18-y:24%;--bga-18-s:36vmin;--bga-18-h:212"></span>
<span class="bga-18__blob" style="--bga-18-t:26s;--bga-18-x:56%;--bga-18-y:74%;--bga-18-s:50vmin;--bga-18-h:38"></span>
</div>
<svg class="bga-18__svg" viewBox="0 0 200 200" aria-hidden="true" focusable="false" preserveAspectRatio="xMidYMid meet">
<defs>
<linearGradient id="bga-18-g" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="oklch(0.78 0.19 12)"/>
<stop offset="1" stop-color="oklch(0.7 0.2 300)"/>
</linearGradient>
</defs>
<path fill="url(#bga-18-g)" d="M45,-58C58,-47,68,-33,71,-17C74,-1,70,17,60,32C50,47,34,59,15,66C-4,73,-25,75,-42,66C-59,57,-72,37,-75,16C-78,-5,-71,-27,-58,-42C-45,-57,-26,-65,-7,-63C12,-61,32,-69,45,-58Z" transform="translate(100 100)">
<animate attributeName="d" dur="18s" repeatCount="indefinite" calcMode="spline"
keyTimes="0;0.33;0.66;1" keySplines="0.45 0 0.55 1;0.45 0 0.55 1;0.45 0 0.55 1"
values="M45,-58C58,-47,68,-33,71,-17C74,-1,70,17,60,32C50,47,34,59,15,66C-4,73,-25,75,-42,66C-59,57,-72,37,-75,16C-78,-5,-71,-27,-58,-42C-45,-57,-26,-65,-7,-63C12,-61,32,-69,45,-58Z;
M53,-64C67,-52,73,-31,74,-11C75,9,71,29,59,44C47,59,27,69,5,71C-17,73,-41,67,-57,53C-73,39,-81,17,-78,-4C-75,-25,-61,-45,-45,-57C-29,-69,-11,-73,7,-74C25,-75,39,-76,53,-64Z;
M39,-51C52,-40,64,-29,70,-14C76,1,76,19,68,33C60,47,44,57,26,64C8,71,-12,75,-31,69C-50,63,-68,47,-74,28C-80,9,-74,-13,-63,-30C-52,-47,-36,-59,-19,-64C-2,-69,17,-62,39,-51Z;
M45,-58C58,-47,68,-33,71,-17C74,-1,70,17,60,32C50,47,34,59,15,66C-4,73,-25,75,-42,66C-59,57,-72,37,-75,16C-78,-5,-71,-27,-58,-42C-45,-57,-26,-65,-7,-63C12,-61,32,-69,45,-58Z"/>
</path>
</svg>
<div class="bga-18__stage">
<div class="bga-18__card">
<p class="bga-18__eyebrow">Two techniques · 18</p>
<h1 class="bga-18__title">Radii for cheap.<br>Paths for precise.</h1>
<p class="bga-18__sub">Eight-value <code>border-radius</code> morphs anything convex almost for free. SVG <code>d</code> interpolation hits exact silhouettes — as long as every keyframe has the same command count.</p>
<div class="bga-18__split">
<div><b>A</b><span>border-radius<br>62% 38% 46% 54% / 58% 42% 58% 42%</span></div>
<div><b>B</b><span>SMIL animate d<br>4 keyframes · spline easing</span></div>
</div>
</div>
</div>
</section>
```
## CSS
```css
.bga-18,
.bga-18 *,
.bga-18 *::before,
.bga-18 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.bga-18 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
position: relative;
overflow: hidden;
isolation: isolate;
--bga-18-bg: oklch(0.97 0.008 300);
--bga-18-ink: oklch(0.22 0.03 300);
background: var(--bga-18-bg);
color: var(--bga-18-ink);
font-family: ui-sans-serif,system-ui,-apple-system,'Segoe UI',sans-serif;
-webkit-font-smoothing: antialiased;
}
.bga-18__wash {
position: absolute;
inset: 0;
background: radial-gradient(120% 100% at 50% 0%,oklch(0.99 0.01 320),oklch(0.93 0.02 290) 80%);
}
.bga-18__blobs {
position: absolute;
inset: 0;
filter: blur(26px) saturate(1.2);
opacity: .72;
}
.bga-18__blob {
position: absolute;
left: var(--bga-18-x);
top: var(--bga-18-y);
width: var(--bga-18-s);
aspect-ratio: 1;
translate: -50% -50%;
background: conic-gradient(from 140deg,
oklch(0.8 0.17 var(--bga-18-h)),
oklch(0.86 0.12 calc(var(--bga-18-h) + 40)),
oklch(0.74 0.19 calc(var(--bga-18-h) - 30)),
oklch(0.8 0.17 var(--bga-18-h)));
mix-blend-mode: multiply;
animation: bga-18-morph var(--bga-18-t) ease-in-out infinite, bga-18-turn calc(var(--bga-18-t) * 2.3) linear infinite;
}
@keyframes bga-18-morph {
0% {
border-radius: 62% 38% 46% 54% / 58% 42% 58% 42%;
}
34% {
border-radius: 38% 62% 63% 37% / 41% 44% 56% 59%;
}
67% {
border-radius: 45% 55% 37% 63% / 63% 57% 43% 37%;
}
100% {
border-radius: 62% 38% 46% 54% / 58% 42% 58% 42%;
}
}
@keyframes bga-18-turn {
to {
rotate: 360deg;
}
}
.bga-18__svg {
position: absolute;
left: 50%;
top: 50%;
translate: -50% -50%;
width: min(74vmin,660px);
height: min(74vmin,660px);
opacity: .2;
filter: blur(.4px);
}
.bga-18__stage {
position: relative;
z-index: 3;
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
align-content: center;
padding: clamp(22px,5vw,68px);
}
.bga-18__card {
width: min(100%,660px);
display: grid;
gap: 16px;
justify-items: center;
text-align: center;
padding: clamp(26px,3.8vw,48px);
border-radius: 30px;
background: oklch(1 0 0/.66);
border: 1px solid oklch(1 0 0/.9);
backdrop-filter: blur(24px) saturate(1.6);
-webkit-backdrop-filter: blur(24px) saturate(1.6);
box-shadow: 0 40px 90px -50px oklch(0.3 0.06 300/.5),inset 0 1px 0 oklch(1 0 0);
}
.bga-18__eyebrow {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
letter-spacing: .24em;
text-transform: uppercase;
color: oklch(0.58 0.19 340);
}
.bga-18__title {
font-size: clamp(27px,5.4vw,54px);
font-weight: 640;
line-height: 1.02;
letter-spacing: -.05em;
text-wrap: balance;
}
.bga-18__sub {
max-width: 50ch;
font-size: clamp(13.5px,1.3vw,16px);
line-height: 1.6;
color: color-mix(in oklab,var(--bga-18-ink) 78%,transparent);
text-wrap: pretty;
}
.bga-18__sub code {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: .88em;
padding: 1px 5px;
border-radius: 5px;
background: oklch(0.3 0.04 300/.09);
}
.bga-18__split {
display: flex;
flex-wrap: wrap;
gap: 10px;
justify-content: center;
margin-top: 6px;
}
.bga-18__split div {
display: flex;
gap: 11px;
align-items: flex-start;
text-align: left;
min-width: 210px;
padding: 13px 16px;
border-radius: 16px;
background: oklch(0.3 0.04 300/.05);
border: 1px solid oklch(0.3 0.04 300/.1);
}
.bga-18__split b {
display: grid;
place-items: center;
width: 24px;
height: 24px;
flex: none;
border-radius: 8px;
font-size: 12px;
background: oklch(0.58 0.19 340);
color: oklch(0.99 0 0);
}
.bga-18__split span {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 10.5px;
line-height: 1.5;
color: color-mix(in oklab,var(--bga-18-ink) 62%,transparent);
}
@media (prefers-reduced-motion: reduce) {
.bga-18__blob {
animation: none;
}
}
```How this works
Technique A — eight-value border-radius. border-radius accepts four horizontal and four vertical radii separated by a slash. Animating all eight independently deforms a square into a continuously changing organic shape:
@keyframes morph {
0% { border-radius: 62% 38% 46% 54% / 58% 42% 58% 42%; }
34% { border-radius: 38% 62% 63% 37% / 41% 44% 56% 59%; }
67% { border-radius: 45% 55% 37% 63% / 63% 57% 43% 37%; }
100% { border-radius: 62% 38% 46% 54% / 58% 42% 58% 42%; }
}Keep the paired values summing to 100% and the shape stays convex and plausible; break that and you get pinched, alien forms — occasionally useful, usually a bug. Returning to the exact opening value at 100% is what makes the loop seamless.
Technique B — SVG path morph. When you need a specific silhouette, animate the d attribute directly with SMIL:
<path d='M...'>
<animate attributeName='d' dur='16s' repeatCount='indefinite'
values='M...; M...; M...' calcMode='spline' keySplines='...'/>
</path>The absolute requirement is that every value in the list has the same number and type of path commands — the interpolator matches them positionally. Mismatch the command list and the shape snaps instead of morphing; that is the one gotcha that catches everyone.
Composition-wise the blobs are stacked with slight rotation offsets and a shared conic gradient, so overlaps produce colour depth rather than flat stacking.
Make it yours
- Change the morph character by editing how far the paired radii deviate from 50% — ±10% is a gentle breathing pebble, ±25% is a lava blob.
- Add
rotateon a longer coprime period; a rotating morph never repeats visually even with only three keyframes. - For the SVG path, generate values with any blob tool and keep the command count identical across all frames — that is the only constraint.
- Use the blob as a
clip-pathon an image (clip-path: url(#blob)) so a photograph morphs inside the organic shape.
Gotchas — read before shipping
- Border-radius morphing only produces convex-ish shapes. Anything with a concave inlet needs the SVG path route.
- SMIL path morphing requires identical command sequences in every value; different node counts snap instead of interpolating.
- Animating
border-radiustriggers repaint, not just composite. Keep blob count low and sizes moderate on mobile. - Blurred blobs behind text still lift the background luminance unpredictably — set copy on a plate, or keep blobs to the periphery.
- CSS
dproperty animation exists in Chromium but not everywhere; SMIL<animate>remains the portable choice today.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 114+ | 17.5+ | 121+ | 114+ |
Floor set by oklch(), color-mix(), backdrop-filter, text-wrap as this demo is written. The core technique itself goes back further — Chrome any modern.
Eight-value border-radius animation works in every engine since 2015. SMIL <animate> on path d is supported in Chrome, Safari and Firefox. oklch()/color-mix() raise the practical floor to 2023 engines and are easily swapped.