Web InnoventixFreeCode

Stripes Background

Original · free

animated stripe background

byWeb InnoventixReact + Tailwind
bgxstripesbackgrounds
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-stripes.json
bgx-stripes.tsx
"use client";

import { useEffect, useMemo, useRef, useState, type CSSProperties } from "react";
import { motion, useReducedMotion } from "motion/react";

type CSSVars = CSSProperties & Record<`--${string}`, string>;

const PALETTES = [
  { id: "indigo", label: "Indigo", a: "rgba(99,102,241,0.55)", b: "rgba(139,92,246,0.26)" },
  { id: "aurora", label: "Aurora", a: "rgba(16,185,129,0.52)", b: "rgba(14,165,233,0.26)" },
  { id: "ember", label: "Ember", a: "rgba(245,158,11,0.52)", b: "rgba(244,63,94,0.26)" },
  { id: "slate", label: "Slate", a: "rgba(100,116,139,0.48)", b: "rgba(148,163,184,0.2)" },
] as const;

type PaletteId = (typeof PALETTES)[number]["id"];

const FEATURES = [
  "Transform-only animation — it lives on the compositor and never touches layout.",
  "Honors prefers-reduced-motion and pauses the moment you ask it to.",
  "Angle, stripe width, palette and drift time all update live below.",
] as const;

function IconPlay() {
  return (
    <svg viewBox="0 0 24 24" className="h-4 w-4" fill="currentColor" aria-hidden="true">
      <path d="M8 5.14v13.72a1 1 0 0 0 1.54.84l10.29-6.86a1 1 0 0 0 0-1.68L9.54 4.3A1 1 0 0 0 8 5.14Z" />
    </svg>
  );
}

function IconPause() {
  return (
    <svg viewBox="0 0 24 24" className="h-4 w-4" fill="currentColor" aria-hidden="true">
      <rect x="6" y="5" width="4" height="14" rx="1.2" />
      <rect x="14" y="5" width="4" height="14" rx="1.2" />
    </svg>
  );
}

function IconReverse() {
  return (
    <svg viewBox="0 0 24 24" className="h-4 w-4" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
      <path d="M3 12a9 9 0 0 1 15.5-6.2L21 8" />
      <path d="M21 3v5h-5" />
      <path d="M21 12a9 9 0 0 1-15.5 6.2L3 16" />
      <path d="M3 21v-5h5" />
    </svg>
  );
}

function IconCopy() {
  return (
    <svg viewBox="0 0 24 24" className="h-4 w-4" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
      <rect x="9" y="9" width="11" height="11" rx="2.2" />
      <path d="M5 15V5a2 2 0 0 1 2-2h8" />
    </svg>
  );
}

function IconCheck() {
  return (
    <svg viewBox="0 0 24 24" className="h-4 w-4" fill="none" stroke="currentColor" strokeWidth={2.4} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
      <path d="m5 13 4 4 10-11" />
    </svg>
  );
}

function RangeControl({
  id,
  label,
  hint,
  min,
  max,
  step,
  value,
  unit,
  onChange,
}: {
  id: string;
  label: string;
  hint: string;
  min: number;
  max: number;
  step: number;
  value: number;
  unit: string;
  onChange: (next: number) => void;
}) {
  return (
    <div>
      <div className="flex items-baseline justify-between gap-3">
        <label htmlFor={id} className="text-sm font-medium text-slate-700 dark:text-slate-200">
          {label}
        </label>
        <span className="rounded-md bg-slate-100 px-2 py-0.5 text-xs font-semibold tabular-nums text-slate-600 dark:bg-slate-800 dark:text-slate-300">
          {value}
          {unit}
        </span>
      </div>
      <input
        id={id}
        type="range"
        min={min}
        max={max}
        step={step}
        value={value}
        aria-valuetext={`${value}${unit}`}
        onChange={(e) => onChange(Number(e.target.value))}
        className="mt-3 w-full cursor-pointer rounded accent-indigo-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:accent-indigo-400 dark:focus-visible:ring-offset-slate-900"
      />
      <p className="mt-1.5 text-xs text-slate-500 dark:text-slate-400">{hint}</p>
    </div>
  );
}

