Skip to content

Color Swatch

Install this component from the Scintillar registry.

Swatch

Value

#6366F1

Controls

Installation

npx shadcn@latest add @scintillar/color-swatch

Source

"use client";

import { cn } from "@/lib/utils";
import { hexToRgb, formatColor, type ColorFormat } from "@/lib/color-utils";

// ── Checker background (transparency indicator) ─────────────────

export const checkerBg = `linear-gradient(45deg, hsl(0 0% 50% / 0.15) 25%, transparent 25%),linear-gradient(-45deg, hsl(0 0% 50% / 0.15) 25%, transparent 25%),linear-gradient(45deg, transparent 75%, hsl(0 0% 50% / 0.15) 75%),linear-gradient(-45deg, transparent 75%, hsl(0 0% 50% / 0.15) 75%)`;
export const checkerSize = "8px 8px";
export const checkerPosition = "0 0, 0 4px, 4px -4px, -4px 0";

// ── Gradient types ──────────────────────────────────────────────

/** The type of gradient fill. */
export type GradientType = "solid" | "linear" | "radial" | "angular";

/** A single color stop in a gradient. */
export interface GradientStop {
  /** Unique identifier for this stop. */
  id: string;
  /** Hex color value (e.g. "#ff0000"). */
  color: string;
  /** Position along the gradient axis (0–100). */
  position: number;
  /** Opacity of this stop (0–100). */
  opacity: number;
}

/** Full gradient configuration. */
export interface GradientValue {
  /** Gradient type. */
  type: GradientType;
  /** Angle in degrees for linear/angular gradients. */
  angle: number;
  /** Array of color stops. */
  stops: GradientStop[];
}

export function gradientToCss(g: GradientValue): string {
  const stopsCss = [...g.stops].sort((a, b) => a.position - b.position)
    .map((s) => { const { r, g: gr, b } = hexToRgb(s.color); return `rgba(${r}, ${gr}, ${b}, ${s.opacity / 100}) ${s.position}%`; }).join(", ");
  switch (g.type) {
    case "linear": return `linear-gradient(${g.angle}deg, ${stopsCss})`;
    case "radial": return `radial-gradient(circle, ${stopsCss})`;
    case "angular": return `conic-gradient(from ${g.angle}deg, ${stopsCss})`;
    default: return g.stops[0]?.color ?? "#000000";
  }
}

// ── ColorSwatch ─────────────────────────────────────────────────

/** Displays a color preview swatch with support for solid colors, opacity, and gradients. */
export function ColorSwatch({ hex, opacity = 100, gradient, size = "size-6", className, ...props }: {
  /** Hex color value (e.g. "#6366f1"). */
  hex: string;
  /** Opacity percentage (0–100). Shows a split preview when < 100. */
  opacity?: number;
  /** Optional gradient configuration. Overrides hex when type is not "solid". */
  gradient?: GradientValue;
  /** Tailwind size class for the swatch (e.g. "size-6", "size-10"). */
  size?: string;
  /** Additional CSS classes. */
  className?: string;
} & React.ButtonHTMLAttributes<HTMLButtonElement>) {
  if (gradient && gradient.type !== "solid") return (
    <button className={cn(size, "rounded shrink-0 overflow-hidden", className)} style={{ backgroundImage: checkerBg, backgroundSize: checkerSize, backgroundPosition: checkerPosition }} {...props}>
      <div className="w-full h-full rounded" style={{ background: gradientToCss(gradient) }} />
    </button>
  );
  if (opacity >= 100) return <button className={cn(size, "rounded shrink-0", className)} style={{ background: hex }} {...props} />;
  return (
    <button className={cn(size, "rounded shrink-0 overflow-hidden flex", className)} style={{ backgroundImage: checkerBg, backgroundSize: checkerSize, backgroundPosition: checkerPosition }} {...props}>
      <div className="w-1/2 h-full" style={{ background: hex }} /><div className="w-1/2 h-full" style={{ background: hex, opacity: opacity / 100 }} />
    </button>
  );
}

// ── ColorValue — swatch + formatted color code ──────────────────

/** Displays a color swatch alongside a formatted color code label. */
export function ColorValue({ hex, opacity = 100, format = "hex", gradient, className }: {
  /** Hex color value. */
  hex: string;
  /** Opacity percentage (0–100). */
  opacity?: number;
  /** Color format for the displayed label. */
  format?: ColorFormat;
  /** Optional gradient configuration. */
  gradient?: GradientValue;
  /** Additional CSS classes. */
  className?: string;
}) {
  if (!hex && !gradient) return <div className={cn("size-6 rounded border border-dashed shrink-0", className)} />;
  const isGrad = gradient && gradient.type !== "solid";
  return (
    <div className={cn("flex items-center gap-2", className)}>
      <ColorSwatch hex={hex} opacity={opacity} gradient={gradient} className="cursor-default" />
      {isGrad ? (
        <span className="text-xs text-muted-foreground truncate capitalize">{gradient.type} · {gradient.stops.length} stops</span>
      ) : (
        <>
          <span className="text-xs font-mono text-muted-foreground truncate">{formatColor(hex, format)}</span>
          {opacity < 100 && <span className="text-[10px] font-mono text-muted-foreground shrink-0">{opacity}%</span>}
        </>
      )}
    </div>
  );
}