# Cluster Map

> Grid heatmap of cluster nodes, each cell tinted by health and load with a hover label, plus a legend. Ships composable parts.

A shadcn/ui block from the [@hirael](https://hirael.com) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/hirael/cluster-map).

- Page, with a live preview: https://shadcn.exchange/r/hirael/cluster-map
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/hirael/cluster-map-light-000cf8eb35851c4c.png
- Registry item: https://hirael.com/r/cluster-map.json
- Categories: blocks, cloud
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://hirael.com/r/cluster-map.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Files

### `registry/hirael/bases/radix/blocks/cluster-map/cluster-map.tsx`

```tsx
// Cluster Map from Hirael <https://hirael.com/blocks/cloud/cluster-map>
// MIT · Mohammad Shehadeh · https://github.com/MohammadShehadeh/hirael

'use client';

import * as React from 'react';

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

export type NodeHealth = 'healthy' | 'warning' | 'critical' | 'idle';

const healthFill: Record<NodeHealth, string> = {
  healthy: 'bg-success',
  warning: 'bg-warning',
  critical: 'bg-destructive',
  idle: 'bg-muted-foreground',
};

const healthLabel: Record<NodeHealth, string> = {
  healthy: 'Healthy',
  warning: 'Warning',
  critical: 'Critical',
  idle: 'Idle',
};

interface ClusterMapProps extends React.ComponentProps<'div'> {
  columns?: number;
}

const ClusterMap = ({ columns = 12, className, style, ...props }: ClusterMapProps) => {
  return (
    <div
      data-slot="cluster-map"
      className={cn('grid gap-1.5', className)}
      style={{
        gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))`,
        ...style,
      }}
      {...props}
    />
  );
};

interface ClusterNodeProps extends Omit<React.ComponentProps<'button'>, 'children'> {
  health: NodeHealth;
  label?: React.ReactNode;
  /** Utilization from 0 to 1, drives the fill intensity. */
  load?: number;
  active?: boolean;
}

const ClusterNode = ({ health, label, load = 1, active, className, title, ...props }: ClusterNodeProps) => {
  const opacity = 0.25 + Math.max(0, Math.min(1, load)) * 0.75;

  return (
    <button
      type="button"
      data-slot="cluster-node"
      data-health={health}
      title={title ?? (label ? String(label) : healthLabel[health])}
      aria-label={label ? `${label}, ${healthLabel[health]}` : healthLabel[health]}
      aria-pressed={active === undefined ? undefined : active}
      data-active={active ? '' : undefined}
      className={cn(
        'group relative aspect-square rounded-sm transition-transform outline-none',
        'hover:z-10 hover:scale-110 focus-visible:z-10 focus-visible:ring-2 focus-visible:ring-ring',
        active && 'z-10 scale-110 ring-2 ring-foreground ring-offset-1 ring-offset-background',
        className,
      )}
      {...props}
    >
      <span className={cn('absolute inset-0 rounded-sm', healthFill[health])} style={{ opacity }} aria-hidden />
    </button>
  );
};

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

const ClusterMapLegend = ({ className, ...props }: ClusterMapLegendProps) => {
  return (
    <div
      data-slot="cluster-map-legend"
      className={cn('flex flex-wrap items-center gap-x-4 gap-y-1.5 text-xs text-muted-foreground', className)}
      {...props}
    />
  );
};

interface ClusterMapLegendItemProps extends React.ComponentProps<'span'> {
  health: NodeHealth;
}

const ClusterMapLegendItem = ({ health, className, children, ...props }: ClusterMapLegendItemProps) => {
  return (
    <span data-slot="cluster-map-legend-item" className={cn('inline-flex items-center gap-1.5', className)} {...props}>
      <span className={cn('size-2.5 rounded-sm', healthFill[health])} aria-hidden />
      {children ?? healthLabel[health]}
    </span>
  );
};

export { ClusterMap, ClusterNode, ClusterMapLegend, ClusterMapLegendItem };

const ENTER =
  'animate-in fade-in slide-in-from-bottom-2 duration-500 ease-[cubic-bezier(0.22,1,0.36,1)] fill-mode-both motion-reduce:animate-none';
const SWAP =
  'animate-in fade-in slide-in-from-bottom-1 duration-250 ease-[cubic-bezier(0.22,1,0.36,1)] fill-mode-both motion-reduce:animate-none';

const CLUSTER_COLUMNS = 16;
const CLUSTER_ROWS = 6;