export default function BgxStripes() {
  const prefersReduced = useReducedMotion();
  const reduce = !!prefersReduced;

  const [paused, setPaused] = useState(false);
  const [reverse, setReverse] = useState(false);
  const [palette, setPalette] = useState<PaletteId>("indigo");
  const [angle, setAngle] = useState(58);
  const [width, setWidth] = useState(34);
  const [drift, setDrift] = useState(11);
  const [copied, setCopied] = useState(false);

  const timerRef = useRef<number | null>(null);

  useEffect(() => {
    return () => {
      if (timerRef.current !== null) window.clearTimeout(timerRef.current);
    };
  }, []);

  const active = PALETTES.find((p) => p.id === palette) ?? PALETTES[0];
  const running = !paused && !reduce;

  const canvasStyle: CSSVars = {
    "--bgx-angle": `${angle}deg`,
    "--bgx-w": `${width}px`,
    "--bgx-dur": `${drift}s`,
    "--bgx-a": active.a,
    "--bgx-b": active.b,
  };

  const stripeStyle: CSSProperties = {
    animationPlayState: running ? "running" : "paused",
    animationDirection: reverse ? "reverse" : "normal",
  };

  const cssSnippet = useMemo(
    () =>
      `.stripes {
  background-image: repeating-linear-gradient(
    ${angle}deg,
    ${active.a} 0 ${width}px,
    ${active.b} ${width}px ${width * 2}px
  );
  animation: drift ${drift}s linear infinite${reverse ? " reverse" : ""};
}
@keyframes drift {
  to { transform: translate3d(-${width * 2}px, 0, 0); }
}`,
    [angle, width, drift, reverse, active],
  );

  const copy = async () => {
    try {
      await navigator.clipboard.writeText(cssSnippet);
      setCopied(true);
      if (timerRef.current !== null) window.clearTimeout(timerRef.current);
      timerRef.current = window.setTimeout(() => setCopied(false), 2000);
    } catch {
      setCopied(false);
    }
  };

  const toggleCls = (on: boolean) =>
    `inline-flex items-center gap-2 rounded-xl border px-3.5 py-2 text-sm font-medium transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-not-allowed disabled:opacity-50 dark:focus-visible:ring-offset-slate-900 ${
      on
        ? "border-indigo-500 bg-indigo-500 text-white shadow-sm shadow-indigo-500/30 dark:border-indigo-400 dark:bg-indigo-400 dark:text-slate-950"
        : "border-slate-200 bg-white/70 text-slate-700 hover:bg-white dark:border-white/10 dark:bg-slate-800/60 dark:text-slate-200 dark:hover:bg-slate-800"
    }`;

  const enter = (delay: number) =>
    prefersReduced
      ? {}
      : {
          initial: { opacity: 0, y: 20 },
          whileInView: { opacity: 1, y: 0 },
          viewport: { once: true, amount: 0.3 },
          transition: { duration: 0.6, ease: [0.22, 1, 0.36, 1] as const, delay },
        };

  return (
    <section className="relative w-full overflow-hidden bg-white text-slate-900 dark:bg-slate-950 dark:text-slate-100">
      <style>{`
        @keyframes bgx-stripes-drift {
          from { transform: translate3d(0, 0, 0); }
          to   { transform: translate3d(calc(var(--bgx-w) * -2), 0, 0); }
        }
        @keyframes bgx-stripes-pulse {
          0%, 100% { opacity: 1; transform: scale(1); }
          50%      { opacity: .35; transform: scale(.6); }
        }
        .bgx-rotor {
          position: absolute;
          top: 50%;
          left: 50%;
          width: 230%;
          height: 230%;
          transform: translate(-50%, -50%) rotate(var(--bgx-angle));
          transform-origin: center;
          transition: transform 500ms cubic-bezier(.22, 1, .36, 1);
        }
        .bgx-stripes {
          position: absolute;
          inset: -240px;
          background-image: repeating-linear-gradient(
            90deg,
            var(--bgx-a) 0,
            var(--bgx-a) var(--bgx-w),
            var(--bgx-b) var(--bgx-w),
            var(--bgx-b) calc(var(--bgx-w) * 2)
          );
          animation: bgx-stripes-drift var(--bgx-dur) linear infinite;
          will-change: transform;
        }
        .bgx-dot { animation: bgx-stripes-pulse 2.4s ease-in-out infinite; }
        @media (prefers-reduced-motion: reduce) {
          .bgx-rotor { transition: none; }
          .bgx-stripes { animation: none; }
          .bgx-dot { animation: none; }
        }
      `}</style>

      {/* Animated stripe field */}
      <div className="absolute inset-0 overflow-hidden" style={canvasStyle} aria-hidden="true">
        <div className="bgx-rotor">
          <div className="bgx-stripes" style={stripeStyle} />
        </div>
        <div className="absolute inset-0 bg-gradient-to-b from-white/80 via-transparent to-white/85 dark:from-slate-950/85 dark:via-transparent dark:to-slate-950/90" />
        <div className="absolute inset-0 bg-gradient-to-r from-white/45 via-transparent to-white/45 dark:from-slate-950/45 dark:via-transparent dark:to-slate-950/45" />
      </div>

      {/* Content */}
      <div className="relative mx-auto grid w-full max-w-6xl items-center gap-8 px-6 py-20 sm:py-28 lg:grid-cols-2 lg:gap-12 lg:px-8">
        <motion.div
          {...enter(0)}
          className="rounded-3xl border border-slate-200/70 bg-white/55 p-8 shadow-xl shadow-slate-900/5 backdrop-blur-md sm:p-10 dark:border-white/10 dark:bg-slate-900/45 dark:shadow-black/30"
        >
          <span className="inline-flex items-center gap-2 rounded-full border border-slate-200/70 bg-white/70 px-3 py-1 text-xs font-medium text-slate-600 dark:border-white/10 dark:bg-slate-800/70 dark:text-slate-300">
            <span className="bgx-dot h-2 w-2 rounded-full bg-emerald-500 dark:bg-emerald-400" />
            Live preview · GPU-drawn
          </span>

          <h1 className="mt-6 text-4xl font-semibold tracking-tight sm:text-5xl">
            A backdrop that sets the tone before a word is read.
          </h1>

          <p className="mt-5 text-base leading-relaxed text-slate-600 dark:text-slate-300">
            Drop this stripe field behind a hero, a pricing table, or a login screen. It drifts
            quietly, keeps its distance from your type, and re-skins to match any brand in a single
            click. Everything on the right is wired to the pattern you are looking at.
          </p>

          <ul className="mt-8 space-y-3">
            {FEATURES.map((feature) => (
              <li key={feature} className="flex items-start gap-3 text-sm text-slate-600 dark:text-slate-300">
                <span className="mt-0.5 inline-flex h-5 w-5 flex-none items-center justify-center rounded-full bg-emerald-100 text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-300">
                  <IconCheck />
                </span>
                <span>{feature}</span>
              </li>
            ))}
          </ul>
        </motion.div>

        <motion.div
          {...enter(0.12)}
          className="rounded-3xl border border-slate-200/70 bg-white/60 p-6 shadow-xl shadow-slate-900/5 backdrop-blur-md sm:p-8 dark:border-white/10 dark:bg-slate-900/50 dark:shadow-black/30"
        >
          <div className="flex items-center justify-between gap-4">
            <div>
              <h2 className="text-lg font-semibold tracking-tight">Tune the field</h2>
              <p className="text-sm text-slate-500 dark:text-slate-400">Live, keyboard-accessible controls.</p>
            </div>
            <button
              type="button"
              onClick={copy}
              className="inline-flex items-center gap-2 rounded-xl border border-slate-200 bg-white/70 px-3 py-2 text-sm font-medium text-slate-700 transition hover:bg-white 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-white/10 dark:bg-slate-800/60 dark:text-slate-200 dark:hover:bg-slate-800 dark:focus-visible:ring-offset-slate-900"
            >
              {copied ? <IconCheck /> : <IconCopy />}
              {copied ? "Copied" : "Copy CSS"}
            </button>
          </div>

          <span role="status" aria-live="polite" className="sr-only">
            {copied ? "CSS copied to clipboard" : ""}
          </span>

          {/* Playback */}
          <div className="mt-6 flex flex-wrap gap-2">
            <button
              type="button"
              onClick={() => setPaused((p) => !p)}
              disabled={reduce}
              aria-pressed={running}
              aria-label={running ? "Pause the animation" : "Play the animation"}
              className={toggleCls(running)}
            >
              {running ? <IconPause /> : <IconPlay />}
              {running ? "Pause" : "Play"}
            </button>
            <button
              type="button"
              onClick={() => setReverse((r) => !r)}
              disabled={reduce}
              aria-pressed={reverse}
              className={toggleCls(reverse)}
            >
              <IconReverse />
              Reverse
            </button>
          </div>

          {reduce && (
            <p className="mt-3 rounded-xl border border-amber-300/60 bg-amber-50 px-3 py-2 text-xs text-amber-800 dark:border-amber-400/25 dark:bg-amber-400/10 dark:text-amber-200">
              Your system requests reduced motion, so drifting is held still. Colour, angle and width
              still update the preview.
            </p>
          )}

          {/* Palette */}
          <fieldset className="mt-6">
            <legend className="text-sm font-medium text-slate-700 dark:text-slate-200">Palette</legend>
            <div className="mt-3 grid grid-cols-2 gap-2 sm:grid-cols-4">
              {PALETTES.map((p) => (
                <label
                  key={p.id}
                  className="group relative flex cursor-pointer items-center gap-2 rounded-xl border border-slate-200 bg-white/60 px-3 py-2 text-sm text-slate-600 transition hover:bg-white dark:border-white/10 dark:bg-slate-800/50 dark:text-slate-300 dark:hover:bg-slate-800 [&:has(:checked)]:border-indigo-500 [&:has(:checked)]:bg-indigo-50 [&:has(:checked)]:text-indigo-700 [&:has(:focus-visible)]:ring-2 [&:has(:focus-visible)]:ring-indigo-500 [&:has(:focus-visible)]:ring-offset-2 [&:has(:focus-visible)]:ring-offset-white dark:[&:has(:checked)]:border-indigo-400 dark:[&:has(:checked)]:bg-indigo-500/15 dark:[&:has(:checked)]:text-indigo-200 dark:[&:has(:focus-visible)]:ring-offset-slate-900"
                >
                  <input
                    type="radio"
                    name="bgx-palette"
                    value={p.id}
                    checked={palette === p.id}
                    onChange={() => setPalette(p.id)}
                    className="sr-only"
                  />
                  <span
                    aria-hidden="true"
                    className="h-3.5 w-3.5 flex-none rounded-full ring-1 ring-inset ring-black/10 dark:ring-white/15"
                    style={{ backgroundImage: `linear-gradient(135deg, ${p.a}, ${p.b})` }}
                  />
                  {p.label}
                </label>
              ))}
            </div>
          </fieldset>

          {/* Sliders */}
          <div className="mt-6 space-y-5">
            <RangeControl
              id="bgx-angle"
              label="Angle"
              hint="Rotates the whole field from flat to steep."
              min={0}
              max={180}
              step={1}
              value={angle}
              unit="°"
              onChange={setAngle}
            />
            <RangeControl
              id="bgx-width"
              label="Stripe width"
              hint="Sets how wide each band of colour is."
              min={12}
              max={72}
              step={1}
              value={width}
              unit="px"
              onChange={setWidth}
            />
            <RangeControl
              id="bgx-drift"
              label="Drift time"
              hint="Seconds for one full loop — higher is calmer."
              min={3}
              max={30}
              step={1}
              value={drift}
              unit="s"
              onChange={setDrift}
            />
          </div>
        </motion.div>
      </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 →