Web InnoventixFreeCode

Section Divider

Original · free

section wave/angle divider

byWeb InnoventixReact + Tailwind
divsectiondividers
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/div-section.json
div-section.tsx
"use client";

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

type Shape = "wave" | "curve" | "angle" | "peaks" | "chevron";

const SHAPES: { id: Shape; label: string; hint: string }[] = [
  { id: "wave", label: "Wave", hint: "Twin-crest sine curve" },
  { id: "curve", label: "Curve", hint: "Single soft arc" },
  { id: "angle", label: "Angle", hint: "Diagonal tilt" },
  { id: "peaks", label: "Peaks", hint: "Repeating ridges" },
  { id: "chevron", label: "Chevron", hint: "One centred point" },
];

const W = 1200;

function r(x: number): number {
  return Math.round(x * 10) / 10;
}

function genPath(shape: Shape, w: number, h: number, a: number, invert: boolean): string {
  const mid = h / 2;
  const iv = (y: number): number => r(invert ? h - y : y);
  const x = (p: number): number => r(w * p);
  switch (shape) {
    case "wave":
      return `M0 ${iv(mid)} C ${x(0.16)} ${iv(mid - a * 1.25)} ${x(0.34)} ${iv(mid + a * 1.25)} ${x(0.5)} ${iv(mid)} C ${x(0.66)} ${iv(mid - a * 1.25)} ${x(0.84)} ${iv(mid + a * 1.25)} ${w} ${iv(mid)} L ${w} ${h} L 0 ${h} Z`;
    case "curve":
      return `M0 ${iv(mid + a * 0.75)} Q ${x(0.5)} ${iv(mid - a * 1.7)} ${w} ${iv(mid + a * 0.75)} L ${w} ${h} L 0 ${h} Z`;
    case "angle":
      return `M0 ${iv(mid - a)} L ${w} ${iv(mid + a)} L ${w} ${h} L 0 ${h} Z`;
    case "peaks":
      return `M0 ${iv(mid + a)} L ${x(1 / 6)} ${iv(mid - a)} L ${x(2 / 6)} ${iv(mid + a)} L ${x(3 / 6)} ${iv(mid - a)} L ${x(4 / 6)} ${iv(mid + a)} L ${x(5 / 6)} ${iv(mid - a)} L ${w} ${iv(mid + a)} L ${w} ${h} L 0 ${h} Z`;
    case "chevron":
      return `M0 ${iv(mid + a)} L ${x(0.5)} ${iv(mid - a)} L ${w} ${iv(mid + a)} L ${w} ${h} L 0 ${h} Z`;
    default:
      return "";
  }
}

function tileWave(w2: number, h: number, a: number, half: number): string {
  const mid = h / 2;
  let d = `M0 ${r(mid)}`;
  let up = true;
  for (let px = 0; px < w2; px += half) {
    const cy = up ? mid - a : mid + a;
    d += ` Q ${r(px + half / 2)} ${r(cy)} ${r(px + half)} ${r(mid)}`;
    up = !up;
  }
  return d;
}

