跪拜 Guibai
← Back to the summary

Five Touchable Background Effects That Replace CSS Gradients

Published: 2026-08-31 Tags: Frontend / React / Canvas / WebGL / Component Library / cos-design / Visual Effects / Interaction Design

When building campaign pages, login screens, or brand landing pages, the background is often an awkward element: pure CSS gradients feel too "template-site," Three.js fluid demos are too heavy, and hastily copied CodePen snippets usually only last for a single campaign.

I've been filling this gap in cos-designinstallable, parameter-tunable, interactive background effects. v3.6 delivered a deep-sea bubble field, v3.5 brought water ripples and smoke fog, and v3.7 turned image previews into 13 kinds of "touchable" interactions.

v3.8.0 drops 5 heavyweight background components in one go, bringing the total component count to 91. Each has a completely different character, but they all share the same engineering conventions: fill to cover the parent, bindVisibilityPause to pause on tab switch, prefers-reduced-motion static-frame degradation, and independent @cos-design/* sub-packages.

Full article about 12 minutes. I suggest opening the Playground first (sidebar → Background Effects) and reading while interacting—I've written each component in sidebar order: first explaining why it was made, then breaking down the core implementation, and finally leaving key interaction frames for you to compare.

Repository: github.com/jiaxiantao/cos-design · Version: [email protected]

Component Chinese Name Rendering Path Independent Package
SoapBubbles Soap Bubble Sky Canvas 2D thin-film optics + merging @cos-design/soap-bubbles
DandelionField Dandelion Sowing Canvas 2D lifecycle + physics @cos-design/dandelion-field
LavaBubble Lava Bubble CPU field simulation + WebGL shading @cos-design/lava-bubble
InkBloom Ink Staining Clear Water Canvas 2D density/velocity field @cos-design/ink-bloom
AuroraVeil Aurora Veil Canvas 2D light-band contour @cos-design/aurora-veil

Opening: Five Scenes, Five "Media"

The five images below are the default states of the five new components in the Playground—from soap bubbles to aurora, each one increasingly "unlike anything CSS can produce":

SoapBubbles — iridescent films slowly rising

DandelionField — multiple puffballs standing still on a grassy slope

LavaBubble — dark red lake surface randomly bulging

InkBloom — a basin of still-shallow clear water

AuroraVeil — starry night with hanging light bands

These five components are not "five color schemes"—they are five media models:

Externally, they're just a few props; internally, each splits into its own modules. In the next five chapters, I'll follow this order—each section ends with a Playground link so you can compare the effects directly.


1. SoapBubbles: Thin-Film Optics, Not Just a Few Semi-Transparent Circles

The first background in v3.8.0 starts with the "lightest" approach: pure Canvas 2D, no field simulation, but it has to fool the eye—the bubble film must shimmer with iridescence, bubbles must merge when they touch, and popping must splash water droplets.

SoapBubbles default drifting — bubbles at different depths each have iridescence

What problem does it solve?

"Soap bubble backgrounds" fail most easily: drawing a few arc shapes with white highlights makes them look like glass marbles. The key to real soap bubbles is thin-film interference—thickness variation producing rainbow color patches—and the metaball shape of the neck contraction during merging.

Core Implementation: film + merge + pop trio

Film color film.ts: filmRgb(thickness) interpolates between 8 interference color stops; drawSoapIridescence scatters 7~12 softBlob color patches + a rim color band inside the bubble, lit by lambertAtAngle.

Motion: Each bubble has rise (upward force), gust (gust multiplier), and drift (horizontal target), tracking target velocity with exponential easing—not uniform ascent, but "sometimes fast, sometimes slow, swaying left and right."

Merging merge.ts: At close range, weak attraction first; upon contact, 30% chance of double-pop, 70% entering the merge state machine. resolveMergePose splits into approach / absorb phases, volume conserved by ∛(r₁³+r₂³), drawn with metaball contours (same approach as BubbleField).

Bursting popBubble: 14+ bead droplets fly out in the direction opposite the click + 28+ mist particles; after the pop animation ends, spawnBubble replenishes a new bubble from the bottom.

import { SoapBubbles } from '@cos-design/soap-bubbles';

<SoapBubbles fill count={28} speed={1} />

Left: click to pop, film fades while droplets fly out. Right: two bubbles approach and merge, neck contracting into a metaball conjoined shape.

SoapBubbles click burstSoapBubbles double merge

Takeaway in one sentence: Iridescence comes from thin-film thickness interpolation; the soul is the merge state machine—volume conservation + metaball drawing.

👉 Try it: #/soapBubbles


2. DandelionField: A Whole Meadow of Breathing Life Cycles

Soap bubbles are "dead-object physics"; for the second component, I wanted to do living-thing logic—not particles floating upward, but dandelion plants actually growing taller, blooming, turning into puffballs, being blown apart by the wind, and germinating again. In the Playground, after the intro animation finishes, you'll see a grassy slope full of mature puffballs—that's the best reading starting point.

DandelionField mature state — multiple puffballs standing still on a grassy slope

What problem does it solve?

There are many "drifting seed particle" demos, but few are made into a complete ecosystem: germination → blooming → turning into puffball → blown apart by wind → seeds land → germinate again. DandelionField demands this closed loop, and mouse sweeps should feel like actually blowing wind.

Core Implementation: Modular Lifecycle

The source code is split into six files by responsibility—a refactor specifically mentioned in the v3.8.0 CHANGELOG:

dandelionField/
├── index.tsx      # React shell, main loop, pointer wind field
├── plant.ts       # Plant state machine: sprout → flower → puffing → mature → wither
├── seed.ts        # Attachment/detachment, drifting, landing, germination
├── draw.ts        # Stems, leaves, puffballs, seeds, glow
├── scene.ts       # Static background + grass cluster wind sway
└── frame-cache.ts # Plant geometry cache (head position not calculated twice per frame)

The plant state machine is the soul. Take mature as an example: first matureHoldLeft randomly dwells, after expiry beginReleasing, releasing seeds irregularly along a scheduledRelease timeline—outer ring flies first, inner ring later, rather than an all-at-once explosion.

Pointer wind field: pointermove calculates gust based on displacement speed; when sweeping over mature/puffing puffballs and distance < 105×scale, boostPlantRelease accelerates seed dispersal; clicking the nearest puffball triggers boostPlantRelease(2.8) to blow the whole head apart.

Seed germination: After landing, delay germinateDelay, decide whether to grow a new plant nearby based on GERMINATION_CHANCE / GERMINATION_NEAR_CHANCE—the closer to the parent plant, the higher the probability, visually like "a patch of grass slowly spreading."

import { DandelionField } from '@cos-design/dandelion-field';

<DandelionField fill plantCount={10} seedCount={32} speed={1} />

Left: quickly sweeping over puffballs, wind field accelerates seed dispersal. Right: clicking blows the whole head apart, the instant seeds just leave the body.

DandelionField swipe wind dispersalDandelionField click blow puffball

Performance optimization (v3.8.0 focus): PlantFrameCache caches head/life geometry; AttachedSeedTracker uses counters instead of per-frame filter for attached seeds—frame rate is more stable when there are many plants.

Takeaway in one sentence: This is not a particle system; it's a plant state machine with a germination closed loop + pointer wind field.

👉 Try it: #/dandelionField (suggest waiting for intro to finish before touching)


3. LavaBubble: CPU Computes the Field, GPU Only Looks

The first two are both Canvas 2D. For the third, I wanted to do a "hard medium"—a lava lake. The difficulty isn't in color gradients, but in the complete event chain: bulging → thinning → muffled cracking → cavity → backfill, plus being clickable and draggable to tear. This is the only one of the five that uses WebGL.

LavaBubble auto bubbling — lake surface randomly bulging

What problem does it solve?

Lava/magma visuals on the web are often done as looping videos or pure shader noise—watchable, but hard to make click-to-bulge → shell crack → splash → cavity backfill into a complete event chain. LavaBubble splits it: CPU 192×192 field simulation writes textures, WebGL fragment shading reads height/heat/cavity to do normal-based lighting.

Core Implementation: Blister Four-Stage State Machine

Each blister goes through:

inflate (shell bulging) → thin (thinning, crack heating) → burst (tearing cavity + splash) → cavity (backfill, residual heat)

sim.ts maintains three scalar fields:

upload() packs an RGBA texture: R=height, G=heat, B=cavity.

The fragment shader shaders.ts, after sampling the texture:

  1. calcNormal computes normals (cavities "dig deeper" normals)
  2. heatColor(t) six-segment gradient—the lowest is still dark red, avoiding pure black crust
  3. wrap lighting + double-layer specular + cavity rim residual heat

Two interaction paths:

import { LavaBubble } from '@cos-design/lava-bubble';

<LavaBubble fill heat={1} autoSpawn activity={1} speed={1} />

Left: burst phase after click, cavity + splash heat streaks. Right: thin phase with shell bulging and crack heating.

LavaBubble click burstLavaBubble shell bulging

Why CPU + GPU split? Bubble lifecycle, splash particles (three spatter types: mist/bomb/crust), and heat diffusion need randomness and branching logic—more intuitive on CPU; full-screen pixel shading is handed to GPU, and a 192² texture is enough to express "the undulations and cracks of a lake surface."

Takeaway in one sentence: Simulation on CPU, visuals on GPU—texture RGB packs three: height / heat / cavity.

👉 Try it: #/lavaBubble (keep autoSpawn on, wait a few seconds, watch random bubbling)


4. InkBloom: One Drop of Ink, Two Fields

Lava is "hard shell + fracturing"; for the fourth, I wanted the completely opposite texture—soft, slow, cumulative. An ink drop falling into clear water, dissolving until the whole basin gets deeper and deeper—this "the more you touch, the deeper it gets" feedback can't be achieved by stacking blur circles.

InkBloom just dropped ink — dense ink core + feathery outer halo

What problem does it solve?

The common approach for "ink diffusion" effects is stacking a few blur circles—it looks similar, but lacks the dissolving and the cumulative sense of the whole water being stained deeper. InkBloom uses a lightweight 2D fluid approximation: density field + velocity field + persistent stain field.

Core Implementation: Three Buffers + Low-Resolution Simulation

Simulation grid simW × simH ≈ canvas / 4, each cell maintains:

Field Variable Role
Density dens Currently suspended dense/light ink blobs
Velocity vx, vy Advection + vortex driving
Stain stain Background color already dissolved into water, almost never fading

Main loop (per frame):

applyForces → viscous damping → advect(dens) → diffuseDensity
→ advect(velocity) → dens dissolves into stain → stain slowly spreads evenly
→ paintWater(avgStain) → renderStain → renderInk

Click injection injectDrop does three things: irregular dense ink core (fbm perturbed boundary), 3~6 random vortices (tangential velocity), outer ring 22-point light ink halo. Drag injectStir writes pointer displacement into the velocity field.

One thing worth mentioning separately in the force field is vorticity confinement—deriving lateral thrust from the curl field to pull out wispy filaments, preventing every diffusion from being a perfect circle:

const cx = (curl[i + 1] - curl[i - 1]) * 0.5;
const cy = (curl[i + simW] - curl[i - simW]) * 0.5;
vx[i] += (cy / len) * curl[i] * eps;
vy[i] -= (cx / len) * curl[i] * eps;

Mass-conserving staining: After dense blobs dissolve, mass transfers into stain; paintWater transitions the clear water from #c5dff0 to #12151a based on avgStain—the more you click, the deeper the whole basin gets. This is the biggest experiential difference from "one-shot blur circles."

import { InkBloom } from '@cos-design/ink-bloom';

<InkBloom fill inkColor="#0c0e12" speed={1} />

Top: single drop of ink, feathery diffusion. Bottom: after multiple consecutive drops, the whole clear water is stained deeper—note the background color has shifted from light blue to dark gray.

InkBloom single drop diffusion — wispy filaments

InkBloom multi-drop deepening — whole basin darkens

Takeaway in one sentence: dens is suspended ink blobs, stain is permanent staining already dissolved into water—mass conserved, the more you click, the deeper it gets.

👉 Try it: #/inkBloom (click 5~8 times consecutively, wait ten seconds to see the background darken)


5. AuroraVeil: Painting the Aurora as "Bendable Ribbons"

The last of the five to be made, and also the most "landing-page-ready." Aurora backgrounds are everywhere, but most either look like wallpapers or require intimidating shader parameter tuning. I wanted to use pure Canvas 2D to create an Arctic night where light bands can be bent by the mouse and clicks trigger burst pulses—no WebGL, but enough "wow."

AuroraVeil default state — starry night + multiple layers of hanging light bands

What problem does it solve?

Common "aurora backgrounds" on the market take two paths: CSS gradient animation (lightweight, but looks like wallpaper) or WebGL noise fields (realistic, but high parameter-tuning cost). AuroraVeil chooses the middle ground: pure Canvas 2D, describing light bands with row-by-row contours, then compositing glow using offscreen buffers + screen blending.

Core Implementation: EdgeProfile Light Bands

Each light band (Veil) is not an image, but a column of y → center x, half-width hw samples:

// Each row y: sine wave superposition + drift + pointer magnet + click pulse perturbation
const centerX = (veil, y, time, sheet) => { /* wave + drift + pointer magnet */ };
const halfWidth = (veil, y, time, sheet) => { /* narrows near pointer */ };

