GODRICH
#css-animation#button#loader#transition#scroll#dashboard#ui#design-trends

Marble Effect CSS: 9 Ink, Comb and Agate Patterns

In this marble effect css set, ink on water is a thin film held by surface tension — 04 pushes it into skewed wave bands, 05 combs the grain twice, offset,

Marbling is a pattern lifted from paint floating on water after it has been dropped, pushed and combed, and each scene here rebuilds one of those moves as a small calculation on a canvas.

Drop, Comb and Cut Stone in the Nine Marbling Scenes Below

Each card jumps to its scene's section, and every scene opens with a short self-running demo that hands control to you at your first click, key press or tap.

Auto-plays · click a tile to jump to its section · all nine in one zip

Marble Effect CSS: 9 Flowing Backgrounds Drawn With Ink Diffusion, Comb Teeth and Stone Grain

The order follows what happens to the paint. 01 to 03 are ink resting on the water as it spreads, tears and folds; 04 and 05 drag a rake and a comb through that film; 06 and 07 catch new drops and dots as they land; 08 and 09 close with a slice of layered stone and ink rings widening from their centers. Each scene is drawn on a single <canvas>, and the stylesheet only sets the stage ratio, the corners, the button and the focus outline. The scenes treat paper marbling patterns as rules acting on a field of numbers rather than as stored pictures, so one constant can reshape a whole scene, and those constants are gathered in the values section further down.

Twenty-four captures in two seconds, each compared with the last

Every scene was captured 24 times over two seconds, and each capture was compared with the one before it, which makes 23 comparisons; the last column counts how many of those found a changed stage.

Scene Cumulative area Mean intensity Moving frames
01 Drop bloom 71.4% 36.3 16/23
02 Branching mesh 30.3% 29.0 23/23
03 Fan pleats 72.9% 26.3 23/23
04 Rake wave bands 71.1% 20.5 23/23
05 Staggered comb feathers 73.8% 33.6 16/23
06 Crown splash 14.2% 36.4 23/23
07 Scattered dots 68.5% 26.9 16/23
08 Agate layers 60.4% 31.9 23/23
09 Ink rings 72.0% 56.9 23/23

Eight scenes pass 30% cumulative area, and none falls below 10%. The smallest, 06 at 14.2%, is a vector scene where only the impact ring's outline and a few small beads change between captures. 01, 05 and 07 stop at 16 of 23 because some captures fall in pauses between demo steps, when the pattern holds still, yet all three still sweep more than 68% of the stage.

Where each pattern earns a place on a page

The drop bloom (01) and the fan pleats (03) pull the eye toward the middle of the stage, so they suit a first screen or the head of an invitation where there is little text. The rake bands (04) run as a long horizontal strip that crops neatly into a footer or the edge under a banner, and the staggered comb feathers (05) lay an even grain that can sit behind a newsletter signup. When an input field goes on top, slip an opaque panel between the pattern and the text so the contrast does not shift with every vein underneath.

The crown splash (06) reacts to the instant of a press, which fits click feedback or the beat when a hero section arrives, while the scattered dots (07) suit small repeating surfaces such as stationery or label backgrounds. The agate bands (08) still read as a cut stone when frozen on one frame, so a business card or jewelry banner can use a still of it, and the slowly widening ink rings (09) belong under a calm brand introduction.

What Does a Marbling Technique Compute First Once It Moves Into Code?

In 01 and 05 the first question for each pixel is where its ink sat before the drop or the comb moved it, and the color is looked up only after that answer. The other scenes compute a value field (density, fold, band, layer or ring strength) and turn it into color, and all of them share the plumbing below.

Paint a small buffer, then stretch it onto the stage

Eight scenes, 01 to 05 and 07 to 09, compute pixel colors in a field smaller than the stage (01 160×100, 02 96×60, 03, 07 and 09 192×120, 04, 05 and 08 240×150), write them into a buffer from createImageData, place it with putImageData and stretch it onto the 480×300 stage with drawImage. Setting imageSmoothingQuality to 'high' softens the cell edges during that stretch. getImageData appears in none of the nine files, so nothing reads the screen back, and 06 skips the buffer entirely to draw ellipses and lines directly.

