All components

Switchboard Card

Switchboard Card is a React and Tailwind CSS card component from SmoothUI, licensed MIT. Copy it and paste it into your project.

SmoothUI MIT cards

What it is

Switchboard Card is a React and Tailwind CSS card component from SmoothUI, licensed MIT. Copy it and paste it into your project.

How to use it

Open it in the catalogue, press copy, and paste it into your project. The prompt you copy carries the code and the rules that tell your agent to reproduce it without changing anything.

Open in the catalogue

Five free copies a month. No card.

No packages beyond the project baseline

Licence

This component comes from SmoothUI and is redistributed under the MIT licence, which permits commercial use. The copyright notice travels with the code.

Switchboard Card

index.tsx
"use client";

import { cn } from "@/lib/utils";
import { useEffect, useMemo, useState } from "react";

export type LightState = "off" | "medium" | "high";

export interface SwitchboardCardProps {
  className?: string;
  columns?: number;
  gridPattern?: number[] | number[][]; // Direct grid pattern: flat array (columns*rows) or 2D array [rows][columns] where 0=off, 1=high
  href?: string;
  onButtonClick?: () => void;
  randomLights?: boolean; // If true, randomly activate some lights
  rows?: number;
  showButton?: boolean;
  subtitle: string;
  title: string;
  transitionDuration?: number; // in milliseconds (default: 200ms for smooth light transitions)
  variant?: "default" | "next";
}

function generateRandomPattern(
  totalLights: number,
  activeRatio = 0.15
): number[] {
  const activeCount = Math.floor(totalLights * activeRatio);
  const pattern: number[] = [];
  const used = new Set<number>();

  while (pattern.length < activeCount) {
    const index = Math.floor(Math.random() * totalLights);
    if (!used.has(index)) {
      used.add(index);
      pattern.push(index);
    }
  }

  return pattern.sort((a, b) => a - b);
}

function parse2DGridPattern(
  grid2D: number[][],
  rows: number,
  columns: number,
  totalLights: number
): number[] {
  const pattern: number[] = [];
  for (let row = 0; row < Math.min(grid2D.length, rows); row++) {
    const rowData = grid2D[row];
    if (Array.isArray(rowData)) {
      for (let col = 0; col < Math.min(rowData.length, columns); col++) {
        if (rowData[col] === 1) {
          const index = col + row * columns;
          if (index >= 0 && index < totalLights) {
            pattern.push(index);
          }
        }
      }
    }
  }
  return pattern;
}

function parseFlatGridPattern(
  flatPattern: number[],
  totalLights: number
): number[] {
  const pattern: number[] = [];
  for (let i = 0; i < Math.min(flatPattern.length, totalLights); i++) {
    if (flatPattern[i] === 1) {
      pattern.push(i);
    }
  }
  return pattern;
}