// Float32Array caches left/right edges, assembled into a closed ribbon
const buildProfile = (veil, time, sheet): EdgeProfile => { /* leftBuf / rightBuf */ };

Rendering is three layers:

  1. paintSky: deep space gradient + 260 depth-flickering stars + horizon vignette
  2. fillRibbon: horizontal/vertical dual-color gradient, source-atop for vertical attenuation
  3. Offscreen compositing: main sheet screen blends one layer, then a 2.4px blurred glowSheet adds soft glow

Interaction-wise, the pointer magnetically bends the nearest light band and narrows the half-width of the area it passes through; clicking triggers burstRef full-screen ripple + up to 4 spreading pulses.

import { AuroraVeil } from '@cos-design/aurora-veil';

<AuroraVeil fill colors={['#7ee8d8', '#4cc9f0', '#9d8df1']} bandCount={5} speed={1} />

AuroraVeil click pulse — light bands erupt energy ripples

Engineering details: MAX_DPR = 2; prefers-reduced-motion stops star twinkle and freezes light bands; uses @cos-design/shared's useCanvasBox like BubbleField.

Takeaway in one sentence: Light bands are not textures; they are row-by-row EdgeProfile + offscreen glow compositing.

👉 Try it: #/auroraVeil (click in the center of a light band, watch the pulse spread)