The paper grain is baked once, and time is cut into 16ms steps

The paper grain under the scenes (paperGrain, three octaves of value noise) does not change over time, so initField bakes it into the grainF array once and each pixel reads it from there. An early version recomputed it on every frame and the capture came back short of moving frames, which is why the bake moved to setup. Time comes from the timestamps requestAnimationFrame hands over, sliced into fixed 16ms steps (FIXED_DT) with at most 100ms of backlog and six steps per frame, and the canvas backing resolution is 480×300 times the device pixel ratio, held at 2 by Math.min(window.devicePixelRatio || 1, 2).

Integer hashes instead of random numbers keep the veins in place

Math.random is called zero times. An integer hash mixed with Math.imul (hash1, hash2), the value noise vnoise2 built on it and the octave stack fbm2 supply all of the irregularity, and each call passes its own seed number directly: 211 for the gain map in 02, 811 and 977 for the dot spots in 07, 101 and 313 for the warp in 08, and 401 for the ring sway in 09. A reload therefore grows the same grain in the same place.

The snippet under each scene below is one unbroken run of lines from that scene's index.html script. The function names match the source, so searching for one in the downloaded files lands you on its full surroundings.

01A Landed Drop Blooming Into Rings That Thin Outward

When a drop of radius R lands, each point outside it moves √(1 + R²/d²) times farther from the center, and inkAt applies the inverse factor √(1 − R²/d²) from the newest drop back toward the oldest to find where a screen point sat before any of them fell. Because this displacement keeps area, two BLOOM_RADIUS 96 drops on one spot trace a point 108.2px from the center back to a radius ratio of 0.521 (50/96) inside the first drop, which means ink that sat at 50px was pushed exactly to 108.2px. Inside a drop, five bands of R/RING_N = 19.2px are painted with a line of pushed-back ink at EDGE_PUSH 0.35 that doubles the band, and the render measured 71.4% cumulative area, 36.3 mean intensity and 16/23 moving frames.

A Landed Drop Blooming Into Rings That Thin Outward
Open in a new tab

01 · A Landed Drop Blooming Into Rings That Thin Outward — Measured area 71.384% · moving frames 16/23Original by GODRICH · SCSS and React ship in the zip · See the password

BLOOM_RADIUSRING_NEDGE_PUSH
    function inkAt(x, y, now) {
        let qx = x, qy = y;
        hitI = -1;
        for (let i = blooms.length - 1; i >= 0; i -= 1) {
            const b = blooms[i];
            const R = radiusOf(b, now);
            if (R <= 0) {
                continue;
            }
            const dx = qx - b.x, dy = qy - b.y;
            const d2 = dx * dx + dy * dy;
            if (d2 < R * R) {
                hitI = i;
                hitQ = Math.sqrt(d2) / R;
                return;
            }
            const s = Math.sqrt(1 - (R * R) / d2);
            qx = b.x + dx * s;
            qy = b.y + dy * s;
        }
    }

02A Compressed Ink Field Splitting Into Narrow Branching Veins

Each cell of a 96×60 grid carries a pressure of density × (1.15 − gain), where the gain map is three octaves of fbm2 (SPLIT_DEPTH 3) carving small valleys inside large ones, and each right and lower neighbor pair moves ink by pressure difference × DENSITY_DIFF_K 0.16, clipped at 24% of a cell's density per trade. Trading alone would smear the ink into an even haze, so the reaction term d + EDGE_KEEP·d(1−d)(d−½)·4 at the end of tradeDens pushes in-between values toward 0 or 1 and lets the emptied valleys stand up as sharp gaps. Run in Node, the source functions widen the gap share (cells under 0.1 density) from 10.6% to 22.7% after 160 steps (2.56 seconds), and the render measured 30.3% cumulative area, 29.0 mean intensity and 23/23 moving frames.

A Compressed Ink Field Splitting Into Narrow Branching Veins
Open in a new tab

02 · A Compressed Ink Field Splitting Into Narrow Branching Veins — Measured area 30.344% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

