Web InnoventixFreeCode

Tilted Marquee

Original · free

tilted marquee band

byWeb InnoventixReact + Tailwind
mqxtiltedmarquees
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/mqx-tilted.json
mqx-tilted.tsx
"use client";

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

const BAND_A = [
  "Design Systems",
  "Framer Motion",
  "Headless Commerce",
  "Edge Rendering",
  "Type & Color",
  "Accessibility First",
  "Rapid Prototyping",
  "Design Tokens",
  "Interaction Design",
] as const;

const BAND_B = [
  "React 19",
  "TypeScript",
  "Tailwind CSS",
  "Motion",
  "GSAP",
  "Vite",
  "Storybook",
  "Playwright",
  "Vitest",
  "Radix UI",
] as const;

const FOCUS_RING =
  "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-neutral-950";

function StarIcon({ className }: { className?: string }) {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="currentColor"
      aria-hidden="true"
      className={className}
    >
      <path d="M12 2.6l1.9 5.9a1 1 0 0 0 .95.69h6.2l-5.02 3.65a1 1 0 0 0-.36 1.12l1.9 5.9-5.02-3.64a1 1 0 0 0-1.18 0L7.25 19.86l1.9-5.9a1 1 0 0 0-.36-1.12L3.77 9.19h6.2a1 1 0 0 0 .95-.69L12 2.6z" />
    </svg>
  );
}

function PlayIcon({ className }: { className?: string }) {
  return (
    <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" className={className}>
      <path d="M8 5.14v13.72a1 1 0 0 0 1.53.85l10.5-6.86a1 1 0 0 0 0-1.7L9.53 4.29A1 1 0 0 0 8 5.14z" />
    </svg>
  );
}

function PauseIcon({ className }: { className?: string }) {
  return (
    <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" className={className}>
      <path d="M7 4.5A1.5 1.5 0 0 1 8.5 6v12A1.5 1.5 0 0 1 5.5 18V6A1.5 1.5 0 0 1 7 4.5zM17 4.5A1.5 1.5 0 0 1 18.5 6v12A1.5 1.5 0 0 1 15.5 18V6A1.5 1.5 0 0 1 17 4.5z" />
    </svg>
  );
}

function SwapIcon({ className }: { className?: string }) {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth="2"
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
      className={className}
    >
      <path d="M7 7h13l-3.5-3.5M17 17H4l3.5 3.5" />
    </svg>
  );
}

function TiltIcon({ className }: { className?: string }) {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth="2"
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
      className={className}
    >
      <path d="M3 15l18-6M5 18l14-4.7" />
    </svg>
  );
}

function Switch({
  checked,
  onChange,
  children,
}: {
  checked: boolean;
  onChange: (next: boolean) => void;
  children: ReactNode;
}) {
  return (
    <button
      type="button"
      role="switch"
      aria-checked={checked}
      onClick={() => onChange(!checked)}
      className={
        "group inline-flex items-center gap-2.5 rounded-full px-1 py-1 text-sm font-medium text-slate-700 transition-colors dark:text-neutral-200 " +
        FOCUS_RING
      }
    >
      <span
        className={
          "relative h-6 w-11 shrink-0 rounded-full transition-colors duration-200 " +
          (checked
            ? "bg-indigo-600 dark:bg-indigo-500"
            : "bg-slate-300 dark:bg-neutral-700")
        }
      >
        <span
          className={
            "absolute top-0.5 left-0.5 h-5 w-5 rounded-full bg-white shadow-sm transition-transform duration-200 " +
            (checked ? "translate-x-5" : "translate-x-0")
          }
        />
      </span>
      <span>{children}</span>
    </button>
  );
}

interface BandProps {
  items: readonly string[];
  variant: "solid" | "outline";
  reverse: boolean;
  durationSec: number;
  playing: boolean;
  pauseOnHover: boolean;
  reduced: boolean;
  label: string;
}

function Group({
  items,
  variant,
  hidden,
}: {
  items: readonly string[];
  variant: "solid" | "outline";
  hidden?: boolean;
}) {
  return (
    <ul
      aria-hidden={hidden || undefined}
      className="mqxtilted-group flex shrink-0 items-center"
    >
      {items.map((label, i) => (
        <li key={`${label}-${i}`} className="shrink-0">
          {variant === "solid" ? (
            <span className="flex items-center">
              <span className="px-5 text-2xl font-semibold uppercase tracking-tight text-white md:px-8 md:text-4xl">
                {label}
              </span>
              <StarIcon className="h-4 w-4 shrink-0 text-white/55 md:h-5 md:w-5" />
            </span>
          ) : (
            <span className="flex items-center pr-3 md:pr-4">
              <span className="rounded-full border border-emerald-300/80 bg-white/80 px-4 py-1.5 text-sm font-medium text-emerald-800 shadow-sm dark:border-emerald-400/30 dark:bg-emerald-500/10 dark:text-emerald-200 md:text-base">
                {label}
              </span>
            </span>
          )}
        </li>
      ))}
    </ul>
  );
}