export default function SectionDividerStudio() {
  const reduced = useReducedMotion();
  const uid = useId();
  const radioName = `${uid}-shape`;
  const heightId = `${uid}-height`;
  const depthId = `${uid}-depth`;

  const [shape, setShape] = useState<Shape>("wave");
  const [height, setHeight] = useState<number>(120);
  const [depth, setDepth] = useState<number>(62);
  const [flipH, setFlipH] = useState<boolean>(false);
  const [invert, setInvert] = useState<boolean>(false);
  const [flow, setFlow] = useState<boolean>(true);
  const [copied, setCopied] = useState<boolean>(false);
  const copyTimer = useRef<number | null>(null);

  const a = useMemo(() => (depth / 100) * (height / 2) * 0.92, [depth, height]);
  const path = useMemo(() => genPath(shape, W, height, a, invert), [shape, height, a, invert]);
  const overlayPath = useMemo(() => tileWave(2400, height, Math.min(a, height * 0.3), 300), [height, a]);

  const svgSnippet = useMemo(
    () =>
      `<svg viewBox="0 0 ${W} ${height}" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg" style="display:block;width:100%;height:${height}px">\n  <path d="${path}" fill="currentColor" />\n</svg>`,
    [path, height],
  );

  const copy = async (): Promise<void> => {
    try {
      await navigator.clipboard.writeText(svgSnippet);
      setCopied(true);
      if (copyTimer.current !== null) window.clearTimeout(copyTimer.current);
      copyTimer.current = window.setTimeout(() => setCopied(false), 1800);
    } catch {
      setCopied(false);
    }
  };

  const pillTransition = reduced
    ? { duration: 0 }
    : ({ type: "spring", stiffness: 380, damping: 30 } as const);

  return (
    <section className="relative w-full overflow-hidden bg-slate-50 px-4 py-16 text-slate-900 sm:px-6 sm:py-24 dark:bg-slate-950 dark:text-slate-100">
      <style>{`
        @keyframes fcdiv-drift { from { transform: translateX(0); } to { transform: translateX(-25%); } }
        @keyframes fcdiv-rise { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
        .fcdiv-anim { animation: fcdiv-drift 9s linear infinite; }
        .fcdiv-rise { animation: fcdiv-rise 0.5s ease-out both; }
        @media (prefers-reduced-motion: reduce) {
          .fcdiv-anim, .fcdiv-rise { animation: none !important; }
        }
      `}</style>

      <div
        aria-hidden="true"
        className="pointer-events-none absolute -top-24 left-1/2 h-72 w-[36rem] -translate-x-1/2 rounded-full bg-violet-300/30 blur-3xl dark:bg-violet-600/10"
      />

      <div className="relative mx-auto max-w-5xl">
        <header className="mx-auto max-w-2xl text-center">
          <span className="inline-flex items-center gap-2 rounded-full border border-indigo-200 bg-white px-3 py-1 text-xs font-semibold uppercase tracking-widest text-indigo-600 dark:border-indigo-500/30 dark:bg-slate-900 dark:text-indigo-300">
            <span className="h-1.5 w-1.5 rounded-full bg-indigo-500" />
            Section dividers
          </span>
          <h2 className="mt-5 text-balance text-3xl font-bold tracking-tight sm:text-4xl">
            Melt one section into the next.
          </h2>
          <p className="mt-4 text-pretty text-base leading-relaxed text-slate-600 dark:text-slate-400">
            Everything below the crest belongs to the next band. Pick a shape, tune the depth,
            mirror or flip the crest, then copy a self-contained SVG that stretches edge-to-edge.
          </p>
        </header>

        {/* Live preview */}
        <div className="mt-10 overflow-hidden rounded-3xl border border-slate-200 shadow-xl shadow-slate-900/5 dark:border-slate-800 dark:shadow-black/30">
          <div
            className="relative isolate bg-gradient-to-br from-indigo-600 via-violet-600 to-indigo-700 px-6 sm:px-10"
            style={{ paddingTop: 40, paddingBottom: height + 32 }}
          >
            <p className="text-xs font-semibold uppercase tracking-widest text-indigo-200">
              Upper band
            </p>
            <h3 className="mt-2 max-w-md text-2xl font-bold text-white sm:text-3xl">
              A hero that doesn&rsquo;t end in a straight line.
            </h3>
            <p className="mt-3 max-w-sm text-sm leading-relaxed text-indigo-100">
              The divider is drawn in the next band&rsquo;s colour, so the seam disappears no matter
              which theme the viewer is in.
            </p>

            {/* Divider */}
            <div
              className="absolute inset-x-0 bottom-0 overflow-hidden text-slate-50 dark:text-slate-900"
              style={{ height, transform: flipH ? "scaleX(-1)" : undefined }}
            >
              <svg
                viewBox={`0 0 ${W} ${height}`}
                preserveAspectRatio="none"
                className="block h-full w-full"
                role="img"
                aria-label={`${shape} section divider`}
              >
                <path d={path} fill="currentColor" />
              </svg>
              {flow ? (
                <svg
                  aria-hidden="true"
                  viewBox={`0 0 2400 ${height}`}
                  preserveAspectRatio="none"
                  className={`pointer-events-none absolute inset-0 h-full text-sky-300/70 dark:text-sky-400/50 ${reduced ? "" : "fcdiv-anim"}`}
                  style={{ width: "200%", maxWidth: "none" }}
                >
                  <path
                    d={overlayPath}
                    fill="none"
                    stroke="currentColor"
                    strokeWidth={3}
                    strokeLinecap="round"
                  />
                </svg>
              ) : null}
            </div>
          </div>

          <div className="bg-slate-50 px-6 py-8 sm:px-10 sm:py-10 dark:bg-slate-900">
            <p className="text-xs font-semibold uppercase tracking-widest text-slate-500 dark:text-slate-400">
              Lower band
            </p>
            <div className="mt-4 grid gap-4 sm:grid-cols-3">
              <Feature
                title="Edge-to-edge"
                body="preserveAspectRatio “none” stretches the path across any width, no math."
                icon={
                  <path
                    d="M3 12h18M3 12l4-4M3 12l4 4M21 12l-4-4M21 12l-4 4"
                    fill="none"
                    stroke="currentColor"
                    strokeWidth={1.75}
                    strokeLinecap="round"
                    strokeLinejoin="round"
                  />
                }
              />
              <Feature
                title="Theme-aware"
                body="The fill is currentColor, so the crest inherits whatever section sits below."
                icon={
                  <path
                    d="M12 3a9 9 0 1 0 9 9 7 7 0 0 1-9-9Z"
                    fill="none"
                    stroke="currentColor"
                    strokeWidth={1.75}
                    strokeLinecap="round"
                    strokeLinejoin="round"
                  />
                }
              />
              <Feature
                title="Zero deps"
                body="Pure inline SVG. No images, no plugins, nothing to download or ship."
                icon={
                  <path
                    d="M12 2 3 7v10l9 5 9-5V7l-9-5Zm0 0v20M3 7l9 5 9-5"
                    fill="none"
                    stroke="currentColor"
                    strokeWidth={1.75}
                    strokeLinecap="round"
                    strokeLinejoin="round"
                  />
                }
              />
            </div>
          </div>
        </div>

        {/* Controls */}
        <div className="mt-6 grid gap-6 rounded-3xl border border-slate-200 bg-white p-6 sm:p-8 md:grid-cols-2 dark:border-slate-800 dark:bg-slate-900">
          <div className="space-y-6">
            <fieldset>
              <legend className="text-sm font-semibold text-slate-700 dark:text-slate-200">
                Divider shape
              </legend>
              <div className="mt-3 grid grid-cols-3 gap-2 sm:grid-cols-5">
                {SHAPES.map((s) => {
                  const active = shape === s.id;
                  return (
                    <label key={s.id} className="relative cursor-pointer" title={s.hint}>
                      <input
                        type="radio"
                        name={radioName}
                        value={s.id}
                        checked={active}
                        onChange={() => setShape(s.id)}
                        className="peer sr-only"
                      />
                      {active ? (
                        <motion.span
                          layoutId={`${uid}-shape-pill`}
                          transition={pillTransition}
                          className="absolute inset-0 rounded-xl bg-indigo-50 ring-2 ring-indigo-500 dark:bg-indigo-500/10"
                        />
                      ) : null}
                      <span className="relative z-10 flex flex-col items-center gap-1.5 rounded-xl border border-slate-200 px-2 py-2.5 transition-colors peer-hover:border-slate-300 peer-focus-visible:outline-none peer-focus-visible:ring-2 peer-focus-visible:ring-indigo-500 peer-focus-visible:ring-offset-2 peer-focus-visible:ring-offset-white dark:border-slate-700 dark:peer-hover:border-slate-600 dark:peer-focus-visible:ring-offset-slate-900">
                        <svg
                          viewBox="0 0 56 22"
                          preserveAspectRatio="none"
                          className={`h-5 w-14 ${active ? "text-indigo-600 dark:text-indigo-400" : "text-slate-400 dark:text-slate-500"}`}
                          aria-hidden="true"
                        >
                          <path d={genPath(s.id, 56, 22, 6, false)} fill="currentColor" />
                        </svg>
                        <span
                          className={`text-xs font-medium ${active ? "text-indigo-700 dark:text-indigo-300" : "text-slate-600 dark:text-slate-300"}`}
                        >
                          {s.label}
                        </span>
                      </span>
                    </label>
                  );
                })}
              </div>
            </fieldset>

            <div>
              <p className="text-sm font-semibold text-slate-700 dark:text-slate-200">Transform</p>
              <div className="mt-3 flex flex-wrap gap-2">
                <Toggle
                  active={flipH}
                  onClick={() => setFlipH((v) => !v)}
                  label="Flip crest"
                  icon={
                    <path
                      d="M12 3v18M7 8 3 12l4 4M17 8l4 4-4 4"
                      fill="none"
                      stroke="currentColor"
                      strokeWidth={1.75}
                      strokeLinecap="round"
                      strokeLinejoin="round"
                    />
                  }
                />
                <Toggle
                  active={invert}
                  onClick={() => setInvert((v) => !v)}
                  label="Invert"
                  icon={
                    <path
                      d="M3 12h18M8 7 12 3l4 4M8 17l4 4 4-4"
                      fill="none"
                      stroke="currentColor"
                      strokeWidth={1.75}
                      strokeLinecap="round"
                      strokeLinejoin="round"
                    />
                  }
                />
                <Toggle
                  active={flow}
                  onClick={() => setFlow((v) => !v)}
                  label="Animate flow"
                  icon={
                    <path
                      d="M3 12c2-3 4-3 6 0s4 3 6 0 4-3 6 0"
                      fill="none"
                      stroke="currentColor"
                      strokeWidth={1.75}
                      strokeLinecap="round"
                      strokeLinejoin="round"
                    />
                  }
                />
              </div>
            </div>
          </div>

          <div className="space-y-6">
            <Slider
              id={heightId}
              label="Height"
              value={height}
              min={48}
              max={180}
              step={4}
              unit="px"
              onChange={(v) => setHeight(v)}
            />
            <Slider
              id={depthId}
              label="Crest depth"
              value={depth}
              min={8}
              max={100}
              step={2}
              unit="%"
              onChange={(v) => setDepth(v)}
            />

            <div>
              <div className="flex items-center justify-between">
                <p className="text-sm font-semibold text-slate-700 dark:text-slate-200">
                  Output SVG
                </p>
                <button
                  type="button"
                  onClick={copy}
                  className="inline-flex items-center gap-1.5 rounded-lg bg-indigo-600 px-3 py-1.5 text-xs font-semibold text-white transition-colors hover:bg-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:focus-visible:ring-offset-slate-900"
                >
                  {copied ? (
                    <>
                      <svg
                        viewBox="0 0 24 24"
                        className="h-3.5 w-3.5 text-emerald-300"
                        aria-hidden="true"
                      >
                        <path
                          d="m5 12 5 5 9-11"
                          fill="none"
                          stroke="currentColor"
                          strokeWidth={2.25}
                          strokeLinecap="round"
                          strokeLinejoin="round"
                        />
                      </svg>
                      Copied
                    </>
                  ) : (
                    <>
                      <svg viewBox="0 0 24 24" className="h-3.5 w-3.5" aria-hidden="true">
                        <path
                          d="M9 9V5a1 1 0 0 1 1-1h9a1 1 0 0 1 1 1v9a1 1 0 0 1-1 1h-4M4 9h9a1 1 0 0 1 1 1v9a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-9a1 1 0 0 1 1-1Z"
                          fill="none"
                          stroke="currentColor"
                          strokeWidth={1.75}
                          strokeLinecap="round"
                          strokeLinejoin="round"
                        />
                      </svg>
                      Copy SVG
                    </>
                  )}
                </button>
              </div>
              <pre className="mt-3 max-h-32 overflow-auto rounded-xl border border-slate-200 bg-slate-50 p-3 text-[11px] leading-relaxed text-slate-700 dark:border-slate-700 dark:bg-slate-950 dark:text-slate-300">
                <code>{svgSnippet}</code>
              </pre>
              <span aria-live="polite" className="sr-only">
                {copied ? "SVG copied to clipboard" : ""}
              </span>
            </div>
          </div>
        </div>
      </div>

      <AnimatePresence>
        {copied ? (
          <motion.div
            initial={{ opacity: 0, y: reduced ? 0 : 12 }}
            animate={{ opacity: 1, y: 0 }}
            exit={{ opacity: 0, y: reduced ? 0 : 12 }}
            transition={{ duration: reduced ? 0 : 0.2 }}
            className="pointer-events-none fixed inset-x-0 bottom-6 z-50 mx-auto flex w-fit items-center gap-2 rounded-full bg-slate-900 px-4 py-2 text-sm font-medium text-white shadow-lg dark:bg-white dark:text-slate-900"
            role="status"
          >
            <svg viewBox="0 0 24 24" className="h-4 w-4 text-emerald-400" aria-hidden="true">
              <path
                d="m5 12 5 5 9-11"
                fill="none"
                stroke="currentColor"
                strokeWidth={2.25}
                strokeLinecap="round"
                strokeLinejoin="round"
              />
            </svg>
            SVG copied to clipboard
          </motion.div>
        ) : null}
      </AnimatePresence>
    </section>
  );
}