CELL_NDENSITY_DIFF_KSPLIT_DEPTH
    // One trade pass — only right and lower neighbor pairs, so no pair is counted twice. Emptied valleys become gaps,
    // and the reaction term d(1−d)(d−½) pushes in-between values toward 0 and 1 so the gap edges stay crisp
    function tradeDens(dx, dy) {
        const out = new Float32Array(dens);
        for (let y = 0; y < GY; y += 1) {
            for (let x = 0; x < GX; x += 1) {
                const i = y * GX + x;
                if (x + 1 < GX) {
                    trade(out, i, i + 1, dx);
                }
                if (y + 1 < GY) {
                    trade(out, i, i + GX, dy);
                }
            }
        }
        for (let i = 0; i < out.length; i += 1) {
            const d = out[i];
            out[i] = clamp(d + EDGE_KEEP * d * (1 - d) * (d - 0.5) * 4, 0, 1);
        }
        dens = out;
        trades += 1;
    }

03Pleats Radiating From One Point and Folding Like a Fan

Measured from the fan center at 0.44 of the field width and 0.94 of its height, pleatWidth adds RADIANT_K × PLEAT_FREQ × sin(angle) to each angle, and the fold coordinate (angle × 15 + width × 0.5)/π lays 30 bands around a full turn, about 15 of them in the upper half that shows on screen. Where the fractional part of that coordinate passes 0.44, the back half darkens across a 0.12-wide edge, and FOLD_CONTRAST 0.34 splits brightness into two steps, 0.66 on the lit face and 0.474 on the folded one, so the surface reads like pressed cloth. The first demo spin starts at 0.0021rad/ms and slows by a factor of 0.965 per step, turning about 55° before it stops, and the render measured 72.9% cumulative area, 26.3 mean intensity and 23/23 moving frames.

Pleats Radiating From One Point and Folding Like a Fan
Open in a new tab

03 · Pleats Radiating From One Point and Folding Like a Fan — Measured area 72.876% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

RADIANT_KPLEAT_FREQFOLD_CONTRAST
                    let ang = Math.atan2(dx, -dy) + fanA;
                    const w = pleatWidth(r, ang);
                    // Fold coordinate — the pleat width presses the angle so density bunches like folded cloth
                    const fold = (ang * PLEAT_FREQ + w * 0.5) / Math.PI;
                    const fr = fold - Math.floor(fold);
                    // Folded face (back half) is darker — the edge eases over 0.12 so the step does not break into teeth
                    const shade = 1 - 0.58 * smooth((fr - 0.44) / 0.12) * (1 - smooth((fr - 0.92) / 0.08));
                    const tint = TINTS[Math.abs(Math.floor(fold)) % TINTS.length];
                    const edge = smooth(1 - Math.abs(fr - 0.5) * 2);
                    let k = smooth(clamp(1 - r / (FH * 0.98), 0, 1)) * 0.8;
                    // Brightness step per fold — shade and FOLD_CONTRAST set the step between the two faces
                    const stepB = shade * (1 - FOLD_CONTRAST) + (1 - shade) * FOLD_CONTRAST;
                    k = k * stepB + edge * 0.06;

04Wave Bands Pushed One Way and Set Into Chevrons

bandAt numbers each band as (y + tilt + wave + ripple) ÷ BAND_PX 13, where the tilt from CHEVRON_SKEW 0.34 leans the bands by 18.8° and the wave is a sine of amplitude 13 (half of WAVE_AMP 26) whose phase shifts from row to row by sin(y × 0.028) × 1.15. Each step adds direction × PHASE_DRIFT 0.022rad to the phase drift, which comes to 1.375rad per second or a crest sliding sideways at 125px/s, and while the direction holds the grain keeps sliding one way and sets into slanted bands. The demo rakes to the right at 200ms and switches to the left at 1900ms, and the render measured 71.1% cumulative area, 20.5 mean intensity and 23/23 moving frames.

Wave Bands Pushed One Way and Set Into Chevrons
Open in a new tab

04 · Wave Bands Pushed One Way and Set Into Chevrons — Measured area 71.056% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