function Band({
  items,
  variant,
  reverse,
  durationSec,
  playing,
  pauseOnHover,
  reduced,
  label,
}: BandProps) {
  const [hovered, setHovered] = useState(false);
  const paused = reduced || !playing || (pauseOnHover && hovered);

  const shell =
    variant === "solid"
      ? "bg-gradient-to-r from-indigo-600 via-violet-600 to-indigo-600 py-4 ring-1 ring-black/10 dark:from-indigo-600 dark:via-violet-600 dark:to-indigo-700 md:py-5"
      : "border-y border-slate-200 bg-slate-100/80 py-3 ring-1 ring-slate-200 backdrop-blur-sm dark:border-neutral-800 dark:bg-neutral-900/70 dark:ring-neutral-800 md:py-4";

  return (
    <div
      className={"mqxtilted-viewport overflow-hidden shadow-lg " + shell}
      role="group"
      aria-label={label}
      onMouseEnter={() => setHovered(true)}
      onMouseLeave={() => setHovered(false)}
    >
      <div
        className="mqxtilted-track flex w-max items-center"
        style={{
          animationDuration: `${durationSec}s`,
          animationDirection: reverse ? "reverse" : "normal",
          animationPlayState: paused ? "paused" : "running",
        }}
      >
        <Group items={items} variant={variant} />
        <Group items={items} variant={variant} hidden />
      </div>
    </div>
  );
}