const clusterHealthFor = (i: number): NodeHealth => {
  if (i % 37 === 0) return 'critical';
  if (i % 11 === 0) return 'warning';
  if (i % 9 === 0) return 'idle';

  return 'healthy';
};

const CLUSTER_NODES = Array.from({ length: CLUSTER_COLUMNS * CLUSTER_ROWS }, (_, i) => {
  const health = clusterHealthFor(i);

  return {
    id: i,
    name: `node-${String(i + 1).padStart(3, '0')}`,
    zone: ['eu-west-1a', 'eu-west-1b', 'eu-west-1c'][i % 3],
    health,
    load: health === 'idle' ? 0.08 : 0.4 + ((i * 7) % 60) / 100,
    pods: health === 'idle' ? 0 : 12 + ((i * 5) % 24),
  };
});

const healthTone: Record<NodeHealth, string> = {
  healthy: 'text-success',
  warning: 'text-warning',
  critical: 'text-destructive',
  idle: 'text-muted-foreground',
};

const ClusterMapBlock = () => {
  const [selectedId, setSelectedId] = React.useState<number | null>(37);
  const selected = selectedId === null ? null : CLUSTER_NODES[selectedId];
  const counts = CLUSTER_NODES.reduce<Record<NodeHealth, number>>(
    (acc, node) => ({ ...acc, [node.health]: acc[node.health] + 1 }),
    { healthy: 0, warning: 0, critical: 0, idle: 0 },
  );

  return (
    <section data-slot="cluster-map-block" className="flex w-full justify-center bg-background p-6 sm:p-10">
      <div className={cn(ENTER, 'flex w-full max-w-2xl flex-col gap-4')}>
        <div className="flex flex-col gap-2">
          <p className="flex items-center gap-2 text-xs text-muted-foreground uppercase">
            <span>prod-cluster</span>
            <span aria-hidden className="text-border">
              |
            </span>
            <span className="tabular-nums">{CLUSTER_NODES.length} nodes</span>
          </p>
          <ClusterMap columns={CLUSTER_COLUMNS}>
            {CLUSTER_NODES.map((node) => (
              <ClusterNode
                key={node.id}
                health={node.health}
                load={node.load}
                label={node.name}
                active={node.id === selectedId}
                onClick={() => setSelectedId((current) => (current === node.id ? null : node.id))}
              />
            ))}
          </ClusterMap>
        </div>

        <div
          data-slot="cluster-map-details"
          aria-live="polite"
          className="min-h-14 rounded-md border border-border bg-card/40 px-4 py-3"
        >
          {selected ? (
            <dl key={selected.id} className={cn(SWAP, 'grid grid-cols-2 gap-x-6 gap-y-2 text-sm sm:grid-cols-4')}>
              <div className="flex flex-col gap-0.5">
                <dt className="text-xs text-muted-foreground">Node</dt>
                <dd className="font-medium">{selected.name}</dd>
              </div>
              <div className="flex flex-col gap-0.5">
                <dt className="text-xs text-muted-foreground">Health</dt>
                <dd className={cn('flex items-center gap-1.5', healthTone[selected.health])}>
                  <span aria-hidden className={cn('size-2 rounded-sm', healthFill[selected.health])} />
                  {healthLabel[selected.health]}
                </dd>
              </div>
              <div className="flex flex-col gap-0.5">
                <dt className="text-xs text-muted-foreground">CPU load</dt>
                <dd className="tabular-nums">{Math.round(selected.load * 100)}%</dd>
              </div>
              <div className="flex flex-col gap-0.5">
                <dt className="text-xs text-muted-foreground">Pods</dt>
                <dd className="tabular-nums">
                  {selected.pods}
                  <span className="ms-1.5 text-xs text-muted-foreground">{selected.zone}</span>
                </dd>
              </div>
            </dl>
          ) : (
            <p key="empty" className={cn(SWAP, 'py-2 text-sm text-muted-foreground')}>
              Select a node to see its health and load.
            </p>
          )}
        </div>

        <ClusterMapLegend>
          {(Object.keys(healthLabel) as NodeHealth[]).map((health) => (
            <ClusterMapLegendItem key={health} health={health}>
              {healthLabel[health]}
              <span className="text-foreground tabular-nums">{counts[health]}</span>
            </ClusterMapLegendItem>
          ))}
        </ClusterMapLegend>
      </div>
    </section>
  );
};

export default ClusterMapBlock;
```