WAVE_AMPPHASE_DRIFTCHEVRON_SKEW
    function bandAt(x, y) {
        // Phase-offset sine sum — each row's phase differs to make waves, and the sideways tilt makes the chevron
        const tilt = CHEVRON_SKEW * (x - W / 2);
        const wave = WAVE_AMP * 0.5 * Math.sin(x * 0.011 + Math.sin(y * 0.028) * 1.15 + drift);
        const fine = WAVE_AMP * 0.22 * Math.sin(x * 0.0031 + y * 0.052 - drift * 0.6);
        return (y + tilt + wave + fine) / BAND_PX;
    }

05Two Opposite Comb Passes, Staggered, Splitting Into Feathers

tineShift gives the pull at horizontal distance d from the nearest tooth line as amount × λ/(d + λ), and with λ = 14 × 0.55 ÷ FEATHER_SPLIT_K 1.3 = 5.92px, a 64px pull on the tooth line falls to 29.3px half a gap (7px) away. sourceY undoes the first comb, dragged down, and then the second, its teeth shifted by COMB_TOOTH_GAP × STAGGER_PHASE = 7px and dragged up, to fetch the color of the 16px horizontal bands laid at the start, so the bands split into feathers wherever the two pulls cross. Across 480px, 34.3 teeth pass, the first 64px pull takes 1138ms and the second 51.2px pull 910ms, and the render measured 73.8% cumulative area, 33.6 mean intensity and 16/23 moving frames.

Two Opposite Comb Passes, Staggered, Splitting Into Feathers
Open in a new tab

05 · Two Opposite Comb Passes, Staggered, Splitting Into Feathers — Measured area 73.832% · moving frames 16/23Original by GODRICH · SCSS and React ship in the zip · See the password

COMB_TOOTH_GAPSTAGGER_PHASEFEATHER_SPLIT_K
    // How far one tooth line drags the ink — amount·λ/(d+λ) at horizontal distance d from the nearest tooth line
    function tineShift(x, offset, amount) {
        const g = COMB_TOOTH_GAP;
        let u = (x - offset) / g;
        u -= Math.round(u);
        const d = Math.abs(u) * g;
        const lam = (g * 0.55) / FEATHER_SPLIT_K;
        return amount * lam / (d + lam);
    }
    // Which height held this screen point's ink before combing — both combs drag only vertically, so undoing y is enough
    function sourceY(x, y) {
        return y - tineShift(x, 0, pull1) + tineShift(x, COMB_TOOTH_GAP * STAGGER_PHASE, pull2);
    }

06Ink Rising Off the Impact Ring and Falling Back as a Crown

Scene 06 draws ellipses and lines with no pixel buffer: when a drop lands, CROWN_N 28 beads ride outward with the impact ring at RING_PULSE 1.1px per step while rising at a starting speed of TANGENT_EJECT × 0.6 to 1.1, and GRAVITY_FALL takes 0.36 off that speed on each step. The moment a bead's height drops below 0, stepBead fixes that spot as a landed dot, and measured in Node the slowest bead climbs 17.4px over 320ms to land at radius 28px while the fastest climbs 61.2px over 592ms and lands at 46.7px. The water's circle is squashed vertically by TILT 0.46 into an ellipse for a slanted view, beads higher on screen are painted first so the overlap reads front to back, and the render measured 14.2% cumulative area, 36.4 mean intensity and 23/23 moving frames.

Ink Rising Off the Impact Ring and Falling Back as a Crown
Open in a new tab

06 · Ink Rising Off the Impact Ring and Falling Back as a Crown — Measured area 14.168% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

RING_PULSETANGENT_EJECTGRAVITY_FALL
    // One bead step — carried outward with the ring (RING_PULSE) while gravity slows its rise; below height 0 it lands
    function stepBead(b) {
        if (b.land > 0) {
            b.land = Math.min(1, b.land + 0.025);
            return;
        }
        b.r += RING_PULSE;
        b.vh -= GRAVITY_FALL;
        b.h += b.vh;
        if (b.h <= 0 && b.vh < 0) {
            b.h = 0;
            b.land = 0.01;
        }
    }

07Scattered Dots Bleeding and Stretching Into Mottled Speckles

