CoreHigh-End Animated Cardsno dependencies

ConstellationCard

Interactive graphite card hosting a capped-budget spatial constellation network that links toward the cursor with static fallback behavior.

Preview

Constellation Network

Distributed Telemetry Mesh

Capped node network responds to pointer coordinates while maintaining sub-millisecond frame budgets.

Customize

4 props
24
88
0.4

Installation

Copies the component source and its CSS Module into your project.

npx motionfoundry add constellation-card

Writes components/ui/constellation-card.tsx and components/styles/constellation-card.module.css.

Usage

Updates live as you change the controls — only props that differ from the defaults are included.

example.tsx
1import { ConstellationCard } from "@motionfoundry/ui";2 3export function Example() {4  return (5    <ConstellationCard />6  );7}

Props

PropTypeDefault
title

Card title.

string'Distributed Telemetry Mesh'
description

Card description.

string'Capped node network...'
nodeCount

Number of constellation vertices (capped at 48).

number24
linkDistance

Maximum pixel threshold for connecting lines.

number88
nodeSpeed

Node drift velocity multiplier.

number0.4

Accessibility

Marks the interactive node canvas aria-hidden='true' while keeping card headings and copy accessible. Renders a static constellation when prefers-reduced-motion is active.

Toggle prefers-reduced-motion in your OS or browser devtools (Rendering → Emulate CSS media feature) to verify the reduced-motion state.

Source

components/ui/constellation-card.tsx
1"use client";2 3import * as React from "react";4import styles from "../styles/constellation-card.module.css";5 6export interface ConstellationCardProps {7  /** Card heading */8  title?: string;9  /** Card description */10  description?: string;11  /** Number of constellation vertices (capped at 48) */12  nodeCount?: number;13  /** Maximum distance in pixels for connecting lines */14  linkDistance?: number;15  /** Drift velocity multiplier */16  nodeSpeed?: number;17  /** Optional children */18  children?: React.ReactNode;19  /** Optional className */20  className?: string;21}22 23export function ConstellationCard({24  title = "Distributed Telemetry Mesh",25  description = "Capped node network responds to pointer coordinates while maintaining sub-millisecond frame budgets.",26  nodeCount = 24,27  linkDistance = 88,28  nodeSpeed = 0.4,29  children,30  className = "",31}: ConstellationCardProps) {32  const canvasRef = React.useRef<HTMLCanvasElement>(null);33  const pointerRef = React.useRef<{ x: number | null; y: number | null }>({ x: null, y: null });34 35  React.useEffect(() => {36    const canvas = canvasRef.current;37    if (!canvas) return;38    const ctx = canvas.getContext("2d");39    if (!ctx) return;40 41    const prefersReduced =42      typeof window !== "undefined" &&43      window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;44 45    let rafId = 0;46    const safeCount = Math.min(Math.max(nodeCount, 8), 48);47 48    const resize = () => {49      const parent = canvas.parentElement;50      if (!parent) return;51      canvas.width = parent.clientWidth;52      canvas.height = parent.clientHeight;53    };54    resize();55 56    const nodes = Array.from({ length: safeCount }, (_, i) => ({57      x: ((i * 67) % 100) * 0.01 * (canvas.width || 360),58      y: ((i * 43) % 100) * 0.01 * (canvas.height || 200),59      vx: ((((i * 31) % 9) - 4) || 1) * 0.14 * nodeSpeed,60      vy: ((((i * 59) % 9) - 4) || -1) * 0.14 * nodeSpeed,61    }));62 63    const draw = () => {64      ctx.clearRect(0, 0, canvas.width, canvas.height);65      for (let i = 0; i < nodes.length; i++) {66        const a = nodes[i];67        if (!prefersReduced) {68          a.x += a.vx;69          a.y += a.vy;70          if (a.x < 0 || a.x > canvas.width) a.vx *= -1;71          if (a.y < 0 || a.y > canvas.height) a.vy *= -1;72        }73        ctx.beginPath();74        ctx.arc(a.x, a.y, 1.5, 0, Math.PI * 2);75        ctx.fillStyle = "rgba(244, 244, 246, 0.65)";76        ctx.fill();77 78        for (let j = i + 1; j < nodes.length; j++) {79          const b = nodes[j];80          const dist = Math.hypot(a.x - b.x, a.y - b.y);81          if (dist < linkDistance) {82            ctx.beginPath();83            ctx.moveTo(a.x, a.y);84            ctx.lineTo(b.x, b.y);85            ctx.strokeStyle = `rgba(228, 228, 231, ${(1 - dist / linkDistance) * 0.2})`;86            ctx.lineWidth = 0.75;87            ctx.stroke();88          }89        }90 91        if (pointerRef.current.x !== null && pointerRef.current.y !== null) {92          const pDist = Math.hypot(a.x - pointerRef.current.x, a.y - pointerRef.current.y);93          if (pDist < linkDistance * 1.3) {94            ctx.beginPath();95            ctx.moveTo(a.x, a.y);96            ctx.lineTo(pointerRef.current.x, pointerRef.current.y);97            ctx.strokeStyle = `rgba(244, 244, 246, ${(1 - pDist / (linkDistance * 1.3)) * 0.42})`;98            ctx.stroke();99          }100        }101      }102 103      if (!prefersReduced) {104        rafId = window.requestAnimationFrame(draw);105      }106    };107 108    draw();109    window.addEventListener("resize", resize);110    return () => {111      window.removeEventListener("resize", resize);112      window.cancelAnimationFrame(rafId);113    };114  }, [nodeCount, linkDistance, nodeSpeed]);115 116  return (117    <div118      onPointerMove={(e) => {119        const rect = e.currentTarget.getBoundingClientRect();120        pointerRef.current = { x: e.clientX - rect.left, y: e.clientY - rect.top };121      }}122      onPointerLeave={() => {123        pointerRef.current = { x: null, y: null };124      }}125      className={`${styles.card} ${className}`.trim()}126    >127      <canvas ref={canvasRef} aria-hidden="true" className={styles.canvas} />128      <div className={styles.content}>129        <span className={styles.badge}>Constellation Network</span>130        <h3 className={styles.title}>{title}</h3>131        <p className={styles.description}>{description}</p>132        {children}133      </div>134    </div>135  );136}