bottega

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):

json
// components.json — register the @bottega namespace once
{
  "registries": {
    "@bottega": { "url": "https://bottega.ariacode.ca/r/{name}.json" }
  }
}

2. Add the component:

bash
npx shadcn add @bottega/floating-shapes

Usage

Usagetsx
<FloatingShapes />

Props

PropTypeDefaultDescription
countnumber24Total instanced shapes to render. Default 24. Keep <=60 for mobile safety.
shapesShapeKind[]["box", "sphere", "octahedron"]Which geometry types to include in the instance pool.
speednumber1Global drift speed multiplier. 1 = default (slow). Clamped 0-3.
depthnumber6Z-spread of the shape field in world units. Default 6.
colorstringvar(--primary)CSS color or var(). Applied to shape material base color. Default "var(--primary)".
opacitynumber0.18Base material opacity for all instances (0-1). Default 0.18 — keeps shapes ghostly.
repulsebooleantrueEnable pointer-proximity magnetic repulsion. Default true.
repulseRadiusnumber2.5World-space radius around pointer where repulsion activates. Default 2.5.
frameloopenumalwaysR3F Canvas frameloop. Default "always". "demand" pauses when nothing changes.
classNamestringForwarded to the outer wrapper for layout control.
minHeightstringMin-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

floating-shapes.tsxtsx
"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