Web InnoventixFreeCode

Grid Background

Original · free

grid line background with fade

byWeb InnoventixReact + Tailwind
bgxgridbackgrounds
Open

Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.

npx shadcn@latest add https://webinnoventix.com/r/bgx-grid.json
bgx-grid.tsx
"use client";

import { useId, useState, type CSSProperties } from "react";
import { motion, useReducedMotion } from "motion/react";

type Density = "sparse" | "regular" | "dense";
type Fade = "radial" | "topdown" | "vignette";
type Accent = "indigo" | "emerald" | "rose" | "amber";

const DENSITY: Record<Density, { label: string; size: number }> = {
  sparse: { label: "Sparse", size: 72 },
  regular: { label: "Regular", size: 44 },
  dense: { label: "Dense", size: 26 },
};

const FADE: Record<Fade, { label: string; mask: string }> = {
  radial: {
    label: "Radial",
    mask: "radial-gradient(ellipse 78% 66% at 50% 42%, #000 26%, transparent 74%)",
  },
  topdown: {
    label: "Top-down",
    mask: "linear-gradient(to bottom, #000 0%, #000 16%, transparent 88%)",
  },
  vignette: {
    label: "Vignette",
    mask: "radial-gradient(circle at 50% 50%, #000 40%, transparent 80%)",
  },
};

const ACCENTS: Record<
  Accent,
  { label: string; swatch: string; ring: string; focus: string; rgb: string }
> = {
  indigo: {
    label: "Indigo",
    swatch: "bg-indigo-500",
    ring: "peer-checked:ring-indigo-500",
    focus: "peer-focus-visible:ring-indigo-500",
    rgb: "99, 102, 241",
  },
  emerald: {
    label: "Emerald",
    swatch: "bg-emerald-500",
    ring: "peer-checked:ring-emerald-500",
    focus: "peer-focus-visible:ring-emerald-500",
    rgb: "16, 185, 129",
  },
  rose: {
    label: "Rose",
    swatch: "bg-rose-500",
    ring: "peer-checked:ring-rose-500",
    focus: "peer-focus-visible:ring-rose-500",
    rgb: "244, 63, 94",
  },
  amber: {
    label: "Amber",
    swatch: "bg-amber-500",
    ring: "peer-checked:ring-amber-500",
    focus: "peer-focus-visible:ring-amber-500",
    rgb: "245, 158, 11",
  },
};

const ACCENT_ORDER: Accent[] = ["indigo", "emerald", "rose", "amber"];

function gridStyle(size: number, minor: string, major: string): CSSProperties {
  const major4 = `${size * 4}px ${size * 4}px`;
  const minor1 = `${size}px ${size}px`;
  return {
    backgroundImage: [
      `linear-gradient(to right, ${major} 1px, transparent 1px)`,
      `linear-gradient(to bottom, ${major} 1px, transparent 1px)`,
      `linear-gradient(to right, ${minor} 1px, transparent 1px)`,
      `linear-gradient(to bottom, ${minor} 1px, transparent 1px)`,
    ].join(", "),
    backgroundSize: `${major4}, ${major4}, ${minor1}, ${minor1}`,
    backgroundPosition: "center center",
  };
}

function Segmented<T extends string>(props: {
  legend: string;
  name: string;
  value: T;
  options: ReadonlyArray<{ value: T; label: string }>;
  onChange: (value: T) => void;
}) {
  const { legend, name, value, options, onChange } = props;
  return (
    <fieldset className="min-w-0">
      <legend className="mb-2 text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400">
        {legend}
      </legend>
      <div className="flex gap-1 rounded-xl bg-slate-100 p-1 dark:bg-slate-800/60">
        {options.map((o) => (
          <label key={o.value} className="relative flex-1">
            <input
              type="radio"
              name={name}
              value={o.value}
              checked={value === o.value}
              onChange={() => onChange(o.value)}
              className="peer sr-only"
            />
            <span className="flex cursor-pointer select-none items-center justify-center rounded-lg px-2.5 py-2 text-sm font-medium text-slate-600 transition-colors hover:text-slate-900 peer-checked:bg-white peer-checked:text-slate-900 peer-checked:shadow-sm peer-focus-visible:ring-2 peer-focus-visible:ring-indigo-500 peer-focus-visible:ring-offset-2 peer-focus-visible:ring-offset-slate-100 dark:text-slate-300 dark:hover:text-white dark:peer-checked:bg-slate-950 dark:peer-checked:text-white dark:peer-focus-visible:ring-offset-slate-800">
              {o.label}
            </span>
          </label>
        ))}
      </div>
    </fieldset>
  );
}

