Backgrounds

Grid Fade

A line grid background whose edges dissolve through a gradient fade mask. Static, pure CSS — border-token lines that sit quietly in both themes.

Install

npx shadcn@latest add @paragon/grid-fade

grid-fade.tsx

import * as React from "react";
import { cn } from "@/lib/utils";

export interface GridFadeProps extends React.ComponentProps<"div"> {
  /** Grid cell size in px. */
  cellSize?: number;
  /** Line thickness in px. */
  strokeWidth?: number;
  /** Which edge(s) of the grid dissolve into the background. */
  fade?: "edges" | "top" | "bottom" | "none";
  /** Overall effect opacity, 0–1. */
  strength?: number;
}

const FADE_MASKS: Record<Exclude<NonNullable<GridFadeProps["fade"]>, "none">, string> = {
  edges: "radial-gradient(ellipse 85% 85% at 50% 50%, #000 25%, transparent 82%)",
  top: "linear-gradient(to bottom, transparent 2%, #000 60%)",
  bottom: "linear-gradient(to bottom, #000 40%, transparent 98%)",
};

/**
 * A line grid background whose edges dissolve through a gradient fade mask.
 *
 * Static, pure CSS: two repeating linear-gradients draw the lines and a
 * mask-image fades them out toward the chosen edge(s). Lines are painted
 * with `currentColor` seeded from the border token, so the grid sits
 * quietly in both themes and recolors via any `text-*` utility.
 * Absolutely positioned — parent needs `position: relative` (and usually
 * `overflow-hidden`).
 */
export function GridFade({
  cellSize = 32,
  strokeWidth = 1,
  fade = "edges",
  strength = 1,
  className,
  style,
  ...props
}: GridFadeProps) {
  const mask = fade === "none" ? undefined : FADE_MASKS[fade];

  return (
    <div
      aria-hidden
      data-slot="grid-fade"
      className={cn("pointer-events-none absolute inset-0 text-border", className)}
      style={{
        opacity: strength,
        backgroundImage: `linear-gradient(to right, currentColor ${strokeWidth}px, transparent ${strokeWidth}px), linear-gradient(to bottom, currentColor ${strokeWidth}px, transparent ${strokeWidth}px)`,
        backgroundSize: `${cellSize}px ${cellSize}px`,
        backgroundPosition: "50% 50%",
        ...(mask ? { maskImage: mask, WebkitMaskImage: mask } : {}),
        ...style,
      }}
      {...props}
    />
  );
}