Build guide

How this clay world holds together

Mellow pairs generated 3D clay imagery with hand-written CSS clay. The whole site rests on one bet: if the CSS material matches the rendered material closely enough, the page reads as a single physical world instead of a photo pasted onto a website. Here's the recipe, the measurements, and the parts that fought back.

Vanilla HTML, CSS, JS No build step 2 generated images 54 KB of artwork
1

Concept

The fiction. Mellow is HR and people ops for teams of 5 to 50. The category is full of software that behaves like a compliance department: dense tables, approval chains, dashboards that want a daily visit. So the product promise is the opposite, and the line writes itself. HR without the HR energy. Everything on the page has to feel like the least stressful possible version of admin, which is why the interactive demo ends with a request approving itself and nobody having a meeting about it.

Why clay. Soft toy clay is the fastest way to make software feel like it isn't going to ask you for a policy number. It's warm, it's handmade, and it has no hard edges to be intimidated by. The risk is that friendly turns into clipart fast. The defence is restraint in layout and richness in material: the page is a plain centred column on a flat cream field, and all the personality lives in how the surfaces catch light.

Why these colours. The field is a single warm cream (#FFF4EC) doing roughly 90% of the work, so the clay pigments read as objects sitting on a studio backdrop rather than as a colour scheme. Peach, butter and mint each carry one job (primary action, highlighted tier, confirmation), and cocoa ink handles every word on the page at 9.8:1. Nothing is decorative-coloured. If a colour appears, it's because an object is made of it.

Why these fonts. Bricolage Grotesque has a wobble in its curves and a variable width axis, so it can be squeezed and stretched per wordmark. It's a display face with a sense of humour, which is exactly what stops the page from reading as a generic pastel SaaS template. Gantari underneath is quiet and round, and gets out of the way. Optical sizing is set to auto so the display cut tightens as the headline grows.

Why bouncy motion. Clay has mass. Every easing on the site overshoots, because a material that squashes has to also wobble back. Nothing uses linear or default ease. Buttons compress wider-and-shorter under the pointer and their shadow tightens, because that's what happens when you press something soft into a table.

2

Technique breakdown

Five things doing most of the work.

The clay material

8 shadow stops

One token, used by every surface on the site. Four inset stops model the object's own curvature (a white key light from the top left, a warm core shadow bottom right), and four outer stops carry it away from the page: a 1px contact shadow, a mid shadow, and two wide ambient stops. Flat fills and single-stop shadows are what make claymorphism look like a bevel filter. The gradient stops are hand-picked rather than mixed toward white, because real clay highlights stay warm.

:root {
  --clay:
    /* the object's own curvature */
    inset 0 4px 7px rgba(255,255,255,.72),
    inset 3px 0 6px rgba(255,255,255,.34),
    inset 0 -7px 13px rgba(74,59,54,.15),
    inset -4px -2px 9px rgba(74,59,54,.09),
    /* and its distance from the page */
    0 1px 1px    rgba(74,59,54,.09),
    0 5px 9px   -3px rgba(74,59,54,.15),
    0 14px 22px -8px rgba(74,59,54,.17),
    0 30px 48px -16px rgba(74,59,54,.13);
}

The squish

scale + shadow shift

Pressing soft clay does two things at once: it spreads wider and flatter, and it gets closer to the surface it's resting on. So the hover state scales x up and y down, nudges the object down a few pixels, and swaps in a shadow set with tighter, shorter stops. Doing only the transform is the common mistake. The shadow is what sells contact. The overshoot in the easing gives it the wobble back.

.btn {
  transform: scale(1);
  box-shadow: var(--clay);
  transition: transform .42s var(--squish),
              box-shadow .42s var(--squish);
}
.btn:hover {
  /* wider, shorter, closer to the table */
  transform: scale(1.055, .9) translateY(3px);
  box-shadow: var(--clay-squish);
}
.btn:active { transform: scale(1.08, .84) translateY(5px); }

/* day cells go the other way: they plump up */
.day:hover { transform: scale(1.14) translateY(-4px);
             box-shadow: var(--clay-plump); }

Compositing the generated plate

the money shot

The generated images have a lit field, not a flat fill, and it's warmer than the page. Butting them against #FFF4EC leaves a visible rectangle. The fix is a scrim: a stack of gradients painted over the image in the exact page colour, dissolving its edges into the layout. The radial stop values are tuned against the image's real subject box (measured at x 22.5–90%, y 31–80%) so the fade reaches zero before it touches any clay. The 0-alpha stops spell out the page colour rather than using transparent.

.stage-scrim { /* sits over the img, inset 0 */
  background:
    /* dissolve everything outside the subject box */
    radial-gradient(ellipse 74% 62% at 56% 55%,
      rgba(255,244,236,0)   55%,
      rgba(255,244,236,.5)  80%,
      var(--bg)           100%),
    /* four-sided insurance: the radial can't reach corners */
    linear-gradient(to right,  var(--bg) 0%, rgba(255,244,236,0) 16%),
    linear-gradient(to left,   var(--bg) 0%, rgba(255,244,236,0) 14%),
    linear-gradient(to bottom, var(--bg) 0%, rgba(255,244,236,0) 15%),
    linear-gradient(to top,    var(--bg) 6%, rgba(255,244,236,0) 17%);
}

Blob geometry

8-value radii

Nothing on the page uses a symmetric radius. Every clay object gets the eight-value border-radius form with each corner a few pixels off its neighbour, which is the difference between "rounded rectangle" and "pinched by hand". The ambient background shapes take the same idea further and animate between radius sets, so they slowly morph while rotating. They run at 13% opacity, because the compositing above only works while the page behind the plate stays the exact cream the scrim fades to.

/* buttons: irregular by a few px per corner */
.blob-a { border-radius: 34px 30px 33px 37px / 33px 37px 30px 34px; }

/* ambient shapes: morph between radius sets while spinning */
@keyframes morph {
  0%   { border-radius: 58% 42% 46% 54% / 48% 56% 44% 52%; }
  50%  { border-radius: 62% 38% 40% 60% / 42% 62% 38% 58%; }
  100% { border-radius: 52% 48% 38% 62% / 44% 52% 48% 56%; }
}
.ablob { animation: morph var(--md) var(--soft) infinite alternate,
                    spin  var(--sd) linear infinite; }

Wobble-in choreography

IntersectionObserver

Everything enters with a slight rotation that settles to zero on a spring, and the rotation direction alternates so a row of cards lands like hand-placed objects instead of a synchronised grid. Reveals fire once and unobserve. The floating chips need three nested elements, because the parallax, the entrance and the idle bob each want to own transform, and the last one to write wins. One transform per layer is the whole trick.

[data-reveal] {
  opacity: 0;
  transform: translateY(26px) scale(.965) rotate(var(--rot));
  transition: opacity .68s var(--soft) var(--d),
              transform .88s var(--spring) var(--d);
}
[data-reveal].is-in { opacity: 1; transform: none; }

/* fires once, then stops watching */
var io = new IntersectionObserver(function (entries) {
  entries.forEach(function (e) {
    if (!e.isIntersecting) return;
    e.target.classList.add('is-in');
    io.unobserve(e.target);
  });
}, { rootMargin: '0px 0px -9% 0px', threshold: .12 });
3

Asset pipeline

Two images, both generated through the Higgsfield MCP on nano_banana_pro (nano_banana_2). Everything else on the page is CSS.

The generated hero plate: three clay figures on a platform
hero-clay · 1200×896 · 970 KB PNG → 26 KB WebP
The generated desk plate: clay laptop, mug, plant and calendar
desk-clay · 1376×768 · 970 KB PNG → 28 KB WebP
Prompt · hero-clay

Three rounded clay blob characters with simple dot eyes, standing together on a clay platform. Soft plasticine texture with visible fingerprints. Flat cream #FFF4EC background. Soft studio lighting, matte finish, no specular highlights. Generous empty space above and below the subjects. 4:3.

Prompt · desk-clay

A clay desk scene: laptop, coffee mug, small potted plant, and a spiral desk calendar, modelled in the same soft plasticine. Scene sits on the right side of the frame with generous negative space on the left for text. Flat cream #FFF4EC background, soft studio lighting, matte finish. 16:9.

The trick, and where it lies to you. Two instructions do the heavy lifting. Naming an exact hex gives you a field you can build a page around instead of guessing. Asking for generous negative space gives you somewhere to put text, and room to hide the seam.

But a generative model renders a lit background, not a flat fill. It reads "#FFF4EC" as a hint, then lights it. So before writing a line of layout I sampled the actual output, and the field came back both warmer than requested and not flat at all:

SampleAsked forActually renderedDelta
Top edge#FFF4EC#F4E5CE30 levels on blue
Near centre#FFF4EC#FDF2E012 levels on blue
Bottom left#FFF4EC#FDF0DF13 levels on blue

So the image carries its own vignette, roughly 18 levels of falloff from centre to corner. Setting the page to the named hex and butting the image against it draws a visible rectangle. That's the trap, and it's why "just use a flat named background" is only half the technique.

What actually works. Don't try to match the field. Dissolve it. The scrim in the technique section paints the page colour back over the image's edges, so there's never a boundary to catch, and the leftover falloff reads as studio lighting. The generous negative space is what buys room for the fade: the subject only occupies the middle 67% of the hero plate, so the scrim has about 250px of empty field to work in before it reaches any clay.

The edge case worth knowing: the desk section sits on a warm band rather than the page cream. Because the scrim fades to a single flat colour, the band behind it has to be that same flat colour. An early version used a subtle gradient there and it produced a 7-level seam at the image's edge, visible as a faint outline, since a gradient can only match a flat colour at one stop. Flat band, no seam.

Verification, not vibes. Eyeballing a screenshot won't catch a 7-level step. Every composite on this page was checked by screenshotting the rendered page, walking pixel columns and rows across each image boundary, and reporting any adjacent-pixel delta of 3 or more. The final pass returns clean on every scan through both plates. The only steps left anywhere near the artwork are the rendered platform's own edge and the CSS chips, which are supposed to have edges.

Optimisation. Both PNGs went through a canvas re-encode to WebP at quality 0.9, which took 1.9 MB down to 54 KB with no visible banding across the smooth field. Worth checking that specifically, because a big soft gradient is exactly where a lossy encoder shows its work.

4

Recreate it

Paste this at Claude to get something in this family. It's structured Role, Task, Context, Format, Constraints, Examples.

Copy-paste prompt

Copied
ROLE
You're an art director and front-end developer. You write hand-made
vanilla HTML/CSS/JS. No frameworks, no build step, no Tailwind.

TASK
Build a one-page marketing site for a product of your choosing, in a
soft clay-toy world. One signature moment: a hero that pairs a
generated 3D clay image with CSS clay UI so convincingly that the page
reads as one physical material, not artwork pasted onto a website.

CONTEXT
Palette: field #FFF4EC, clay peach #FFB39B, butter #FFD6A5,
mint #C9E4DE, cocoa ink #4A3B36. Fonts: Bricolage Grotesque
(display, optical sizing auto) + Gantari (body), via Google Fonts.
The field does ~90% of the work. Colour only appears where an object
is made of it.

FORMAT
index.html + styles.css + main.js. Sections: nav, hero, proof strip,
features with one live interactive demo, product showcase, pricing,
final CTA, footer.

CONSTRAINTS
- Clay = layered shadows or nothing. At least 4 inset stops (curvature)
  plus 3-4 outer stops (contact, mid, ambient). Never a flat fill,
  never a single-stop shadow, never a symmetric border-radius: use the
  8-value form with every corner a few px off its neighbour.
- Scale the shadow to the object. A chip-tuned shadow vanishes under a
  380px card. Keep a separate large-surface token.
- Squish = scale(1.05, .9) + translateY down + a TIGHTER shadow set.
  The shadow change is what sells contact. Custom overshooting
  beziers only, never linear or default ease.
- One transform per element. If entrance, parallax and idle motion all
  want transform, nest them. The last write wins otherwise, and
  animation-fill-mode: forwards will silently kill your hover states.
- Compositing a generated image: sample its real background first, it
  will NOT be the hex you asked for and it will not be flat. Then fade
  its edges into the page with a scrim of stacked gradients in the
  exact page colour. Tune the stops against the measured subject box
  so the fade never touches the subject.
- Verify seams by walking pixels across the boundary, not by eye. Any
  adjacent step over ~3 levels is visible.
- Copy: contractions, no em-dashes, blunt and specific. No lorem
  ipsum, no testimonials, no invented statistics.
- Body text ≥ 4.5:1. Tap targets ≥ 44px. No horizontal scroll at
  390px. Respect prefers-reduced-motion. Console clean.

EXAMPLES
Voice: "HR without the HR energy." / "It does the boring half." /
"Mellow covers the parts of HR that are just admin wearing a lanyard."
Pricing tiers named for the material: Pinch, Handful, Armful.
Interaction: a calendar where day cells plump up under the pointer and
a request squishes into a list with a squash-and-stretch landing.
5

Iteration log

Three passes, screenshotted and read each time. What actually changed.

Pass 1 · Structure

11 fixes
  • Killed the ambient blobs. At 30% opacity and 34px blur they read as a gradient-mesh template, and worse, they broke the site's whole premise: the page behind the plate was no longer the cream the scrim fades to. Dropped to 13% opacity and 72px blur.
  • Blobs were being sliced by section boundaries, drawing a hard horizontal line across the page above the desk section. Added a fade mask to the blob container so a clip edge can't exist.
  • Added a large-surface shadow token. The chip-tuned clay shadow read as nothing under a 380px card, so the feature cards looked like faint ghosts. Cards, tiers, trays and the final slab now use their own scaled set.
  • Real 16px horizontal overflow at 390px. The desk image used width: calc(100% + gutter*2) with a negative margin, but that section carries no inline padding, so it overshot by exactly the gutter.
  • Then a second, subtler 2px overflow. The day cell has aspect-ratio and min-height: 44px, so Chromium derives a 46.8px min-content width. Seven of those blew out the repeat(7, 1fr) track, because 1fr means minmax(auto, 1fr). Pinned the track minimum with minmax(0, 1fr).
  • Contrast failures. The hero note and pricing note used the hairline ink token at roughly 3.3:1. Moved to the body-secondary token at 4.7:1.
  • Fixed three bugs before the first screenshot, found by reading my own code: animation-fill-mode: forwards on the hero plate was permanently overriding the parallax transform, the underline's dash math breaks under non-scaling-stroke (switched to a scaleX wipe), and mobile day cells landed at 43px.
  • The underline overshot the word and ran under the full stop. It was also cutting through the descenders of "energy", which read as a mistake rather than a mark.
  • The fourth hero chip was cream on cream, so it was effectively invisible. Made it tan, which also ties it to the rendered platform.
  • Grew the hero plate from 760px to 820px and bought the pixels back by tightening the copy block, since the diorama looked lonely in the fold.
  • Tightened a 248px canyon between the cards and the desk section caused by doubled section padding, and strengthened the proof strip, which was a whisper.

Pass 2 · Depth

9 additions
  • Gave the desk section its own warm band. The page was one cream tone end to end, and the plate's warmth looked accidental. Scoping the field token to the band makes the image dissolve into a deliberate object instead of the page.
  • That immediately produced three seams, all from one cause: the plate is absolutely positioned, so it never contributed height, the section collapsed to its min-height, and the band's mask fade overlapped the image. Sized the band so its core always contains the plate.
  • Then a 7-level seam survived, because the band was a gradient and the scrim fades to a flat colour, so the two only matched at one stop. Flattened the band to exactly the scrim's colour. Clean.
  • Recut the nav from glass to clay. A frosted-glass pill is off-material in a clay world, and it was dissolving against the cream anyway.
  • Chips now squish under the pointer. This needed the idle bob moved to a wrapper element, because the animation and the hover state were fighting over the same transform.
  • Added an "Out this week" clay well to the requests panel, which was visibly sparse next to the tall calendar.
  • Added meta rows to the feature cards with clay pigment dots, so each card has a hairline and a spec line instead of ending in dead space.
  • Pressed two of the final slab's pebbles into it as thumb-dents, lit from below, so the slab reads as worked clay rather than dots on a rectangle.
  • Themed the scrollbar as an extruded clay rail, and gave the free tier the same price baseline structure as the paid ones so the row doesn't sit ragged.

Pass 3 · Final QA

verified
  • 390px: no horizontal scroll, day cells at 44px, hero chips swap to a static row under the diorama, tiers and cards stack.
  • Pixel-verified every composite by walking columns and rows across both plates. All scans clean at a 3-level threshold.
  • Reduced motion: blobs stop morphing, chips stop bobbing, reveals resolve instantly, the calendar approves without the stagger, and every squish and plump is neutralised.
  • Meta, title, description, Open Graph and an inline SVG favicon on both routes. Console clean on both.
  • Deployed and re-shot against the live URL to confirm production renders identically and the artwork actually loads over HTTPS.
6

Attribution

This site was designed and built entirely by Claude Opus 4.8. The concept, art direction, copy, every line of HTML, CSS and JavaScript, all three iteration passes, and the deployment are its work. No other model contributed to this page.

The clay imagery was generated via the Higgsfield MCP on nano_banana_pro (nano_banana_2). The two prompts used are printed in full in the asset pipeline section above. The images were then re-encoded to WebP and composited with CSS. Everything else you see, including all the clay UI, is CSS.

The wider showcase has two authors. This is one site in a set of ten. The other sites were designed and built by Claude Fable 5. Fable's usage credits ran out partway through the project, and the remaining work was finished on Opus 4.8 with the user's approval. So the showcase carries two models' work, and each site's credit line names whichever model actually built it. This one is Opus 4.8's.

Mellow itself is a fictional product invented for this showcase. The company names in the proof strip are invented too, there are no testimonials anywhere on the site, and none of the numbers are research findings. Nothing here is for sale.