three
Floating Shapes
An ambient field of drifting, self-rotating 3D primitives (boxes, spheres, octahedra, torii) rendered with @react-three/fiber. Each shape carries a seeded drift phase and depth-based opacity so the field never pulses in unison, and ~30% are tinted toward --border to break monotony — the whole field reads in the product palette (--primary) rather than a neutral gray. Optional pointer-proximity magnetic repulsion pushes nearby shapes gently away from the cursor and springs them back like a recovering water surface. Composed over canvas-wrapper, so it inherits WebGL feature-detect, lazy mount, dispose, and a reduced-motion / no-WebGL static poster. The <Canvas> scene is dynamic-imported ssr:false (R3F can't SSR) and reads color tokens off the canvas parent at runtime. Decorative: the canvas is aria-hidden and the wrapper is role='presentation' with pointer-events:none so it never steals interaction from page content above it.
Install
1. Register the namespace (once per project):
// components.json — register the @bottega namespace once
{
"registries": {
"@bottega": { "url": "https://bottega.ariacode.ca/r/{name}.json" }
}
}2. Add the component:
npx shadcn add @bottega/floating-shapesUsage
<FloatingShapes />Props
| Prop | Type | Default | Description |
|---|---|---|---|
| count | number | 24 | Total instanced shapes to render. Default 24. Keep <=60 for mobile safety. |
| shapes | ShapeKind[] | ["box", "sphere", "octahedron"] | Which geometry types to include in the instance pool. |
| speed | number | 1 | Global drift speed multiplier. 1 = default (slow). Clamped 0-3. |
| depth | number | 6 | Z-spread of the shape field in world units. Default 6. |
| color | string | var(--primary) | CSS color or var(). Applied to shape material base color. Default "var(--primary)". |
| opacity | number | 0.18 | Base material opacity for all instances (0-1). Default 0.18 — keeps shapes ghostly. |
| repulse | boolean | true | Enable pointer-proximity magnetic repulsion. Default true. |
| repulseRadius | number | 2.5 | World-space radius around pointer where repulsion activates. Default 2.5. |
| frameloop | enum | always | R3F Canvas frameloop. Default "always". "demand" pauses when nothing changes. |
| className | string | — | Forwarded to the outer wrapper for layout control. |
| minHeight | string | — | Min-height of the mount box, forwarded to canvas-wrapper. Default "20rem". Set "0" when the parent already constrains height so the field fills its container instead of being floored at 20rem. |
Source
"use client";
import dynamic from "next/dynamic";
import { CanvasWrapper } from "@/registry/three/canvas-wrapper/canvas-wrapper";
import { FloatingShapesPoster } from "./floating-shapes-poster";
export type ShapeKind = "box" | "sphere" | "octahedron" | "torus";
export interface FloatingShapesProps {
/** Total instanced shapes to render. Default 24. Keep <=60 for mobile safety. */
count?: number;
/** Which geometry types to include in the instance pool. */
shapes?: ShapeKind[];
/** Global drift speed multiplier. 1 = default (slow). Clamped 0-3. */
speed?: number;
/** Z-spread of the shape field in world units. Default 6. */
depth?: number;
/** CSS color or var(). Applied to shape material base color. Default "var(--primary)". */
color?: string;
/** Base material opacity for all instances (0-1). Default 0.18 — keeps shapes ghostly. */
opacity?: number;
/** Enable pointer-proximity magnetic repulsion. Default true. */
repulse?: boolean;
/** World-space radius around pointer where repulsion activates. Default 2.5. */
repulseRadius?: number;
/** R3F Canvas frameloop. Default "always". "demand" pauses when nothing changes. */
frameloop?: "always" | "demand";
/** Forwarded to the outer wrapper for layout control. */
className?: string;
/**
* Min-height of the mount box, forwarded to canvas-wrapper. Default "20rem".
* Set "0" when the parent already constrains height so the field fills its
* container instead of being floored at 20rem.
*/
minHeight?: string;
}
// R3F cannot SSR — load the <Canvas> scene client-side only. The poster is what
// shows while it loads (and on every guarded path: no-WebGL / reduced-motion /
// offscreen). canvas-wrapper owns all three guards.
const FloatingShapesScene = dynamic(() => import("./floating-shapes-scene"), {
ssr: false,
loading: () => <FloatingShapesPoster />,
});
/**
* A field of instanced 3D primitives (cubes, spheres, octahedra, torii) drifting
* and self-rotating in a low-depth ambient scene, themed entirely from the
* contract CSS variables (--primary / --border) so the field reads as part of
* the product palette rather than a neutral gray soup. Shapes respond to pointer
* proximity with a gentle magnetic repulsion that springs back like a water
* surface recovering.
*
* Composed over canvas-wrapper, so it inherits the full §7 3D contract: WebGL
* feature-detect, lazy mount, dispose, and a reduced-motion / no-WebGL poster
* fallback. The scene is dynamic-imported ssr:false (R3F can't SSR). Decorative
* by design: the canvas is aria-hidden and the wrapper is role="presentation".
*/
export function FloatingShapes({
count = 24,
shapes = ["box", "sphere", "octahedron"],
speed = 1,
depth = 6,
color = "var(--primary)",
opacity = 0.18,
repulse = true,
repulseRadius = 2.5,
frameloop = "always",
className,
minHeight,
}: FloatingShapesProps) {
return (
<CanvasWrapper
className={className}
aspect="16 / 9"
minHeight={minHeight}
poster={<FloatingShapesPoster shapes={shapes} />}
>
<FloatingShapesScene
count={count}
shapes={shapes}
speed={speed}
depth={depth}
color={color}
opacity={opacity}
repulse={repulse}
repulseRadius={repulseRadius}
frameloop={frameloop}
/>
</CanvasWrapper>
);
}
export default FloatingShapes;
Dependencies
- three
- @react-three/fiber
- @bottega/canvas-wrapper
- @bottega/tokens