seedDots compares one hash value against SCATTER_P 0.55 at each of 814 inner spots on a 5px grid and places a dot only when the hash is smaller, which yields 435 dots (53.4%), and the spot hashes also fix each dot's offset, size (1.3 to 2.8) and color so a rerun puts the same dot in the same place. splatDots walks only each dot's own radius box, stacking a (1 − r²)² × 0.55 weight together with the dot's color at that weight, so overlapping spots settle on a weighted average of neighbor colors and each dot bleeds to 12.5 to 26.9px on screen. The kernel's horizontal radius and the x position then stretch up to STRETCH_ALONG_COMB 1.9 times along the comb direction, and the render measured 68.5% cumulative area, 26.9 mean intensity and 16/23 moving frames.

Scattered Dots Bleeding and Stretching Into Mottled Speckles
Open in a new tab

07 · Scattered Dots Bleeding and Stretching Into Mottled Speckles — Measured area 68.47% · moving frames 16/23Original by GODRICH · SCSS and React ship in the zip · See the password

SCATTER_PBLEND_RADIUSSTRETCH_ALONG_COMB
    function seedDots() {
        dots = [];
        let i = 0;
        for (let gy = 1; gy < FH / 5 - 1; gy += 1) {
            for (let gx = 1; gx < FW / 5 - 1; gx += 1) {
                const h = hash2(gx, gy, 811);
                // Place a dot in an inner grid cell when the seed hash falls under the probability — the same spot each run
                if (h < SCATTER_P) {
                    dots.push({
                        x: gx * 5 + (hash2(gx, gy, 977) - 0.5) * 3.6,
                        y: gy * 5 + (hash2(gx, gy, 341) - 0.5) * 3.6,
                        s: 1.3 + hash2(gx, gy, 553) * 1.5,
                        tint: Math.floor(hash2(gx, gy, 761) * TINTS.length) % TINTS.length,
                        k: 0.55 + hash2(gx, gy, 197) * 0.5,
                    });
                    i += 1;
                }
            }
        }
        scatterT = 0;
        stretchT = 0;
    }

08Stone Bands Whose Width Shifts With Per-Layer Density

warp is domain warping: it bends the screen coordinate itself through two 4-octave fbm2 calls, so instead of shaking the picture it moves the address used to look up a layer by up to 21.9px, and that bend drifts by DOMAIN_SHIFT 0.0048 on each step. layerAt takes the layer coordinate (x + 0.22y) × 0.055 plus low-frequency noise and darkens only the part of its fractional value above 1 − AGATE_LAYER_DENSITY, so in samples taken every 3px the dense-layer share comes out at 32.0%, close to the density value of 0.35. A thin gloss sits at the top of each dense layer (around 0.82) so the slice reads as cut stone, and the render measured 60.4% cumulative area, 31.9 mean intensity and 23/23 moving frames.

Stone Bands Whose Width Shifts With Per-Layer Density
Open in a new tab

08 · Stone Bands Whose Width Shifts With Per-Layer Density — Measured area 60.386% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

AGATE_LAYER_DENSITYWARP_AMPDOMAIN_SHIFT
    function warp(x, y) {
        // Domain warp — bend the coordinate itself with noise so the layer boundaries curve
        const a = (fbm2(x * 0.012 + warpT, y * 0.012, 4, 101) - 0.5) * 2 * WARP_AMP;
        const b = (fbm2(x * 0.012 + 519.3, y * 0.012 - warpT * 0.7, 4, 313) - 0.5) * 2 * WARP_AMP * 0.8;
        return [x + a, y + b];
    }
    function layerAt(wx, wy) {
        // Layer coordinate — the band axis of the agate slice; dense layers run thin and tight
        const s = (wx + wy * 0.22) * 0.055 + fbm2(wx * 0.006, wy * 0.006, 3, 61) * 1.5;
        const fract = s - Math.floor(s);
        // Layer density sets the band width — AGATE_LAYER_DENSITY decides the dark share inside a band
        const dense = smooth((fract - (1 - AGATE_LAYER_DENSITY)) / AGATE_LAYER_DENSITY);
        return dense;
    }

09Concentric Rings Spreading From Centers, Swaying and Overlapping