export default function BgxGrid() {
  const reduce = useReducedMotion() ?? false;
  const uid = useId();
  const beamLabelId = `${uid}-beams`;

  const [density, setDensity] = useState<Density>("regular");
  const [fade, setFade] = useState<Fade>("radial");
  const [accent, setAccent] = useState<Accent>("indigo");
  const [beams, setBeams] = useState<boolean>(true);

  const effectiveBeams = beams && !reduce;
  const size = DENSITY[density].size;
  const rgb = ACCENTS[accent].rgb;

  const maskStyle: CSSProperties = {
    maskImage: FADE[fade].mask,
    WebkitMaskImage: FADE[fade].mask,
    maskRepeat: "no-repeat",
    WebkitMaskRepeat: "no-repeat",
    maskSize: "100% 100%",
    WebkitMaskSize: "100% 100%",
    maskPosition: "center",
    WebkitMaskPosition: "center",
  };

  function reset() {
    setDensity("regular");
    setFade("radial");
    setAccent("indigo");
    setBeams(true);
  }

  const densityOptions = (Object.keys(DENSITY) as Density[]).map((k) => ({
    value: k,
    label: DENSITY[k].label,
  }));
  const fadeOptions = (Object.keys(FADE) as Fade[]).map((k) => ({
    value: k,
    label: FADE[k].label,
  }));

  return (
    <section className="relative w-full overflow-hidden bg-slate-50 text-slate-900 dark:bg-slate-950 dark:text-slate-100">
      <style>{`
        @keyframes bgxgrid-beam-x {
          0% { transform: translateX(-32vw); opacity: 0; }
          12% { opacity: 1; }
          88% { opacity: 1; }
          100% { transform: translateX(132vw); opacity: 0; }
        }
        @keyframes bgxgrid-beam-y {
          0% { transform: translateY(-32vh); opacity: 0; }
          12% { opacity: 1; }
          88% { opacity: 1; }
          100% { transform: translateY(132vh); opacity: 0; }
        }
        @keyframes bgxgrid-glow {
          0%, 100% { opacity: 0.32; transform: translate(-50%, -50%) scale(1); }
          50% { opacity: 0.58; transform: translate(-50%, -50%) scale(1.08); }
        }
        .bgxgrid-beam-x { animation: bgxgrid-beam-x 7.5s linear infinite; }
        .bgxgrid-beam-y { animation: bgxgrid-beam-y 9.5s linear infinite; animation-delay: 1.2s; }
        .bgxgrid-glow-anim { animation: bgxgrid-glow 6s ease-in-out infinite; }
        @media (prefers-reduced-motion: reduce) {
          .bgxgrid-beam-x, .bgxgrid-beam-y, .bgxgrid-glow-anim {
            animation: none !important;
          }
        }
      `}</style>

      {/* Background: grid lines + fade mask + accent glow + optional beams */}
      <div
        aria-hidden="true"
        className="pointer-events-none absolute inset-0 overflow-hidden"
        style={maskStyle}
      >
        <div
          className="absolute inset-0 dark:hidden"
          style={gridStyle(size, "rgba(15, 23, 42, 0.06)", "rgba(15, 23, 42, 0.11)")}
        />
        <div
          className="absolute inset-0 hidden dark:block"
          style={gridStyle(size, "rgba(148, 163, 184, 0.08)", "rgba(148, 163, 184, 0.15)")}
        />

        <div
          className={`absolute left-1/2 top-1/2 h-[34rem] w-[34rem] max-w-full -translate-x-1/2 -translate-y-1/2 rounded-full blur-2xl ${
            effectiveBeams ? "bgxgrid-glow-anim" : ""
          }`}
          style={{
            background: `radial-gradient(circle at center, rgba(${rgb}, 0.22), transparent 68%)`,
            opacity: effectiveBeams ? undefined : 0.4,
          }}
        />

        {effectiveBeams && (
          <>
            <span
              className="bgxgrid-beam-x absolute left-0 top-[34%] h-px w-[22%] will-change-transform"
              style={{
                background: `linear-gradient(90deg, transparent, rgba(${rgb}, 0.85), transparent)`,
              }}
            />
            <span
              className="bgxgrid-beam-y absolute left-[62%] top-0 h-[22%] w-px will-change-transform"
              style={{
                background: `linear-gradient(180deg, transparent, rgba(${rgb}, 0.85), transparent)`,
              }}
            />
          </>
        )}
      </div>

      {/* Foreground content */}
      <div className="relative z-10 mx-auto max-w-6xl px-6 py-24 sm:py-32">
        <motion.div
          initial={reduce ? false : { opacity: 0, y: 18 }}
          whileInView={reduce ? undefined : { opacity: 1, y: 0 }}
          viewport={{ once: true, margin: "-80px" }}
          transition={{ duration: 0.6, ease: [0.22, 1, 0.36, 1] }}
          className="mx-auto max-w-2xl text-center"
        >
          <span className="inline-flex items-center gap-2 rounded-full border border-slate-200 bg-white/70 px-3 py-1 text-xs font-semibold uppercase tracking-widest text-slate-600 backdrop-blur dark:border-slate-800 dark:bg-slate-900/70 dark:text-slate-300">
            <svg
              viewBox="0 0 24 24"
              aria-hidden="true"
              className="h-3.5 w-3.5"
              fill="none"
              stroke="currentColor"
              strokeWidth="1.6"
              strokeLinecap="round"
            >
              <path d="M3 9h18M3 15h18M9 3v18M15 3v18" />
            </svg>
            Backgrounds / Grid
          </span>

          <h2 className="mt-6 text-balance text-4xl font-semibold tracking-tight sm:text-5xl">
            A grid that knows when to disappear
          </h2>

          <p className="mt-5 text-pretty text-base leading-relaxed text-slate-600 dark:text-slate-300 sm:text-lg">
            Structured guide lines that dissolve into the page, so your content
            stays the hero. Tune the density, choose where the grid fades out,
            and pick an accent that matches your product — every change updates
            the backdrop live.
          </p>
        </motion.div>

        <motion.div
          initial={reduce ? false : { opacity: 0, y: 24 }}
          whileInView={reduce ? undefined : { opacity: 1, y: 0 }}
          viewport={{ once: true, margin: "-80px" }}
          transition={{ duration: 0.6, delay: 0.08, ease: [0.22, 1, 0.36, 1] }}
          className="mx-auto mt-12 max-w-3xl rounded-2xl border border-slate-200 bg-white/70 p-6 shadow-xl shadow-slate-900/5 backdrop-blur-md dark:border-slate-800 dark:bg-slate-900/60 dark:shadow-black/20 sm:p-8"
        >
          <div className="flex items-center justify-between gap-4">
            <div>
              <p className="text-sm font-semibold text-slate-900 dark:text-white">
                Live controls
              </p>
              <p className="mt-0.5 text-xs text-slate-500 dark:text-slate-400">
                Adjust the background behind this card.
              </p>
            </div>
            <button
              type="button"
              onClick={reset}
              className="inline-flex items-center gap-1.5 rounded-lg border border-slate-200 bg-white px-3 py-1.5 text-sm font-medium text-slate-700 transition-colors hover:bg-slate-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:border-slate-700 dark:bg-slate-800 dark:text-slate-200 dark:hover:bg-slate-700 dark:focus-visible:ring-offset-slate-900"
            >
              <svg
                viewBox="0 0 24 24"
                aria-hidden="true"
                className="h-4 w-4"
                fill="none"
                stroke="currentColor"
                strokeWidth="1.7"
                strokeLinecap="round"
                strokeLinejoin="round"
              >
                <path d="M3 12a9 9 0 1 0 3-6.7L3 8" />
                <path d="M3 3v5h5" />
              </svg>
              Reset
            </button>
          </div>

          <div className="mt-6 grid gap-6 sm:grid-cols-2">
            <Segmented<Density>
              legend="Density"
              name={`${uid}-density`}
              value={density}
              options={densityOptions}
              onChange={setDensity}
            />
            <Segmented<Fade>
              legend="Fade"
              name={`${uid}-fade`}
              value={fade}
              options={fadeOptions}
              onChange={setFade}
            />
          </div>

          <div className="mt-6 grid gap-6 sm:grid-cols-2 sm:items-start">
            <fieldset>
              <legend className="mb-2 text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400">
                Accent
              </legend>
              <div className="flex gap-3">
                {ACCENT_ORDER.map((a) => (
                  <label key={a} className="relative inline-flex">
                    <input
                      type="radio"
                      name={`${uid}-accent`}
                      value={a}
                      checked={accent === a}
                      onChange={() => setAccent(a)}
                      className="peer sr-only"
                    />
                    <span
                      aria-hidden="true"
                      className={`grid h-9 w-9 cursor-pointer place-items-center rounded-full ring-2 ring-transparent ring-offset-2 ring-offset-white transition dark:ring-offset-slate-900 ${ACCENTS[a].swatch} ${ACCENTS[a].ring} ${ACCENTS[a].focus}`}
                    >
                      {accent === a && (
                        <svg
                          viewBox="0 0 24 24"
                          className="h-4 w-4 text-white"
                          fill="none"
                          stroke="currentColor"
                          strokeWidth="3"
                          strokeLinecap="round"
                          strokeLinejoin="round"
                        >
                          <path d="M5 13l4 4L19 7" />
                        </svg>
                      )}
                    </span>
                    <span className="sr-only">{ACCENTS[a].label}</span>
                  </label>
                ))}
              </div>
            </fieldset>

            <div>
              <p className="mb-2 text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400">
                Motion
              </p>
              <div className="flex items-center justify-between rounded-xl bg-slate-100 px-4 py-3 dark:bg-slate-800/60">
                <span
                  id={beamLabelId}
                  className="text-sm font-medium text-slate-700 dark:text-slate-200"
                >
                  Animated beams
                  {reduce && (
                    <span className="mt-0.5 block text-xs font-normal text-slate-500 dark:text-slate-400">
                      Off — your system reduces motion.
                    </span>
                  )}
                </span>
                <button
                  type="button"
                  role="switch"
                  aria-checked={effectiveBeams}
                  aria-labelledby={beamLabelId}
                  disabled={reduce}
                  onClick={() => setBeams((v) => !v)}
                  className="relative inline-flex h-6 w-11 shrink-0 items-center rounded-full bg-slate-300 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-100 disabled:cursor-not-allowed disabled:opacity-40 aria-checked:bg-indigo-500 dark:bg-slate-700 dark:focus-visible:ring-offset-slate-800 dark:aria-checked:bg-indigo-500"
                >
                  <span
                    className={`inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform ${
                      effectiveBeams ? "translate-x-6" : "translate-x-1"
                    }`}
                  />
                </button>
              </div>
            </div>
          </div>

          <p aria-live="polite" className="sr-only">
            {`Grid density ${DENSITY[density].label}, ${FADE[fade].label} fade, ${ACCENTS[accent].label} accent, beams ${effectiveBeams ? "on" : "off"}.`}
          </p>
        </motion.div>

        <p className="mx-auto mt-8 max-w-3xl text-center text-xs text-slate-400 dark:text-slate-500">
          Pure CSS gradients masked with a fade — no images, no canvas. Renders
          crisp on any background in light and dark.
        </p>
      </div>
    </section>
  );
}

Dependencies

motion

Licence

Built by Web Innoventix. Free for personal and commercial use, no attribution required.

Built by Web Innoventix

Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.

Get a free quote

Similar components

Browse all →