Skip to content
shadcn.io

shadcn.io is not affiliated with official shadcn/ui

Shaders

Pro

Soap Bubbles 2D Shader for React

Clear soap bubbles rising through drifting color: bright rims, iridescent film and soft highlights, in layers with depth of field. For React with Base UI, when a playful hero, a product launch or a kids' or bath brand wants light, bouncy motion.

Framework

Soap Bubbles 2D Shader floats dozens of soap bubbles over a slowly drifting field of three colors. The bubbles sit in five layers: each layer rises at its own pace, the nearest fastest, and every bubble jiggles gently in place. Each one is clear, with a bright rim, an iridescent film that swirls toward its edge, a window highlight and a faint second reflection, and it bends the bubbles and colors behind it like a weak lens. The nearest layer and the farthest are out of focus, so the scene has depth. A WGSL fragment shader on WebGPU draws it all for every pixel, every frame; there are no particles, just a grid of cells per layer.

Use it behind a playful hero, a product launch, or a page for a kids', bath or beauty brand. For bubbles in plain CSS, use the Bubbles background; for water seen from above, Water Shader. It runs only while it's on screen, holds a still frame with reduced motion, and shows a gradient of the same colors with a few rings in browsers without WebGPU.

Preview width

Installation

Part of Pro
Pro unlocks the code and one-line install of every Pro button and text effect, plus the MCP for your AI editor.
Have Pro already? Sign in to open it.Opens here as soon as you're Pro.

Usage

import {
  SoapBubbles2dShader,
} from "@/components/ui/soap-bubbles-2d-shader";
<SoapBubbles2dShader className="flex min-h-[32rem] items-center justify-center">
  <div className="rounded-xl bg-black/50 px-8 py-6">
    <h1 className="text-5xl font-semibold text-white">Make a splash</h1>
  </div>
</SoapBubbles2dShader>

Soap Bubbles 2D Shader is a container: your content sits on top of the bubbles, in normal flow. It fills its own box, so give it a height or content. For a backdrop behind the whole page, render it empty with className="fixed inset-0 -z-10". The default colors are mid-toned and vivid in both color schemes, so put white text on a soft scrim, as above.

Demos

Big bubbles

scale={1.6} makes every bubble about 1.6 times bigger, so fewer fit in the box. refraction={0} turns the lens off, so the bubbles are clear glass and the layers behind show through unbent.

Preview width

In a card

fromColor="#0b1026", viaColor="#1e3a8a" and toColor="#7c3aed" set a midnight blue and violet field; the white rims and the film read like light on it.

Preview width

Performance

About 1.7 ms of GPU time a frame at 1280×720 on an M2 Max laptop; slower GPUs take several times longer. Each pixel checks nine cells in each of the five layers and shades the one bubble that covers it per layer, and the blur of the out-of-focus layers is drawn by widening the falloffs, not by extra samples, so none of the props change the cost. quality={0.75} draws a little over half the pixels, for phones and large screens, with slightly softer rims. The animation stops when the shader scrolls out of view, and the browser pauses it in a hidden tab. Every shader on a page shares one GPU device and one frame loop, and vgpu (about 50 KB gzipped) loads only when the first one mounts.

Browser support

The shader needs WebGPU: Chrome and Edge, Safari 26 and later, and Firefox on Windows and macOS. Elsewhere, and until the first frame, the box shows a gradient of the same colors with three white rings, so the layout never jumps. It follows the color props. data-state on the root reads unsupported there, if you want to style around it. See Shader Canvas.

Accessibility

The bubbles are decoration: they're hidden from assistive tech and let every click through to your content. With reduced motion it draws one still frame and stops. Nothing flashes: the bubbles rise and the colors drift over seconds. At the default colors, white text has a contrast of about 3.8:1 on half the frame and drops near 2:1 on the brighter green. Behind a bg-black/50 scrim it's above 6:1 on half the frame, above 3.8:1 on three quarters and above 3.4:1 on nine tenths, fine for a headline; put small text on a solid panel.