export default function SwitchboardCard({
  title,
  subtitle,
  columns = 18,
  rows = 5,
  gridPattern,
  randomLights = false,
  transitionDuration = 200,
  className,
  variant = "default",
  showButton: _showButton = true,
  href,
  onButtonClick,
}: SwitchboardCardProps) {
  const totalLights = columns * rows;
  const isRandomLightsMode = randomLights && !gridPattern;

  // Convert gridPattern to light indices
  // Priority: gridPattern > randomLights
  const basePattern = useMemo<number[]>(() => {
    if (gridPattern) {
      // Handle 2D array [rows][columns]
      const is2DArray =
        Array.isArray(gridPattern[0]) && typeof gridPattern[0] !== "number";
      if (is2DArray) {
        return parse2DGridPattern(
          gridPattern as number[][],
          rows,
          columns,
          totalLights
        );
      }
      // Handle flat array [columns*rows]
      return parseFlatGridPattern(gridPattern as number[], totalLights);
    }
    if (randomLights) {
      return generateRandomPattern(totalLights);
    }
    return [];
  }, [gridPattern, randomLights, columns, rows, totalLights]);

  // Animated light states
  // For random lights: all start off, will animate randomly through medium → high → medium → off
  // For word/pattern: pattern lights start high (stay high), rest off
  const [lightStates, setLightStates] = useState<LightState[]>(() => {
    const states: LightState[] = new Array(totalLights).fill("off");
    if (!isRandomLightsMode) {
      // For word/pattern, set pattern lights to high
      for (const index of basePattern) {
        if (index >= 0 && index < totalLights) {
          states[index] = "high";
        }
      }
    }
    // For random lights, all start off (will animate)
    return states;
  });

  // Animate lights - only for random lights mode (word/pattern stays high)
  useEffect(() => {
    // Word/pattern mode: no animation, lights stay high
    if (!isRandomLightsMode) {
      return;
    }

    const interval = setInterval(() => {
      setLightStates((prev) => {
        const next = [...prev];
        // For random lights: animate all lights randomly through off → high (via medium transition)
        // Randomly select a subset of lights to animate each tick
        const lightsToAnimate = Math.floor(totalLights * 0.2); // Animate ~20% of all lights per tick
        const allIndices = Array.from({ length: totalLights }, (_, i) => i);
        const shuffled = allIndices.sort(() => Math.random() - 0.5);

        for (let i = 0; i < Math.min(lightsToAnimate, shuffled.length); i++) {
          const index = shuffled[i];
          if (index >= 0 && index < totalLights) {
            const current = prev[index];
            // State transition logic for random lights: off → high (with medium as intermediate transition state)
            // Medium state is only used as a transition, not a final state
            if (current === "off") {
              // Start transition to high by going to medium first
              next[index] = "medium";
            } else if (current === "medium") {
              // Continue transition: medium → high
              next[index] = "high";
            } else if (current === "high") {
              // Turn off directly (no medium transition when turning off)
              next[index] = "off";
            }
          }
        }
        return next;
      });
    }, 200); // 0.2s interval to match transition duration

    return () => clearInterval(interval);
  }, [isRandomLightsMode, totalLights]);

  const isNextVariant = variant === "next";

  const CardContent = (
    <div
      className={cn(
        "relative flex h-[380px] w-full flex-col overflow-hidden rounded-xl border p-6 transition-[border-color,background-color] duration-150",
        isNextVariant
          ? "border-0 dark:border-0 dark:shadow-[inset_0_0_6px_rgba(255,255,255,0.1)]"
          : "border-border bg-background hover:border-foreground/20",
        className
      )}
      data-variant={isNextVariant ? "next" : undefined}
      style={
        isNextVariant
          ? ({
              background:
                "linear-gradient(110deg, oklch(0.65 0 0) 0.06%, oklch(0.54 0 0) 100%)",
            } as React.CSSProperties)
          : undefined
      }
    >
      {/* Illustration/Switchboard */}
      <div
        className="flex h-full w-full items-center justify-center"
        data-illustration="true"
      >
        <div
          className="grid h-full w-full"
          style={{
            gap: `min(1px, calc(100% / ${columns} / 10))`,
            gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))`,
            gridTemplateRows: `repeat(${rows}, minmax(0, 1fr))`,
            maxHeight: "81px",
            transitionDuration: `${transitionDuration}ms`,
          }}
        >
          {lightStates.map((state, index) => (
            <LightBulb
              // biome-ignore lint/suspicious/noArrayIndexKey: Light grid position is stable and never reorders
              key={`light-${index}`}
              state={state}
              transitionDuration={transitionDuration}
            />
          ))}
        </div>
      </div>

      {/* Title */}
      <div className="mt-4 flex flex-row items-center justify-start gap-2 text-left">
        <h3
          className={cn(
            "text-left font-semibold leading-8 tracking-[-0.04em]",
            isNextVariant ? "mt-4 text-2xl" : "text-foreground text-xl"
          )}
          data-title="true"
          style={
            isNextVariant
              ? ({ color: "oklch(0.95 0 0)" } as React.CSSProperties)
              : undefined
          }
        >
          {title}
        </h3>
      </div>

      {/* Subtitle */}
      <p
        className={cn(
          "text-left text-sm leading-relaxed tracking-[-0.01em]",
          isNextVariant
            ? "mt-1 max-w-[80%] text-base"
            : "mt-1 text-foreground/70"
        )}
        data-subtitle="true"
        style={
          isNextVariant
            ? ({ color: "oklch(0.9 0 0)" } as React.CSSProperties)
            : undefined
        }
      >
        {subtitle}
      </p>
    </div>
  );

  if (href) {
    return (
      <a
        aria-label={title}
        className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
        href={href}
      >
        {CardContent}
      </a>
    );
  }

  if (onButtonClick) {
    return (
      <button
        aria-label={title}
        className="cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
        onClick={onButtonClick}
        type="button"
      >
        {CardContent}
      </button>
    );
  }

  return CardContent;
}

function LightBulb({
  state,
  transitionDuration,
}: {
  state: LightState;
  transitionDuration: number;
}) {
  const transitionStyle = {
    transition: `opacity ${transitionDuration}ms ease-out, transform ${transitionDuration}ms ease-out`,
  };

  return (
    <div
      className={cn(
        "relative rounded-full",
        state === "off" && "bg-foreground/50",
        state === "high" ? "h-[2px] w-[2px]" : "h-px w-px"
      )}
      data-state={state}
      style={{
        ...transitionStyle,
        transform: state === "off" ? "unset" : "scale(1)",
      }}
    >
      {/* Glow effect for medium state (::before equivalent) */}
      {state === "medium" && (
        <div
          className="absolute inset-0 rounded-full"
          style={{
            ...transitionStyle,
            backgroundColor: "var(--color-brand)",
            boxShadow:
              "0 0 2px 1px color-mix(in oklch, var(--color-brand), transparent 60%), 0 0 4px 1.5px color-mix(in oklch, var(--color-brand), transparent 75%)",
            opacity: 1,
          }}
        />
      )}

      {/* Glow effect for high state (::after equivalent) */}
      {state === "high" && (
        <div
          className="absolute inset-0 rounded-full"
          style={{
            ...transitionStyle,
            backgroundColor: "var(--color-brand)",
            boxShadow:
              "0 0 2px 1px color-mix(in oklch, var(--color-brand), transparent 40%), 0 0 4px 1.5px color-mix(in oklch, var(--color-brand), transparent 65%), 0 0 6px 2px color-mix(in oklch, var(--color-brand), transparent 80%)",
            opacity: 1,
          }}
        />
      )}
    </div>
  );
}

Similar components