function Feature({
  title,
  body,
  icon,
}: {
  title: string;
  body: string;
  icon: ReactNode;
}) {
  return (
    <div className="rounded-2xl border border-slate-200 bg-white p-4 dark:border-slate-800 dark:bg-slate-950">
      <span className="flex h-9 w-9 items-center justify-center rounded-lg bg-indigo-50 text-indigo-600 dark:bg-indigo-500/10 dark:text-indigo-400">
        <svg viewBox="0 0 24 24" className="h-5 w-5" aria-hidden="true">
          {icon}
        </svg>
      </span>
      <h4 className="mt-3 text-sm font-semibold text-slate-800 dark:text-slate-100">{title}</h4>
      <p className="mt-1 text-sm leading-relaxed text-slate-600 dark:text-slate-400">{body}</p>
    </div>
  );
}

function Toggle({
  active,
  onClick,
  label,
  icon,
}: {
  active: boolean;
  onClick: () => void;
  label: string;
  icon: ReactNode;
}) {
  return (
    <button
      type="button"
      onClick={onClick}
      aria-pressed={active}
      className={`inline-flex items-center gap-2 rounded-xl border px-3 py-2 text-sm font-medium transition-colors 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-slate-900 ${
        active
          ? "border-indigo-500 bg-indigo-50 text-indigo-700 dark:border-indigo-500/60 dark:bg-indigo-500/10 dark:text-indigo-300"
          : "border-slate-200 bg-white text-slate-600 hover:border-slate-300 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-300 dark:hover:border-slate-600"
      }`}
    >
      <svg viewBox="0 0 24 24" className="h-4 w-4" aria-hidden="true">
        {icon}
      </svg>
      {label}
    </button>
  );
}

function Slider({
  id,
  label,
  value,
  min,
  max,
  step,
  unit,
  onChange,
}: {
  id: string;
  label: string;
  value: number;
  min: number;
  max: number;
  step: number;
  unit: string;
  onChange: (v: number) => void;
}) {
  return (
    <div>
      <div className="flex items-center justify-between">
        <label htmlFor={id} className="text-sm font-semibold text-slate-700 dark:text-slate-200">
          {label}
        </label>
        <span className="rounded-md bg-slate-100 px-2 py-0.5 font-mono text-xs 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}
        onChange={(e) => onChange(Number(e.currentTarget.value))}
        className="mt-3 h-2 w-full cursor-pointer appearance-none rounded-full bg-slate-200 accent-indigo-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:bg-slate-700 dark:focus-visible:ring-offset-slate-900"
      />
    </div>
  );
}

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 →