Skip to content
6

Network Topology

Coordinate-placed topology diagram: nodes with status and icons over an SVG edge layer, with animated active links. Ships composable parts.

Preview

Installation

npx shadcn@latest add https://hirael.com/r/network-topology.json

Code

components/blocks/network-topology.tsx
'use client';

import * as React from 'react';
import { Database, Globe, HardDrive, Network, Server } from 'lucide-react';

import { cn } from '@/lib/utils';

type Point = [x: number, y: number];

export type EdgeStatus = 'active' | 'idle' | 'down';

const edgeStroke: Record<EdgeStatus, string> = {
  active: 'text-accent-cool',
  idle: 'text-border',
  down: 'text-destructive',
};

export type NodeStatus = 'online' | 'warning' | 'down' | 'idle';

const nodeDot: Record<NodeStatus, string> = {
  online: 'bg-success',
  warning: 'bg-warning',
  down: 'bg-destructive',
  idle: 'bg-muted-foreground',
};

// Keyframes travel with the component so the animated edges work the moment
// it is copied into a project — no globals.css or Tailwind config edits needed.
const TOPOLOGY_KEYFRAMES = `
@keyframes msh-network-dash {
  to { stroke-dashoffset: -14; }
}
`;

type NetworkTopologyProps = React.ComponentProps<'div'>;

const NetworkTopology = ({ className, children, ...props }: NetworkTopologyProps) => {
  return (
    <div
      data-slot="network-topology"
      dir="ltr"
      className={cn('relative w-full overflow-hidden rounded-lg border border-border bg-card', className)}
      {...props}
    >
      <style dangerouslySetInnerHTML={{ __html: TOPOLOGY_KEYFRAMES }} />
      {children}
    </div>
  );
};

type NetworkTopologyEdgesProps = React.ComponentProps<'svg'>;

const NetworkTopologyEdges = ({ className, ...props }: NetworkTopologyEdgesProps) => {
  return (
    <svg
      data-slot="network-topology-edges"
      viewBox="0 0 100 100"
      preserveAspectRatio="none"
      aria-hidden
      className={cn('absolute inset-0 size-full', className)}
      {...props}
    />
  );
};

interface NetworkEdgeProps extends Omit<React.ComponentProps<'line'>, 'x1' | 'y1' | 'x2' | 'y2' | 'from' | 'to'> {
  from: Point;
  to: Point;
  status?: EdgeStatus;
  animated?: boolean;
}

const NetworkEdge = ({ from, to, status = 'idle', animated, className, style, ...props }: NetworkEdgeProps) => {
  const isAnimated = animated && status !== 'down';
  return (
    <line
      data-slot="network-edge"
      data-status={status}
      x1={from[0]}
      y1={from[1]}
      x2={to[0]}
      y2={to[1]}
      stroke="currentColor"
      strokeWidth={1.5}
      strokeLinecap="round"
      vectorEffect="non-scaling-stroke"
      strokeDasharray={isAnimated ? '4 3' : undefined}
      style={
        isAnimated
          ? {
              animationName: 'msh-network-dash',
              animationDuration: '1s',
              animationTimingFunction: 'linear',
              animationIterationCount: 'infinite',
              ...style,
            }
          : style
      }
      className={cn(edgeStroke[status], isAnimated && 'motion-reduce:[animation-play-state:paused]', className)}
      {...props}
    />
  );
};

interface NetworkNodeProps extends React.ComponentProps<'div'> {
  x: number;
  y: number;
  status?: NodeStatus;
  icon?: React.ReactNode;
  label?: React.ReactNode;
  sublabel?: React.ReactNode;
  variant?: 'node' | 'hub';
}

const NetworkNode = ({
  x,
  y,
  status = 'online',
  icon,
  label,
  sublabel,
  variant = 'node',
  className,
  style,
  ...props
}: NetworkNodeProps) => {
  return (
    <div
      data-slot="network-node"
      data-status={status}
      className={cn(
        'absolute flex -translate-x-1/2 -translate-y-1/2 items-center gap-2 rounded-md border px-2.5 py-1.5 shadow-sm',
        variant === 'hub'
          ? 'border-primary/50 bg-primary/15 text-foreground'
          : 'border-border bg-card text-card-foreground',
        className,
      )}
      style={{ left: `${x}%`, top: `${y}%`, ...style }}
      {...props}
    >
      {icon ? <span className="flex size-4 items-center justify-center [&_svg]:size-4">{icon}</span> : null}
      {label || sublabel ? (
        <span className="flex min-w-0 flex-col leading-tight">
          {label ? <span className="truncate text-xs font-medium">{label}</span> : null}
          {sublabel ? <span className="truncate font-mono text-[10px] text-muted-foreground">{sublabel}</span> : null}
        </span>
      ) : null}
      <span className={cn('size-2 shrink-0 rounded-full ring-2 ring-card', nodeDot[status])} aria-hidden />
      <span className="sr-only">{status}</span>
    </div>
  );
};

export { NetworkTopology, NetworkTopologyEdges, NetworkEdge, NetworkNode };

const NetworkTopologyBlock = () => {
  return (
    <section data-slot="network-topology-block" className="flex w-full justify-center bg-background p-6 sm:p-10">
      <div className="w-full max-w-2xl">
        <NetworkTopology className="h-80">
          <NetworkTopologyEdges>
            <NetworkEdge from={[50, 12]} to={[50, 38]} status="active" animated />
            <NetworkEdge from={[50, 38]} to={[22, 66]} status="active" animated />
            <NetworkEdge from={[50, 38]} to={[50, 66]} status="active" animated />
            <NetworkEdge from={[50, 38]} to={[78, 66]} status="down" />
            <NetworkEdge from={[22, 66]} to={[35, 90]} status="active" />
            <NetworkEdge from={[50, 66]} to={[35, 90]} status="active" />
            <NetworkEdge from={[50, 66]} to={[65, 90]} status="active" />
            <NetworkEdge from={[78, 66]} to={[65, 90]} status="idle" />
          </NetworkTopologyEdges>

          <NetworkNode x={50} y={12} variant="hub" status="online" icon={<Globe />} label="Gateway" sublabel="edge" />
          <NetworkNode x={50} y={38} status="online" icon={<Network />} label="Load balancer" sublabel="lb-01" />
          <NetworkNode x={22} y={66} status="online" icon={<Server />} label="api-1" sublabel="10.0.1.4" />
          <NetworkNode x={50} y={66} status="warning" icon={<Server />} label="api-2" sublabel="10.0.1.5" />
          <NetworkNode x={78} y={66} status="down" icon={<Server />} label="api-3" sublabel="10.0.1.6" />
          <NetworkNode x={35} y={90} status="online" icon={<Database />} label="db" sublabel="primary" />
          <NetworkNode x={65} y={90} status="idle" icon={<HardDrive />} label="cache" sublabel="redis" />
        </NetworkTopology>
      </div>
    </section>
  );
};

export default NetworkTopologyBlock;

Dependencies

npm

lucide-react