6. How to Choose Among the Five? A Table Makes It Clear

After reading about the five components, you've probably already matched them to scenarios in your head. I've condensed the selection into a table—arranged in Playground sidebar order for easy cross-reference:

The Vibe You Want Recommended Component Interaction Method Rendering Cost
Childlike / Summer / Light Campaign SoapBubbles Click to pop, auto-merge Medium (pure Canvas)
Healing / Spring Campaign / Nature DandelionField Swipe to blow wind, click to burst Medium-High (many entities + lifecycle)
Hardcore / Gaming / Dark Hero LavaBubble Click to bulge, drag to tear Medium-High (WebGL + CPU sim)
Chinese Style / Literary / Whitespace Layout InkBloom Click to drop ink, drag to stir Medium (low-resolution field)
Techy / Login Screen / Nordic Night AuroraVeil Move to bend, click to pulse Medium (offscreen blur)

Also, a consistent cos-design recommendation: put only one strong full-screen animated background per page. For a second background, reduce its size or turn off interactive, otherwise they compete visually and for GPU.

If you want to try all five, just click down the Playground sidebar from top to bottom—exactly the same order as this article.


7. Engineering: Sub-packages, Degradation, Next.js

All five components have been independently published and can be installed on demand—no need to drag in the whole library just for one soap bubble:

