Skip to content
UI Kit
Accent color

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.

ComponentVariantsWhat it does
card-stack-3dfan, deck, cascade, ring, drumCards 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-3dphone, laptop, tablet, browser, anglesA 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-cardteam, facts, product, stats, giftCards 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-cardfeatures, product, profile, pricing, ticketCards 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-layersinterface, scroll, diorama, inspectorA 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-stagedashboard, stack, page, workflowA 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.
ComponentVariantsWhat it does
depth-textextrude, layers, rise, echo, crawlType 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-flightchapters, gallery, timeline, wordsA 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-panelsfeatures, shelf, orbit, splitPanels 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-floorgrid, stage, bars, horizonA 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-showcasephone, laptop, cube, cardAn 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-zoomcards, frames, rings, portalA 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.

ComponentVariantsWhat it does
dot-spheresplit, horizon, card, ctaA 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-orbsplit, rise, card, ctaA 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-rayshero, band, card, ctaShafts 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-fieldhero, band, card, ctaGlowing 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-gradienthero, band, card, ctaFlowing 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-fieldhero, band, card, ctaA 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.

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-gradient
ShaderGradient, variant heroView in Components
import { 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.

src/pages/index.astro
<!-- 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#

PropTypeDefaultWhat it does
fragmentstring·The body of the fragment shader in GLSL ES 1.00. The helper puts the uniform header in front of it.
colorsstring[][]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.
uniformsRecord<string, number | number[]>·Extra uniforms by name, declared in your fragment.
speednumber1Pace of u_time. 1 is the designed pace.
intensitynumber1Passed on as u_intensity, 1 is the designed strength.
maxDprnumber1.5Highest pixel ratio on desktop.
phoneDprnumber1Highest pixel ratio on phones.
scalenumber1Render scale for soft pictures. 0.5 draws a quarter of the pixels.
phoneFpsnumber30Frames per second on phones. Desktop runs at the rate of the display.
pointerbooleantrueu_mouse and u_hover follow a fine pointer over the parent element.
stillnumber6Seconds of shader time for the still frame with reduced motion and for the first frame.
fallbackstring | ReactNode·A CSS background or a node. Shown without JavaScript, without WebGL, until the first frame and after a lost context.
maskstring·A CSS mask over the whole layer, for example a soft edge.

ShaderBackdrop#

PropTypeDefaultWhat it does
layerReactNode·The ShaderCanvas.
scrim'none' | 'soft' | 'center' | 'left' | 'bottom' | 'strong''none'Shade between picture and content, shaped like the text block it protects.
scrimStrengthnumber0.55How strongly the scrim covers, 0 to 1. With strong the default is 0.78.
scrimColorstringvar(--background)Color of the scrim. Inside a card use var(--card).
childrenReactNode·Content above the picture. Without children only the layer renders and fills its relative parent.
className, contentClassNamestring·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.

UniformTypeWhat it does
u_resvec2Size of the canvas in device pixels.
u_timefloatSeconds, counted only while the shader runs, times speed.
u_mouse, u_hovervec2, floatPointer in 0 to 1 with y up, eased, and how far a fine pointer is over the parent.
u_scrollfloat0 when the top meets the bottom of the window, 1 when the bottom leaves at the top.
u_darkfloat1 inside .dark, so one shader serves light and dark mode.
u_qualityfloat1 on desktop, 0.55 on phones. Use it for fewer layers, rows or particles.
u_intensity, u_dprfloatThe intensity prop and the device pixels per CSS pixel.
u_c0 to u_c3vec3The 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.

HelperReturnsWhat it does
usePointerTilt()ref, handlersThe element leans toward a mouse or pen. Writes --tilt-x, --tilt-y, --glare-x and --glare-y without a React render.
useActive()ref, booleanTrue while the element is on screen and the tab is visible. Idle motion runs only then.
SlabComponentA flat body with real thickness, so a turned card or device shows an edge.
Glare, tiltTransformComponent, stringA 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 transform and opacity. 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.

src/components/Mist.tsx
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>
  );
}

Often searched

Jump to

Copied to the clipboard