export default function MqxTilted() {
  const prefersReduced = useReducedMotion();
  const reduced = prefersReduced === true;

  const [playing, setPlaying] = useState(true);
  const [reversed, setReversed] = useState(false);
  const [pauseOnHover, setPauseOnHover] = useState(true);
  const [tilted, setTilted] = useState(true);
  const [speed, setSpeed] = useState(1);

  const speedId = useId();
  const headingId = useId();

  const durA = Math.round((42 / speed) * 100) / 100;
  const durB = Math.round((32 / speed) * 100) / 100;

  const statusText = reduced
    ? "Motion reduced by system preference"
    : `${playing ? "Playing" : "Paused"} at ${speed.toFixed(2)}× speed`;

  return (
    <section
      aria-labelledby={headingId}
      className="relative w-full overflow-hidden bg-slate-50 px-5 py-20 text-slate-900 sm:px-8 md:py-28 dark:bg-neutral-950 dark:text-neutral-100"
    >
      <style>{`
        @keyframes mqxtilted-marquee {
          from { transform: translateX(0); }
          to   { transform: translateX(-50%); }
        }
        .mqxtilted-track {
          animation-name: mqxtilted-marquee;
          animation-timing-function: linear;
          animation-iteration-count: infinite;
          will-change: transform;
        }
        @media (prefers-reduced-motion: reduce) {
          .mqxtilted-track {
            animation: none !important;
            transform: none !important;
          }
        }
        .mqxtilted-range::-webkit-slider-thumb {
          -webkit-appearance: none;
          appearance: none;
          height: 1.1rem;
          width: 1.1rem;
          border-radius: 9999px;
          background: #6366f1;
          border: 2px solid #ffffff;
          box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);
          cursor: pointer;
        }
        .mqxtilted-range::-moz-range-thumb {
          height: 1.1rem;
          width: 1.1rem;
          border-radius: 9999px;
          background: #6366f1;
          border: 2px solid #ffffff;
          box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);
          cursor: pointer;
        }
      `}</style>

      {/* decorative backdrop */}
      <div
        aria-hidden="true"
        className="pointer-events-none absolute inset-0 -z-10 bg-[radial-gradient(60%_50%_at_50%_0%,rgba(99,102,241,0.12),transparent_70%)] dark:bg-[radial-gradient(60%_50%_at_50%_0%,rgba(99,102,241,0.16),transparent_70%)]"
      />

      <div className="relative z-10 mx-auto max-w-5xl">
        <header className="mx-auto max-w-2xl text-center">
          <motion.p
            initial={reduced ? false : { opacity: 0, y: 12 }}
            whileInView={reduced ? undefined : { opacity: 1, y: 0 }}
            viewport={{ once: true, margin: "-80px" }}
            transition={{ duration: 0.5 }}
            className="inline-flex items-center gap-2 rounded-full border border-slate-200 bg-white px-3 py-1 text-xs font-semibold uppercase tracking-[0.18em] text-indigo-700 dark:border-neutral-800 dark:bg-neutral-900 dark:text-indigo-300"
          >
            <span className="h-1.5 w-1.5 rounded-full bg-indigo-500" />
            Studio Capabilities
          </motion.p>
          <motion.h2
            id={headingId}
            initial={reduced ? false : { opacity: 0, y: 16 }}
            whileInView={reduced ? undefined : { opacity: 1, y: 0 }}
            viewport={{ once: true, margin: "-80px" }}
            transition={{ duration: 0.55, delay: 0.05 }}
            className="mt-5 text-3xl font-semibold tracking-tight text-slate-900 sm:text-4xl md:text-5xl dark:text-white"
          >
            A studio, always in motion
          </motion.h2>
          <motion.p
            initial={reduced ? false : { opacity: 0, y: 16 }}
            whileInView={reduced ? undefined : { opacity: 1, y: 0 }}
            viewport={{ once: true, margin: "-80px" }}
            transition={{ duration: 0.55, delay: 0.1 }}
            className="mt-4 text-base leading-relaxed text-slate-600 md:text-lg dark:text-neutral-400"
          >
            Two crossing bands of everything we ship, tilted just off-axis. Take
            the controls: play it, slow it down, flip its direction, or set it
            straight.
          </motion.p>
        </header>

        {/* Controls */}
        <div className="mx-auto mt-10 flex max-w-3xl flex-col gap-5 rounded-2xl border border-slate-200 bg-white/80 p-5 shadow-sm backdrop-blur-sm dark:border-neutral-800 dark:bg-neutral-900/70 sm:p-6">
          <div className="flex flex-wrap items-center gap-3">
            <button
              type="button"
              aria-pressed={playing}
              onClick={() => setPlaying((p) => !p)}
              className={
                "inline-flex items-center gap-2 rounded-xl bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm transition-colors hover:bg-indigo-700 active:bg-indigo-800 dark:bg-indigo-500 dark:hover:bg-indigo-400 " +
                FOCUS_RING
              }
            >
              {playing ? (
                <PauseIcon className="h-4 w-4" />
              ) : (
                <PlayIcon className="h-4 w-4" />
              )}
              {playing ? "Pause" : "Play"}
            </button>

            <button
              type="button"
              aria-pressed={reversed}
              onClick={() => setReversed((r) => !r)}
              className={
                "inline-flex items-center gap-2 rounded-xl border border-slate-300 bg-white px-4 py-2.5 text-sm font-semibold text-slate-700 transition-colors hover:bg-slate-50 dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-100 dark:hover:bg-neutral-700 " +
                FOCUS_RING
              }
            >
              <SwapIcon className="h-4 w-4" />
              {reversed ? "Reversed" : "Reverse"}
            </button>

            <div className="ml-auto">
              <span
                aria-live="polite"
                className="inline-flex items-center gap-2 rounded-lg border border-slate-200 bg-slate-50 px-3 py-1.5 text-xs font-medium text-slate-600 dark:border-neutral-800 dark:bg-neutral-800/60 dark:text-neutral-300"
              >
                <span
                  className={
                    "h-2 w-2 rounded-full " +
                    (reduced
                      ? "bg-amber-500"
                      : playing
                        ? "bg-emerald-500"
                        : "bg-slate-400 dark:bg-neutral-500")
                  }
                />
                {statusText}
              </span>
            </div>
          </div>

          <div className="grid gap-5 sm:grid-cols-[1fr_auto_auto] sm:items-center">
            <div className="flex items-center gap-3">
              <label
                htmlFor={speedId}
                className="shrink-0 text-sm font-medium text-slate-700 dark:text-neutral-200"
              >
                Speed
              </label>
              <input
                id={speedId}
                type="range"
                min={0.25}
                max={2}
                step={0.25}
                value={speed}
                onChange={(e) => setSpeed(Number(e.target.value))}
                aria-valuetext={`${speed.toFixed(2)} times`}
                className={
                  "mqxtilted-range h-2 w-full cursor-pointer appearance-none rounded-full bg-slate-200 accent-indigo-600 dark:bg-neutral-700 dark:accent-indigo-400 " +
                  FOCUS_RING
                }
              />
              <span className="w-12 shrink-0 text-right text-sm font-semibold tabular-nums text-indigo-700 dark:text-indigo-300">
                {speed.toFixed(2)}
                {"×"}
              </span>
            </div>

            <div className="hidden h-6 w-px bg-slate-200 dark:bg-neutral-800 sm:block" />

            <div className="flex flex-wrap items-center gap-x-6 gap-y-3">
              <Switch checked={pauseOnHover} onChange={setPauseOnHover}>
                Pause on hover
              </Switch>
              <Switch checked={tilted} onChange={setTilted}>
                <span className="inline-flex items-center gap-1.5">
                  <TiltIcon className="h-4 w-4 text-slate-500 dark:text-neutral-400" />
                  Tilt
                </span>
              </Switch>
            </div>
          </div>
        </div>
      </div>

      {/* Tilted marquee band */}
      <div className="relative mt-14 md:mt-16">
        <motion.div
          initial={false}
          animate={{ rotate: tilted ? -4 : 0 }}
          transition={
            reduced
              ? { duration: 0 }
              : { type: "spring", stiffness: 120, damping: 18 }
          }
          style={{ width: "118%", marginLeft: "-9%" }}
          className="origin-center"
        >
          <div className="flex flex-col gap-3 md:gap-4">
            <Band
              items={BAND_A}
              variant="solid"
              reverse={reversed}
              durationSec={durA}
              playing={playing}
              pauseOnHover={pauseOnHover}
              reduced={reduced}
              label="Design and product capabilities"
            />
            <Band
              items={BAND_B}
              variant="outline"
              reverse={!reversed}
              durationSec={durB}
              playing={playing}
              pauseOnHover={pauseOnHover}
              reduced={reduced}
              label="Engineering stack"
            />
          </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 →