litenBeta

Neural Network

A layered network diagram with a forward-pass pulse travelling along its strongest connections, firing each layer of nodes as the signal arrives.

Neural Network renders a fully connected, layered network as a single SVG: faint resting wires with seeded weights, an accent pulse sweeping the strongest connections layer by layer, and top-lit nodes whose cores fire as the signal reaches them. Layout and weights are seeded, so server and client render the same network. Built with Motion.

Installation

Complete the shared Setup first, then add Motion.

Terminal
bun add motion

Copy the component into components/ui/neural-network.tsx.

components/ui/neural-network.tsx
'use client';import * as React from 'react';import { motion, useReducedMotion } from 'motion/react';import { cn } from '@/lib/cn';const EASE_OUT = [0.23, 1, 0.32, 1] as const;export type NeuralNetworkProps = {  layers?: number[];  inputs?: React.ReactNode[];  outputs?: React.ReactNode[];  accent?: string;  duration?: number;  density?: number;  nodeRadius?: number;  labels?: boolean;  className?: string;};const W = 560;const H = 300;const TRAVEL = 0.82;function seeded(n: number): number {  const x = Math.sin(n * 12.9898) * 43758.5453;  return x - Math.floor(x);}type Edge = {  d: string;  weight: number;  active: boolean;};export function NeuralNetwork({  layers = [4, 6, 6, 3],  inputs,  outputs,  accent = '#f0883e',  duration = 3.6,  density = 0.3,  nodeRadius = 7,  labels = false,  className,}: NeuralNetworkProps) {  const reduce = useReducedMotion();  const id = React.useId().replace(/[:]/g, '');  const resolved = React.useMemo(() => {    const out = [...layers];    if (inputs?.length) out[0] = inputs.length;    if (outputs?.length) out[out.length - 1] = outputs.length;    return out;  }, [layers, inputs, outputs]);  const gaps = resolved.length - 1;  const positions = React.useMemo(() => {    const padX = 56;    const top = 40;    const bottom = labels ? 62 : 40;    const maxN = Math.max(...resolved);    const rowGap = maxN === 1 ? 0 : (H - top - bottom) / (maxN - 1);    const centerY = top + (H - top - bottom) / 2;    return resolved.map((n, i) => {      const x =        resolved.length === 1          ? W / 2          : padX + (i * (W - padX * 2)) / (resolved.length - 1);      return Array.from({ length: n }, (_, j) => ({        x,        y: centerY + (j - (n - 1) / 2) * rowGap,      }));    });  }, [resolved, labels]);  const edges = React.useMemo(() => {    const out: Edge[][] = [];    for (let g = 0; g < gaps; g++) {      const layer: Edge[] = [];      positions[g].forEach((from, a) => {        positions[g + 1].forEach((to, b) => {          const weight = seeded(g * 97.13 + a * 13.7 + b * 3.1 + 1);          const mx = (from.x + to.x) / 2;          layer.push({            d: `M ${from.x} ${from.y} C ${mx} ${from.y}, ${mx} ${to.y}, ${to.x} ${to.y}`,            weight,            active: weight >= 1 - density,          });        });      });      out.push(layer);    }    return out;  }, [positions, gaps, density]);  const coreKeyframes = (layer: number, peak: number) => {    const base = 0.15;    if (gaps < 1) return { opacity: [base, peak, base], times: [0, 0.05, 0.4] };    if (layer === 0)      return { opacity: [base, peak, base, base], times: [0, 0.04, 0.26, 1] };    const t = (layer * TRAVEL) / gaps;    const rise = Math.min(t + 0.04, 0.99);    const fall = Math.min(t + 0.26, 0.995);    return {      opacity: [base, base, peak, base, base],      times: [0, t, rise, fall, 1],    };  };  const chipLayer = (l: number) =>    (l === 0 && !!inputs?.length) ||    (l === resolved.length - 1 && !!outputs?.length);  const renderChips = (l: number, items: React.ReactNode[]) =>    items.map((item, n) => {      const node = positions[l][n];      const frames = coreKeyframes(l, 0.9);      return (        <div          key={`c-${l}-${n}`}          className="absolute -translate-x-1/2 -translate-y-1/2"          style={{            left: `${(node.x / W) * 100}%`,            top: `${(node.y / H) * 100}%`,          }}        >          {!reduce && (            <motion.div              aria-hidden              className="absolute -inset-[3px] rounded-[13px] border"              style={{ borderColor: accent }}              initial={{ opacity: 0 }}              animate={{ opacity: frames.opacity.map((o) => (o <= 0.15 ? 0 : o)) }}              transition={{                duration,                times: frames.times,                repeat: Infinity,                ease: 'linear',              }}            />          )}          <div            className={cn(              'relative grid size-10 place-items-center rounded-[10px]',              'border border-black/[0.09] bg-white text-neutral-800',              'dark:border-white/[0.08] dark:bg-[#232320] dark:text-neutral-100',              'shadow-[0_1px_1.5px_0_rgba(27,27,25,0.05)] dark:shadow-[0_1px_1.5px_0_rgba(0,0,0,0.4)]',            )}          >            {item}          </div>        </div>      );    });  return (    <div className={cn('relative w-full', className)}>      <svg        role="img"        aria-label={`Neural network with ${resolved.length} layers of ${resolved.join(', ')} nodes`}        viewBox={`0 0 ${W} ${H}`}        fill="none"        className="block h-auto w-full text-neutral-900 dark:text-white"      >        <motion.g          initial={{ opacity: 0 }}          animate={{ opacity: 1 }}          transition={{ duration: 0.5, ease: EASE_OUT }}        >          {edges.map((layer, g) =>            layer.map((edge, e) => (              <path                key={`w-${g}-${e}`}                d={edge.d}                stroke="currentColor"                strokeOpacity={0.06 + edge.weight * 0.09}                strokeWidth={1.1}                strokeLinecap="round"              />            )),          )}          {edges.map((layer, g) =>            layer.map(              (edge, e) =>                edge.active && (                  <path                    key={`p-${g}-${e}`}                    d={edge.d}                    stroke={reduce ? accent : `url(#nn-${id}-${g})`}                    strokeOpacity={reduce ? 0.45 : 1}                    strokeWidth={1.5}                    strokeLinecap="round"                  />                ),            ),          )}        </motion.g>        {positions.map((layer, l) => chipLayer(l) ? null : (          <motion.g            key={`l-${l}`}            initial={{ opacity: 0 }}            animate={{ opacity: 1 }}            transition={{              duration: 0.4,              ease: EASE_OUT,              delay: reduce ? 0 : l * 0.06,            }}          >            {layer.map((node, n) => {              const act = seeded(l * 51.7 + n * 7.3 + 2);              const peak = 0.55 + act * 0.45;              const frames = coreKeyframes(l, peak);              return (                <g key={n}>                  <circle                    cx={node.x}                    cy={node.y + 1.2}                    r={nodeRadius}                    className="fill-black/[0.10] dark:fill-black/50"                  />                  <circle                    cx={node.x}                    cy={node.y}                    r={nodeRadius}                    strokeWidth={1}                    className="fill-white stroke-black/[0.10] dark:fill-[#1c1c1c] dark:stroke-white/[0.12]"                  />                  <path                    d={`M ${node.x - nodeRadius * 0.707} ${node.y - nodeRadius * 0.707} A ${nodeRadius} ${nodeRadius} 0 0 1 ${node.x + nodeRadius * 0.707} ${node.y - nodeRadius * 0.707}`}                    strokeWidth={1}                    strokeLinecap="round"                    className="stroke-white dark:stroke-white/25"                  />                  {reduce ? (                    <circle                      cx={node.x}                      cy={node.y}                      r={nodeRadius * 0.42}                      fill={accent}                      opacity={0.25 + act * 0.5}                    />                  ) : (                    <motion.circle                      cx={node.x}                      cy={node.y}                      r={nodeRadius * 0.42}                      fill={accent}                      initial={{ opacity: 0.15 }}                      animate={{ opacity: frames.opacity }}                      transition={{                        duration,                        times: frames.times,                        repeat: Infinity,                        ease: 'linear',                      }}                    />                  )}                </g>              );            })}          </motion.g>        ))}        {labels &&          positions.map((layer, l) => (            <text              key={`t-${l}`}              x={layer[0].x}              y={H - 18}              textAnchor="middle"              fontSize={9.5}              letterSpacing="0.12em"              className="fill-neutral-400 uppercase dark:fill-[#8b8b8b]"            >              {l === 0 ? 'Input' : l === resolved.length - 1 ? 'Output' : 'Hidden'}            </text>          ))}        {!reduce && (          <defs>            {edges.map((_, g) => {              const xA = positions[g][0].x;              const xB = positions[g + 1][0].x;              const span = (xB - xA) * 0.5;              const t0 = (g * TRAVEL) / gaps;              const t1 = ((g + 1) * TRAVEL) / gaps;              return (                <motion.linearGradient                  key={`g-${g}`}                  id={`nn-${id}-${g}`}                  gradientUnits="userSpaceOnUse"                  y1="0"                  y2="0"                  initial={{ x1: xA - span, x2: xA }}                  animate={{                    x1: [xA - span, xA - span, xB, xB],                    x2: [xA, xA, xB + span, xB + span],                  }}                  transition={{                    duration,                    times: [0, t0, t1, 1],                    repeat: Infinity,                    ease: 'linear',                  }}                >                  <stop stopColor={accent} stopOpacity="0" />                  <stop offset="0.5" stopColor={accent} />                  <stop offset="1" stopColor={accent} stopOpacity="0" />                </motion.linearGradient>              );            })}          </defs>        )}      </svg>      {inputs?.length ? renderChips(0, inputs) : null}      {outputs?.length ? renderChips(resolved.length - 1, outputs) : null}    </div>  );}

Usage

Example.tsx
import { NeuralNetwork } from '@/components/ui/neural-network';

export default function Example() {
  return <NeuralNetwork />;
}

Examples

Pass inputs and outputs to swap the edge layers for icon chips and turn the diagram into a real pipeline: sources flow in, the model fires, results come out.

Shape the architecture with layers, raise density to light more connections, and turn on labels for the input / hidden / output captions.

InputHiddenHiddenHiddenOutput

Props

PropTypeDefaultDescription
layersnumber[][4, 6, 6, 3]Node count per layer, left to right.
inputsReactNode[]-Icons rendered as chips in place of the input layer.
outputsReactNode[]-Icons rendered as chips in place of the output layer.
accentstring"#f0883e"Color of the pulse and the firing node cores.
durationnumber3.6Seconds for one full forward pass.
densitynumber0.3Fraction of connections that carry the pulse (0 to 1).
nodeRadiusnumber7Radius of each node in viewBox units.
labelsbooleanfalseShow input / hidden / output captions under the columns.
classNamestring-Extra classes for the wrapper.
On this page0%