pnpm add @cos-design/soap-bubbles @cos-design/dandelion-field @cos-design/lava-bubble @cos-design/ink-bloom @cos-design/aurora-veil
# or full install
pnpm add [email protected]

For Next.js App Router, remember dynamic import + ssr: false (Canvas/WebGL is client-only):

import dynamic from 'next/dynamic';

const SoapBubbles = dynamic(
  () => import('@cos-design/soap-bubbles').then((m) => m.SoapBubbles),
  { ssr: false }
);

export default function Hero() {
  return (
    <section style={{ position: 'relative', minHeight: '100vh' }}>
      <SoapBubbles fill />
      {/* foreground content z-index must be higher than canvas */}
    </section>
  );
}

Shared conventions (gradually unified since v3.7, all five new components follow):

@cos-design/shared this version also exports softSat and other math utilities for reuse by subsequent components.


Closing: From Demos to a "Media Library"

Looking back at v3.8.0, these five components are not simply "+5"—they are filling in the media dimension of cos-design's background line:

Together with the earlier WeatherBackground, BubbleField, and RippleWater, they form a library of options for "making heavyweight backgrounds without Three.js."

If you only have time to touch one: summer campaign page, poke SoapBubbles; login screen, bend AuroraVeil; Chinese-style whitespace, drip InkBloom. Whether a background can hold attention often comes down to this layer of interactive media texture—after reading this, open the Playground and walk through the sidebar; it's more intuitive than looking at ten screenshots.


Links and Further Reading

Resource URL
Playground https://jiaxiantao.xyz/cos-design/#/
SoapBubbles https://jiaxiantao.xyz/cos-design/#/soapBubbles
DandelionField https://jiaxiantao.xyz/cos-design/#/dandelionField
LavaBubble https://jiaxiantao.xyz/cos-design/#/lavaBubble
InkBloom https://jiaxiantao.xyz/cos-design/#/inkBloom
AuroraVeil https://jiaxiantao.xyz/cos-design/#/auroraVeil
GitHub https://github.com/jiaxiantao/cos-design
npm https://www.npmjs.com/package/cos-design

Same Series Articles