Areas
3D surfaces, scenes and backgrounds
Depth for your pages in two techniques. CSS perspective for devices, cards and scroll scenes, small WebGL shaders for living backgrounds. Both start on screen, rest outside of it and show a still picture for every visitor.
On this page
CSS 3D or WebGL#
The kit builds depth in two ways. CSS 3D puts real content into space, your screens, cards and text stay HTML, sharp, selectable and clickable. WebGL paints a picture behind the content, light, particles or a sphere of dots, pixel by pixel with a small shader.
Pick CSS 3D when the content itself carries the depth, such as a product screen in a turned device or pricing cards that lean toward the pointer. The 3D surfaces stand in space for this, the 3D scenes move while the page scrolls.
| Component | Variants | What it does |
|---|---|---|
| card-stack-3d | fan, deck, cascade, ring, drum | Cards arranged in real depth, a fan in a hand, a deck to deal from, a cascade of windows, a turning ring and a drum to flip through. Live HTML on every card, arrow keys everywhere, the ring pauses on hover and focus. |
| device-3d | phone, laptop, tablet, browser, angles | A phone, tablet, laptop or browser window in real CSS perspective, with an edge and a back. It rests at a camera angle, leans toward the pointer and floats gently while on screen, your content stays live inside. |
| flip-card | team, facts, product, stats, gift | Cards with a front and a back that turn over in 3D, on hover, click, tap or with the keyboard. The hidden side is inert, both sides share one cell so nothing jumps, and reduced motion cross fades. |
| tilt-card | features, product, profile, pricing, ticket | Cards that lean toward the pointer like glass, with a light that follows and contents floating at their own depth. Focus lifts them too, on touch screens and with reduced motion they lie flat and calm. |
| depth-layers | interface, scroll, diorama, inspector | A picture that comes apart into its layers on hover, focus, a button or the scroll. Every layer keeps its name in real text beside it, without JavaScript the fan still opens on hover. |
| isometric-stage | dashboard, stack, page, workflow | A product or system drawn as plates in an isometric room, lifted apart into its layers. A legend beside it lifts one plate on hover or focus, a button puts the stage together and takes it apart. |
| Component | Variants | What it does |
|---|---|---|
| depth-text | extrude, layers, rise, echo, crawl | Type in 3D layers that turn, rise and come apart with the scroll. The words stay one real heading, the depth comes from copies hidden from screen readers. Without JavaScript the finished picture stands. |
| camera-flight | chapters, gallery, timeline, words | A camera that flies forward through planes of content while the page scrolls. Each plane comes out of the haze, sharpens in front of you and passes by. Without JavaScript the planes stand as a calm list. |
| floating-panels | features, shelf, orbit, split | Panels that float in space and come forward one after another while the page scrolls. The one in front turns to face you, the others wait in the room. Without JavaScript they stand in a calm grid. |
| perspective-floor | grid, stage, bars, horizon | A floor of lines or dots that runs to a soft horizon, as a stage for a heading, cards, figures or one offer. It streams toward you while scrolling and drifts slowly while on screen, text always stands on the calm page colour. |
| rotate-showcase | phone, laptop, cube, card | An object that turns with the scroll and shows a new side or page at every turn, with the matching chapter beside it. Screens change while the back faces you, so the change is never seen. |
| tunnel-zoom | cards, frames, rings, portal | A zoom through a tunnel while the page scrolls, lined with cards, framed pictures, rings with figures or a gate of light. The section stays, the camera travels, a closing text waits at the end. |
Pick WebGL when the picture is the stage for a message, behind a hero, a band of numbers or the call to action at the end. The 3D backgrounds take your content as children and lay it over the picture.
| Component | Variants | What it does |
|---|---|---|
| dot-sphere | split, horizon, card, cta | A turning sphere of evenly spaced dots, ray traced with WebGL. Lit front, faint back, a few dots glowing in the accent and a halo around it. Tilts with the pointer. |
| glow-orb | split, rise, card, cta | A soft, breathing orb of light, shaded like a sphere with a swirl inside and a wide glow, drawn with WebGL. Leans toward the pointer. |
| light-rays | hero, band, card, cta | Shafts of light falling from above with haze and floating dust, drawn with WebGL. They shimmer without repeating, the source follows the pointer a little. |
| particle-field | hero, band, card, cta | Glowing particles in up to six depth layers, drawn with WebGL. Near ones large and soft, far ones fine, the pointer spreads the layers and lights its surroundings. |
| shader-gradient | hero, band, card, cta | Flowing light in the accent color and its neighbours, drawn live with WebGL. Folds like silk, follows the pointer and the scroll, with a still picture as fallback. |
| wave-field | hero, band, card, cta | A wavy surface in perspective, drawn as lines or dots with WebGL. Glides toward the viewer, crests catch the accent, the pointer raises the waves under it. |
- 3D surfacesDevices, cards and stages in real CSS perspective.
- 3D scenesRooms that move while the page scrolls.
- 3D backgroundsLiving WebGL pictures behind your content.
Install#
Install every 3D component like any other. aiw add brings the helpers along as a dependency, depth for the CSS surfaces and webgl for the backgrounds. That is all it needs, both helpers are written in house and work without three.js.
aiw add @ai-website/device-3d @ai-website/shader-gradientimport { ShaderGradient } from '@/components/ui/shader-gradient';
import { Device3D } from '@/components/ui/device-3d';
<ShaderGradient scrim="left" scrimStrength={0.7} className="min-h-[560px]">
<div className="kit-container grid items-center gap-12 py-24 md:grid-cols-2">
<div>
<h1>Your account, in one hand</h1>
<p>Send, save and split, with one look at the whole month.</p>
</div>
<Device3D device="phone" angle="hero" width={280} label="Banking app on a phone">
<img src="/app-home.avif" alt="Home screen of the app" width={390} height={844} />
</Device3D>
</div>
</ShaderGradient>In Astro a 3D component gets a client directive so it runs. client:visible fits almost always, the motion starts on screen anyway.
<!-- Without a directive the still picture shows, finished and calm -->
<ShaderGradient scrim="center">…</ShaderGradient>
<!-- With a directive the shader starts once the section comes into view -->
<ShaderGradient client:visible scrim="center">…</ShaderGradient>The WebGL helper#
webgl is a slim helper without a library. One triangle covers the canvas, the fragment shader paints every pixel. The helper takes care of everything around it, sizing with a capped pixel ratio, starting on screen, pausing out of view and in hidden tabs, theme and accent changes, pointer and scroll, a lost and restored context and the clean up on unmount.
ShaderCanvas#
| Prop | Type | Default | What it does |
|---|---|---|---|
| fragment | string | · | The body of the fragment shader in GLSL ES 1.00. The helper puts the uniform header in front of it. |
| colors | string[] | [] | Up to four CSS colors, read in the scope of the canvas. Tokens, color-mix and oklch from work. Arrive as u_c0 to u_c3. |
| uniforms | Record<string, number | number[]> | · | Extra uniforms by name, declared in your fragment. |
| speed | number | 1 | Pace of u_time. 1 is the designed pace. |
| intensity | number | 1 | Passed on as u_intensity, 1 is the designed strength. |
| maxDpr | number | 1.5 | Highest pixel ratio on desktop. |
| phoneDpr | number | 1 | Highest pixel ratio on phones. |
| scale | number | 1 | Render scale for soft pictures. 0.5 draws a quarter of the pixels. |
| phoneFps | number | 30 | Frames per second on phones. Desktop runs at the rate of the display. |
| pointer | boolean | true | u_mouse and u_hover follow a fine pointer over the parent element. |
| still | number | 6 | Seconds of shader time for the still frame with reduced motion and for the first frame. |
| fallback | string | ReactNode | · | A CSS background or a node. Shown without JavaScript, without WebGL, until the first frame and after a lost context. |
| mask | string | · | A CSS mask over the whole layer, for example a soft edge. |
ShaderBackdrop#
| Prop | Type | Default | What it does |
|---|---|---|---|
| layer | ReactNode | · | The ShaderCanvas. |
| scrim | 'none' | 'soft' | 'center' | 'left' | 'bottom' | 'strong' | 'none' | Shade between picture and content, shaped like the text block it protects. |
| scrimStrength | number | 0.55 | How strongly the scrim covers, 0 to 1. With strong the default is 0.78. |
| scrimColor | string | var(--background) | Color of the scrim. Inside a card use var(--card). |
| children | ReactNode | · | Content above the picture. Without children only the layer renders and fills its relative parent. |
| className, contentClassName | string | · | Classes for the outer box and for the content box. |
Every background of the kit shares the same props intensity, speed, colors, interactive, fallback and the three scrim props. Every shader receives these uniforms by itself.
| Uniform | Type | What it does |
|---|---|---|
| u_res | vec2 | Size of the canvas in device pixels. |
| u_time | float | Seconds, counted only while the shader runs, times speed. |
| u_mouse, u_hover | vec2, float | Pointer in 0 to 1 with y up, eased, and how far a fine pointer is over the parent. |
| u_scroll | float | 0 when the top meets the bottom of the window, 1 when the bottom leaves at the top. |
| u_dark | float | 1 inside .dark, so one shader serves light and dark mode. |
| u_quality | float | 1 on desktop, 0.55 on phones. Use it for fewer layers, rows or particles. |
| u_intensity, u_dpr | float | The intensity prop and the device pixels per CSS pixel. |
| u_c0 to u_c3 | vec3 | The colors from the colors prop in sRGB 0 to 1. |
Helpers for CSS 3D#
The CSS surfaces and scenes share depth. Its helpers write CSS variables straight onto the element, so a surface moves without a React render.
| Helper | Returns | What it does |
|---|---|---|
| usePointerTilt() | ref, handlers | The element leans toward a mouse or pen. Writes --tilt-x, --tilt-y, --glare-x and --glare-y without a React render. |
| useActive() | ref, boolean | True while the element is on screen and the tab is visible. Idle motion runs only then. |
| Slab | Component | A flat body with real thickness, so a turned card or device shows an edge. |
| Glare, tiltTransform | Component, string | A soft light that follows the pointer, and the shared rotate expression. |
Text on 3D#
Text on a moving picture needs calm underneath. The scrim lays a shade in the color of the page between picture and content and follows the shape of your text block. Center suits a centered headline, left a hero with text on the left, bottom text along the lower edge and soft text across the whole area. Strong covers the most, for long paragraphs or forms.
- Check the contrast at the brightest moment of the picture, at least 4.5 to 1 for body text and 3 to 1 for large headlines.
- Inside a card the scrim takes the card color, so
scrimColor="var(--card)". - On phones the center and left scrims cover the whole area, because the text takes almost the full width there.
- Buttons and links on the picture keep their full area and focus ring, the shader never takes clicks.
Still picture and reduced motion#
Every 3D component has a still picture. For the backgrounds it is a CSS picture in the same colors, shown without JavaScript, without WebGL, until the first real frame and after a lost context, with a soft crossfade. Pass fallback for a still picture of your own.
With reduced motion the shader draws one calm frame and then rests. The CSS surfaces rest in their finished pose, scenes show every layer in order and readable. On touch devices nothing leans toward the finger, the surface keeps its resting pose.
Performance#
- The pixel ratio is capped, 1.5 on desktop and 1 on phones. For soft pictures scale halves the resolution once more.
- Phones draw 30 frames per second with u_quality at 0.55, so fewer layers and particles. Desktop runs at the rate of the display.
- A shader starts only on screen, pauses out of view and in hidden tabs and frees everything on unmount.
- CSS 3D moves only
transformandopacity. Pointer and scroll run through CSS variables, not through React state. - Every shader component is built for under 8 KB gzip and 60 frames per second on desktop. Add it with client:visible, then its code loads only shortly before it comes into view.
Your own shader#
For a background of your own you write only the fragment shader. GLSL_NOISE brings hash, noise and fbm, ACCENT_PALETTE the accent with two neighbours and a pale tint. Output premultiplied alpha, so the page shows through and one shader fits background, surface and card.
import { ShaderCanvas, ShaderBackdrop, GLSL_NOISE, ACCENT_PALETTE } from '@/components/ui/webgl';
const FRAG = `${GLSL_NOISE}
void main(){
vec2 p = gl_FragCoord.xy / u_res;
float n = fbm(p * 3. + vec2(u_time * .05, 0.));
vec3 c = mix(u_c0, u_c1, n);
float a = smoothstep(.2, .9, n) * .6 * u_intensity;
gl_FragColor = vec4(c * a, a);
}`;
export function Mist({ children }: { children: React.ReactNode }) {
return (
<ShaderBackdrop
scrim="center"
layer={<ShaderCanvas fragment={FRAG} colors={ACCENT_PALETTE} scale={0.5} fallback="radial-gradient(60% 60% at 50% 40%, color-mix(in oklab, var(--primary) 30%, transparent), transparent)" />}
>
{children}
</ShaderBackdrop>
);
}