FAQ

How are the bubbles placed?

Each of the five layers is a grid of cells that drifts upward. A hash of each cell decides whether it holds a bubble, where in the cell it sits and how big it is, and every pixel checks only its own cell and the eight around it. There are no particles to simulate: the same cells come back every frame, so the bubbles stay put relative to their layer and only wobble a little.

How do I get fewer, bigger bubbles?

Raise scale: at 2 each bubble is twice as big and four times fewer fit in the box; at 0.5 you get many small ones. density sets how many cells hold a bubble, from 0 (none) to 1 (all), without changing their size. shadcn.io's version had a count that worked backwards (higher meant fewer, bigger bubbles) and a size prop that did nothing; these two replace both.

Why are some bubbles blurred?

It's depth of field. The middle layer is in focus; the nearest layer, with the biggest bubbles, is the most blurred, and the farthest, with the smallest, slightly. The blurred bubbles are dimmer too, since the same light is spread wider, which keeps the in-focus ones crisp and in front.

What does refraction do?

It sets how much each bubble bends what's behind it: the layers farther back and the colored field shift inside it, as through a weak lens. At 0 the bubbles are clear glass; at 2 the bubbles behind are visibly warped through the near ones. It doesn't move the bubbles themselves. For more or less motion, use speed.

Can I change the colors?

Yes. fromColor, viaColor and toColor take any CSS color, and follow a theme switch, so var(--primary) works. Two slow noise fields mix them: viaColor covers most of the frame, fromColor pools toward one side, and toColor drifts in patches. The rims, highlights and film stay white and iridescent, so they read on light and dark colors alike.

Will it slow my page down?

It's light: about 1.7 ms of GPU time a frame at 1280×720 on an M2 Max laptop, and several times that on slower GPUs. It stops when it's off screen. scale, density and refraction don't change the cost; quality={0.75} does.

Credits

Inspired by Soap Bubbles 2D (opens in a new tab) by foxes on Shadertoy. The code is our own: layers of bubbles on jittered grids over a noise field, with depth of field and a lens, is the shared idea. The bubble shading, the iridescent film, the front-to-back compositing, the blur by wider falloffs, the color props and the API are new.

API Reference

SoapBubbles2dShader

data-slot="soap-bubbles-2d-shader"

PropType
density?number

How many bubbles there are, from 0 (none) to 1 (every spot taken). @default 0.35.

Default
0.35
fallback?string

What shows until the first frame and in browsers without WebGPU: a CSS background value (gradients, sizes, a colour) that looks like the shader. It can use the colour props' --shader-<field> variables.

fromColor?string

The first background color, pooling on one side. Any CSS color. @default "#1d5b78".

Default
"#1d5b78"
interactive?boolean

Whether u.pointer follows the pointer. @default false.

paused?boolean

Holds the current frame. @default false.

quality?number

Render scale, from 0.25 to 1, of the screen's pixels (capped at 2×). @default 1.

refraction?number

How much each bubble bends what's behind it, from 0 (clear glass) to 2. @default 1.

Default
1
scale?number

Bubble size, from 0.5 (many small bubbles) to 2 (fewer, bigger ones). @default 1.

Default
1
speed?number

Animation speed: 1 is normal, 0 holds the frame. @default 1.

time?number

A fixed moment, in seconds: draws it and doesn't animate.

toColor?string

The third background color, in drifting patches. Any CSS color. @default "#e46fd0".

Default
"#e46fd0"
viaColor?string

The main background color, across the middle. Any CSS color. @default "#6fd66a".

Default
"#6fd66a"
render?ReactElement | ((props: object, state: object) => ReactElement)

Replace the host element or compose it with another component.

Type
ReactElement | ((props: object, state: object) => ReactElement)
className?string

Forwarded to the part.

All 101 shaders