Infinite Plane

Tiled pannable backdrop for the canvas with dot or grid pattern that drifts with the viewport.

Report a bug

Preview

Switch between light and dark to inspect the embedded Storybook preview.

Installation

pnpm dlx shadcn@latest add https://ui.vllnt.ai/r/infinite-plane.json

Storybook

Explore all variants, controls, and accessibility checks in the interactive Storybook playground.

View in Storybook

4 stories available:

Code

"use client"; import { type ComponentPropsWithoutRef, forwardRef, type ReactNode, } from "react"; import { cn } from "../../lib/utils"; /** * Pattern style for the plane backdrop. * * @public */ export type InfinitePlanePattern = "blank" | "dot" | "grid"; /** * Localizable strings. * * @public */ export type InfinitePlaneLabels = { /** Aria-label override. Defaults to `"Infinite plane"`. */ region?: string; }; const DEFAULT_LABELS = { region: "Infinite plane", } as const satisfies Required<InfinitePlaneLabels>; /** * Props for {@link InfinitePlane}. * * @public */ export type InfinitePlaneProps = { /** Children render in the plane's coordinate space. */ children?: ReactNode; /** Localizable strings. */ labels?: InfinitePlaneLabels; /** Backdrop pattern. Defaults to `"dot"`. */ pattern?: InfinitePlanePattern; /** Pattern grid spacing in pixels. Defaults to `32`. */ spacing?: number; /** Optional offset applied to the pattern (drift with viewport translation). Defaults to `{ x: 0, y: 0 }`. */ translate?: { x: number; y: number }; /** Zoom factor — drives the pattern's effective spacing. Defaults to `1`. */ zoom?: number; } & ComponentPropsWithoutRef<"div">; const safeSpacing = (value: number): number => (value < 4 ? 4 : value); const safeZoom = (value: number): number => { if (value < 0.1) { return 0.1; } if (value > 10) { return 10; } return value; }; const buildBackground = (input: { pattern: InfinitePlanePattern; spacing: number; translate: { x: number; y: number }; zoom: number; }): React.CSSProperties => { if (input.pattern === "blank") { return {}; } const size = safeSpacing(input.spacing) * safeZoom(input.zoom); const pos = `${input.translate.x}px ${input.translate.y}px`; if (input.pattern === "grid") { return { backgroundImage: "linear-gradient(to right, oklch(var(--border)) 1px, transparent 1px), linear-gradient(to bottom, oklch(var(--border)) 1px, transparent 1px)", backgroundPosition: pos, backgroundSize: `${size}px ${size}px`, }; } return { backgroundImage: "radial-gradient(circle, oklch(var(--border)) 1px, transparent 1px)", backgroundPosition: pos, backgroundSize: `${size}px ${size}px`, }; }; /** * Tiled pannable backdrop for the canvas. Renders a `dot` or `grid` * pattern that drifts with the viewport translate + scales with the * zoom, plus a slot for spatial children that share its coordinate * space. * * Pure presentation; the host owns the viewport transform and supplies * `translate` + `zoom` from its pan / zoom controller. * * @example * ```tsx * <InfinitePlane translate={{ x: pan.x, y: pan.y }} zoom={zoom}> * <ObjectCard …/> * <ObjectCard …/> * </InfinitePlane> * ``` * * @public */ export const InfinitePlane = forwardRef<HTMLDivElement, InfinitePlaneProps>( (props, ref) => { const { children, className, labels, pattern = "dot", spacing = 32, translate = { x: 0, y: 0 }, zoom = 1, ...rest } = props; const resolvedLabels = { ...DEFAULT_LABELS, ...labels }; const background = buildBackground({ pattern, spacing, translate, zoom }); return ( <div aria-label={resolvedLabels.region} className={cn( "relative h-full w-full overflow-hidden bg-background", className, )} data-infinite-plane data-infinite-plane-pattern={pattern} ref={ref} role="region" style={background} {...rest} > {children} </div> ); }, ); InfinitePlane.displayName = "InfinitePlane";

Dependencies

  • @vllnt/ui@^0.2.1