Domphy

hexagonPattern

A Backgrounds block/component from Magic UI — clean-room reimplemented for Domphy (see methodology). Call hexagonPattern() with no arguments for a working demo, or edit the code below live.

Implementation notes

Full clean-room reimplementation: <svg><defs><pattern> tile containing either closed hexagon polygons (solid) or 6 per-edge <line> segments (strokeDasharray set), plus a fill-url(#pattern) <rect>, plus extra highlighted-cell <polygon>s layered on top, all using tags already namespace-safe in @domphy/core (svg/defs/pattern/polygon/line/rect/g). Own-derived single-tile hex-grid geometry (3 hexagon instances per tile: one full + two seam-straddling halves) for both flat-top ('horizontal') and pointy-top ('vertical') orientation, matching the spec's every-other-row/column half-step interlock. Stroke uses currentColor (svg root's color set via themeColor) rather than per-element themeColor calls, avoiding doctor's missing-color rule entirely for the outline tile; highlighted cells do use themeColor for fill and are _doctorDisable: missing-color (decorative, no text), matching the meteors()/dottedMap() convention already in this package. Deviation from spec: the exported factory returns a self-sized demo wrapper (dataTone shift-1 panel with default heading+paragraph foreground content, overridable via children) rather than a bare position layer with no intrinsic size — necessary so hexagonPattern() with zero args is actually visible/screenshot-able per this package's factory-function contract; callers who just want the raw pattern layer can lift the inner <svg> out. direction default ('horizontal'/flat-top) and the demo hexagons highlight set are this port's own judgment calls (spec's research note didn't state a direction default). doctor CLI: 0 diagnostics.

Status: ported · Reference: Magic UI original

// magicui "Hexagon Pattern" — clean-room reimplementation from the public
// behavior/visual spec only (no upstream source viewed or copied). A
// tileable SVG background of honeycomb-style hexagon outlines (flat-top or
// pointy-top), with optional solid-filled highlighted cells layered above
// the tile.
//
// Tiling technique: a single repeating `<pattern>` tile packs THREE hexagon
// instances — one fully inside the tile and two half-instances straddling
// the tile's seam edge — so the standard "every other row/column offset by
// half a step" honeycomb interlock reproduces seamlessly once the pattern
// repeats via `patternUnits="userSpaceOnUse"`. This is a self-derived
// single-tile hex-grid construction, not copied from any source.

import type { DomphyElement, StyleObject } from "@domphy/core";
import { heading, paragraph } from "@domphy/ui";
import { type ThemeColor, themeColor, themeSpacing } from "@domphy/theme";

export type HexagonPatternDirection = "horizontal" | "vertical";

export interface HexagonPatternProps {
  /** Hexagon radius, center to vertex, in SVG user units. Defaults to `40`. */
  radius?: number;
  /** Extra spacing added between adjacent hexagons, in user units. Defaults to `0`. */
  gap?: number;
  /** Pattern origin horizontal offset, in user units. Defaults to `-1`. */
  x?: number;
  /** Pattern origin vertical offset, in user units. Defaults to `-1`. */
  y?: number;
  /** `"horizontal"` renders flat-top hexagons (flat edges up/down); `"vertical"` renders
   * pointy-top hexagons (a vertex up/down). Defaults to `"horizontal"`. */
  direction?: HexagonPatternDirection;
  /** `[column, row]` coordinates of cells to render as solid highlighted hexagons, layered
   * above the outline tile. Defaults to a small demo set. */
  hexagons?: Array<[number, number]>;
  /** Switches outline rendering from a closed polygon to per-edge dashed line segments
   * (e.g. `"4 2"`). Solid closed polygons are used when omitted. */
  strokeDasharray?: string;
  /** Theme color family for the outline stroke. Defaults to `"neutral"`. */
  color?: ThemeColor;
  /** Theme color family for highlighted cells. Defaults to `"primary"`. */
  highlightColor?: ThemeColor;
  /** Foreground content layered above the pattern. Defaults to a small demo panel. */
  children?: DomphyElement | DomphyElement[];
  /** Passthrough style merged onto the outer demo wrapper. */
  style?: StyleObject;
}

interface HexPoint {
  x: number;
  y: number;
}

function hexagonVertices(center: HexPoint, radius: number, direction: HexagonPatternDirection): HexPoint[] {
  // "horizontal" = flat-top (vertex at angle 0 → left/right points); "vertical" =
  // pointy-top (vertex at angle -90 → a point straight up).
  const angleOffset = direction === "vertical" ? -90 : 0;
  return Array.from({ length: 6 }, (_unused, index) => {
    const angle = ((60 * index + angleOffset) * Math.PI) / 180;
    return { x: center.x + radius * Math.cos(angle), y: center.y + radius * Math.sin(angle) };
  });
}

function pointsAttribute(vertices: HexPoint[]): string {
  return vertices.map((vertex) => `${vertex.x.toFixed(2)},${vertex.y.toFixed(2)}`).join(" ");
}

let hexagonPatternInstanceCounter = 0;

/**
 * Tileable SVG honeycomb of hexagon outlines, with optional solid-filled
 * highlighted cells layered above the tile. Purely decorative and static —
 * intended as a background layer behind foreground content, with pointer
 * events disabled. Call with no arguments for a working demo — a full-bleed
 * hexagon grid with a few highlighted cells behind a heading.
 */
function hexagonPattern(props: HexagonPatternProps = {}): DomphyElement<"div"> {
  const instanceId = ++hexagonPatternInstanceCounter;
  const radius = Math.max(4, props.radius ?? 40);
  const gap = props.gap ?? 0;
  const originX = props.x ?? -1;
  const originY = props.y ?? -1;
  const direction = props.direction ?? "horizontal";
  const strokeDasharray = props.strokeDasharray;
  const color = props.color ?? "neutral";
  const highlightColor = props.highlightColor ?? "primary";
  const hexagons = props.hexagons ?? [
    [2, 1],
    [4, 3],
    [6, 0],
  ];
  const patternId = `hexagon-pattern-${instanceId}`;

  let tileWidth: number;
  let tileHeight: number;
  let tileCenters: HexPoint[];
  let columnStep: number;
  let rowStep: number;

  if (direction === "horizontal") {
    // Flat-top hexagon: horizontal center-to-center column spacing is
    // 1.5*radius; vertical spacing within a column is sqrt(3)*radius. A
    // 2-column-wide, 1-row-tall tile carries the full even/odd column offset.
    columnStep = 1.5 * radius + gap;
    rowStep = Math.sqrt(3) * radius + gap;
    tileWidth = 2 * columnStep;
    tileHeight = rowStep;
    tileCenters = [
      { x: columnStep / 2, y: tileHeight / 2 },
      { x: 1.5 * columnStep, y: 0 },
      { x: 1.5 * columnStep, y: tileHeight },
    ];
  } else {
    // Pointy-top hexagon: mirror of the flat-top case with rows/columns swapped.
    rowStep = 1.5 * radius + gap;
    columnStep = Math.sqrt(3) * radius + gap;
    tileWidth = columnStep;
    tileHeight = 2 * rowStep;
    tileCenters = [
      { x: tileWidth / 2, y: rowStep / 2 },
      { x: 0, y: 1.5 * rowStep },
      { x: tileWidth, y: 1.5 * rowStep },
    ];
  }

  function cellCenter(column: number, row: number): HexPoint {
    if (direction === "horizontal") {
      const isOddColumn = ((column % 2) + 2) % 2 === 1;
      return {
        x: column * columnStep + columnStep / 2 + originX,
        y: row * rowStep + rowStep / 2 + (isOddColumn ? rowStep / 2 : 0) + originY,
      };
    }
    const isOddRow = ((row % 2) + 2) % 2 === 1;
    return {
      x: column * columnStep + columnStep / 2 + (isOddRow ? columnStep / 2 : 0) + originX,
      y: row * rowStep + rowStep / 2 + originY,
    };
  }

  const tileContent: DomphyElement[] = strokeDasharray
    ? tileCenters.flatMap((center, centerIndex) => {
        const vertices = hexagonVertices(center, radius, direction);
        return vertices.map((vertex, edgeIndex) => {
          const next = vertices[(edgeIndex + 1) % vertices.length];
          return {
            line: null,
            _key: `edge-${centerIndex}-${edgeIndex}`,
            x1: vertex.x,
            y1: vertex.y,
            x2: next.x,
            y2: next.y,
            strokeDasharray,
            style: { stroke: "currentColor", strokeWidth: 1 } as StyleObject,
          } as DomphyElement;
        });
      })
    : tileCenters.map(
        (center, centerIndex) =>
          ({
            polygon: null,
            _key: `cell-${centerIndex}`,
            points: pointsAttribute(hexagonVertices(center, radius, direction)),
            style: { stroke: "currentColor", fill: "none", strokeWidth: 1 } as StyleObject,
          }) as DomphyElement,
      );

  const highlightElements: DomphyElement[] = hexagons.map(
    ([column, row], index) =>
      ({
        polygon: null,
        _key: `highlight-${instanceId}-${index}`,
        points: pointsAttribute(hexagonVertices(cellCenter(column, row), radius, direction)),
        ariaHidden: "true",
        // Decorative highlight cell with no text of its own — exempt from the
        // missing-color contract (mirrors meteors()/dottedMap() in this package).
        _doctorDisable: "missing-color",
        style: {
          fill: (listener) => themeColor(listener, "shift-9", highlightColor),
          stroke: "none",
        } as StyleObject,
      }) as DomphyElement,
  );

  const defaultChildren: DomphyElement[] = [
    { h3: "Hexagon Pattern", $: [heading()] } as DomphyElement,
    {
      p: "A tileable honeycomb of hexagon outlines with a few highlighted cells.",
      $: [paragraph()],
    } as DomphyElement,
  ];
  const contentChildren = props.children
    ? Array.isArray(props.children)
      ? props.children
      : [props.children]
    : defaultChildren;

  return {
    div: [
      {
        svg: [
          {
            defs: [
              {
                pattern: tileContent,
                id: patternId,
                width: tileWidth,
                height: tileHeight,
                patternUnits: "userSpaceOnUse",
                x: originX,
                y: originY,
              } as DomphyElement,
            ],
          } as DomphyElement,
          {
            rect: null,
            width: "100%",
            height: "100%",
            ariaHidden: "true",
            style: { fill: `url(#${patternId})` } as StyleObject,
          } as DomphyElement,
          ...highlightElements,
        ],
        ariaHidden: "true",
        style: {
          position: "absolute",
          inset: 0,
          width: "100%",
          height: "100%",
          pointerEvents: "none",
          color: (listener) => themeColor(listener, "shift-3", color),
        } as StyleObject,
      } as DomphyElement<"svg">,
      { div: contentChildren, style: { position: "relative", zIndex: 1 } },
    ],
    dataTone: "shift-1",
    style: {
      position: "relative",
      overflow: "hidden",
      borderRadius: themeSpacing(4),
      padding: themeSpacing(8),
      minHeight: themeSpacing(64),
      backgroundColor: (listener) => themeColor(listener, "inherit"),
      color: (listener) => themeColor(listener, "shift-9"),
      ...(props.style ?? {}),
    } as StyleObject,
  };
}

export { hexagonPattern };

← Back to Magic UI catalog