ringField grows rings around each center at RING_SPEED 0.9px per step (56.25px per second) and sways each ring's radius with noise that flows per angle, up to ±4.16px (RADIUS_NOISE 0.16 × RING_GAP 26), so the ink line trembles. The return value is Σc + INTERFERE_K × ((Σc)² − Σc²)/2, and since rings from one center sit 26px apart the product term stays near 0 among them, so ink darker than the plain sum appears only at the nodes where rings from two centers cross. The demo drops ink on the first center at 200, 480 and 760ms and on the second at 1000 and 1300ms without erasing older rings, and the render measured 72.0% cumulative area, 56.9 mean intensity and 23/23 moving frames, the highest intensity of the nine.

Concentric Rings Spreading From Centers, Swaying and Overlapping
Open in a new tab

09 · Concentric Rings Spreading From Centers, Swaying and Overlapping — Measured area 72.013% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

RING_SPEEDRADIUS_NOISEINTERFERE_K
    function ringField(x, y, now) {
        // Concentric rings spreading in time — the radius grows each step, and the gaps between rings form the wave pattern
        let v = 0;
        let v2 = 0;
        for (let i = 0; i < rings.length; i += 1) {
            const rg = rings[i];
            const dx = x - rg.x, dy = y - rg.y;
            const r = Math.sqrt(dx * dx + dy * dy);
            if (r > rg.r + 2) {
                continue;
            }
            // Radius noise — noise flowing per angle makes the grain sway
            const ang = Math.atan2(dy, dx);
            const sway = RADIUS_NOISE * RING_GAP * (fbm2(Math.cos(ang) * 2.4 + 7.1, Math.sin(ang) * 2.4 + now * 0.00021, 3, 401) - 0.5) * 2;
            const d = r - (rg.r + sway);
            const w = RING_GAP * 1.3;
            if (Math.abs(d) < w) {
                const c = Math.exp(-(d * d) / (w * w * 0.24)) * (1 - rg.r / RING_MAX_R);
                v += c;
                v2 += c * c;
            }
        }
        // Two-center interference — where rings overlap, add the product term ((Σc)² − Σc²)/2 scaled by INTERFERE_K.
        // Rings from one center sit RING_GAP apart and barely overlap, so this term rises at the nodes where two centers' rings meet
        return v + INTERFERE_K * (v * v - v2) * 0.5;

Diffusion Radius, Comb Tooth Gap, Layer Density — The Numbers Behind the Veins

None of these values came from the render. inkAt, tineShift, layerAt and tradeDens were called in Node with one constant changed at a time, so read them apart from the capture table above. This page has no marble background generator with sliders, but each heading below works like one of its controls: change the named constant in the downloaded source and the scene redraws with a new grain.

Raising BLOOM_RADIUS from 96 to 200 lets one drop nearly fill the stage

One drop's footprint grows from 20.1% to 87.3% of the screen. RING_N stays at 5, so each band thickens from 19.2px to 40px, and ink that sat 50px from the center ends up at 206.2px instead of 108.2px. Each new drop then shoves most of the earlier pattern off the stage, leaving one large set of rings instead of stacked layers of grain.

Widening COMB_TOOTH_GAP from 14 to 34 makes each feather 2.4 times wider

The teeth crossing 480px drop from 34.3 to 14.1, and the falloff width λ grows from 5.92px to 14.38px. The pull halfway between teeth stays at 29.3px in both cases, because λ scales with the gap, so the feather keeps its proportions and only its size changes.

Raising AGATE_LAYER_DENSITY from 0.35 to 0.72 clumps thin bands into coarse stone

The share of samples inside a dense layer rises from 32.0% to 65.9%, and the mean density from 0.174 to 0.359. WARP_AMP stays at 26, so the largest boundary bend holds at 21.9px. Band weight and bend live on separate constants, which lets you tune how heavy the stone looks apart from how much it waves.

Dropping SPLIT_DEPTH from 3 to 1 opens the gaps a little sooner

With a single octave the gain map keeps only its large valleys and loses the small ones nested inside them. After the same 160 steps the gap share reaches 24.5% instead of 22.7%, so a few thick gaps open first in place of fine branching ones.

When Close Palette Colors, Oversized Patterns and Reduced Motion Break the Veins

At a fold contrast of 0.5 the brightness steps in 03 merge

The lit face's brightness is 1 − FOLD_CONTRAST and the folded face's is 0.42 × (1 − contrast) + 0.58 × contrast, so at exactly 0.5 both steps land on the same 0.5. The name suggests that more contrast means sharper pleats, but past 0.5 only the folded face's step value climbs above the lit face's, while a separate line keeps mixing ink into the folded face. At 0.5 itself the brightness step disappears, yet that ink mix keeps a shade on the folded face, so the pleats flatten without vanishing.

A wider blend radius in 07 turns the colors muddy

As BLEND_RADIUS grows, more neighboring dots overlap at each spot, and their weighted average drifts toward a grayish middle of the five pastel tints. Raising SCATTER_P to 1 fails in a similar way: all 814 spots fill and the speckles turn into an even ground.

A large drop in 01 shoves the earlier pattern out at once

Past DROP_MAX 7 drops, the oldest drop is discarded on the assumption that it has already been pushed off the stage. If a big drop has been sitting in the middle of the stage for a while, it can vanish in a single frame. Each pixel walks back through every drop, so a higher cap raises the work in proportion.

The stretch in 07 pushes right-side dots off first

The stretch multiplies the x coordinate itself, so dots at the left edge barely move while dots farther right travel farther. At 1.9 times, the dots in the right half leave the stage, so for a full-width background move the stretch origin to the center.

When the bend in 08 outgrows the layer spacing, bands break into islands

Layers sit about 18px apart, and if a larger WARP_AMP pushes the bend past that spacing, one layer crosses the same row twice and folds over itself. Bands that should run on then break into islands and look more like stains than stone.

The crown in 06 does not flare outward

Each bead is an approximation that rises and falls straight along the ring wall, so the rim does not spread outward and split the way a real crown would. To get that flare, give each bead an outward speed of its own and add it to b.r, the bead's distance from the ring's center, on each step while the bead is in the air. A GRAVITY_FALL that is too small stretches the flight past RING_LIFE 1500ms so beads hang in the air after the ring fades, and drops near the edge would throw beads off the stage, which is why pointer coordinates are clamped to 40 to W−40 across and 60 to H−40 down.

Without its reaction term, 02 freezes within 0.75 seconds

The first version ran with EDGE_KEEP at 0, and the ink spread into an even haze that converged in 0.75 seconds, after which not one frame changed. In the other direction, a larger DENSITY_DIFF_K that keeps hitting the 24% cap clips the flow and sets the grain into a grid.

Two combs on the same lines cancel out in 05

With STAGGER_PHASE at 0 the second comb drags the very lines the first one dragged, in the opposite direction, and when the two pulls are equal the pattern returns fully to its starting bands. On the 240×150 field a 14px gap spans 7 field pixels, so a gap under 6px leaves each tooth under 3 field pixels and broken lines take the place of feathers.

A larger interference factor in 09 clots the nodes

Raising INTERFERE_K pushes the nodes where two centers' rings meet up against the ink ceiling of 0.92, so they swell into blobs instead of points. Rings past RING_MAX_R 320 are invisible yet keep being computed while they remain among the latest 24.

Stretched small fields show stair steps

01 stretches 160×100 three times and 02 stretches 96×60 five times, so up close the band and cell edges look stepped. A larger field smooths the steps, but the work grows with width times height, and 08 is already the heaviest of the nine, running two 4-octave and two 3-octave noise calls per pixel across 240×150, or 36,000 pixels.

Under reduced motion, the rise of the crown in 06 is not shown

With reduced motion on, 06 draws only the result of finishSplash, which runs the same stepBead until every bead has landed. The dots settle in the same places as in the animation, but the rising crown itself is skipped, and 09 likewise recomputes fully spread rings without showing them spread.

If you would rather find each breaking point by hand, one constant at a time, download the zip below and unlock it with 95rs6qwq. The plain HTML version (vanilla/) and the React version (react/) share the same constant names, so every name in this article turns up in either one.

Turning Ink, Comb and Stone Values, and Accessibility for a Flowing Backdrop

Change Value What you see
Change the ink diffusion radius 01 BLOOM_RADIUS 96 → 200 One drop covers 87.3% of the screen and each band thickens to 40px, so a single large set of rings replaces the stacked grain.
Change the comb tooth gap 05 COMB_TOOTH_GAP 14 → 34 Teeth fall to 14.1 and feathers grow 2.4 times wider, while the 29.3px half-gap pull keeps their proportions.
Change the stone layer density 08 AGATE_LAYER_DENSITY 0.35 → 0.72 The dense-layer share rises to 65.9% and bands clump into a coarse grain, with the 21.9px bend unchanged.

You can play each of the nine scenes without a mouse. Each canvas carries tabindex="0" and an aria-label with the scene's name, and the keys differ by scene: in 01 the arrows move a ring-shaped cursor and Enter or Space drops ink, 02 pushes the ink with the arrows, 03 turns the fan with ← and →, and 04 sets the comb direction with ← and →. In 05 ↓ pulls the first comb and ↑ the second, 06 moves a cursor drawn as a pressed ellipse with the arrows and drops with Enter or Space, 07 scatters again on Enter or Space, 08 swaps in another slice with ←, → or Enter, and 09 drops ink with Enter or Space. preventDefault runs only for the keys a scene uses, so Tab still moves focus; each scene has one <button>, and both canvas and button show a :focus-visible outline. On touch screens touch-action: pan-y lets a vertical swipe across the stage keep scrolling the page.

The status line sits quietly with aria-live="off" while the demo plays, and the first pointer, key or button press calls wake(), which ends the demo and switches it to polite. That order keeps a screen reader from narrating a demo that nobody started.

prefers-reduced-motion is checked by each scene twice, once through matchMedia in the script and once in the stylesheet. With the setting on, no rAF loop starts, and the scene is drawn a single time in its finished state. Input then jumps straight to its target without in-between frames: 01 adds a fully spread drop, 02 runs 24 trades at once, 03 rotates 0.28rad, 04 applies a 0.9rad phase, 05 moves to the target pull, 06 shows every bead landed, 07 goes to the full scatter with a 0.55 stretch, 08 swaps the slice and 09 shows fully spread rings, each followed by a single draw() call.

How Far Can Marble Background CSS Go Without a Library?

No library, CDN or image file is involved in any of the nine scenes, but the motion is script rather than CSS, and CSS builds only the frame. .hs sizes the stage with width: min(92vw, calc(160vh - 12.8vw)) and aspect-ratio: 480 / 300, then adds 16px corners, overflow: hidden and a shadow. None of the nine stylesheets holds a @keyframes rule. Layered gradients can paint fixed patterns such as stripes or concentric circles, but a new drop in 01 that shoves every earlier drop aside, or neighboring cells in 02 that trade ink, depend on the frame that came before, and a set of style rules had no way to carry that state in these scenes.

If fixed patterns redrawn from a seed are what you need, the seeded background pattern scenes are the closer neighbor, and the sunset gradient scenes show how much gradient layers alone can do to a sky. For deeper pixel work, carry on to the droplet refraction canvas effects and the grid fluid simulation, which moves ink from cell to cell. The way each buffer reaches the screen follows the putImageData and requestAnimationFrame references.

Questions to Ask Before Lifting Marble Texture CSS Veins Onto Your Own Page

Where do I change the colors to match my own site?

In each of the nine files, lines 27 to 34 near the top of the script hold the paper color PAPER (252, 246, 234), the ink INK (58, 48, 76) and TINTS with five ink colors. Swapping those arrays recolors the scene and leaves the math untouched. If text will sit on the pattern, test its contrast against both the darkest and the lightest of those colors.

Can these paper marbling patterns come out different on each reload?

Not as shipped, because the seed numbers passed to the hash calls are fixed and the same pattern returns on each load. Replace 811 in hash2(gx, gy, 811) for 07, or 101 and 313 in the fbm2 calls inside warp for 08, and the grain changes; for a new pattern per visit, derive those numbers from a value that differs per load. Each file also declares a SEED constant of 463, but the current functions do not read it and use the numbers written at each call instead.

Is it heavy to stretch across a full page background?

The cost follows the field's pixel count, not the screen size. A bigger stage keeps the same field and only stretches it further, so the work stays flat while the stair steps grow with the stretch factor. For 08, the heaviest scene, shrink its field or use one frozen frame, which still reads as a stone slice, and first ask whether that spot needs motion at all.

Enter the archive password

The password is inside this article. You will find it as you read.