# Flow Field Background (`flow-field`)

> Curl noise vector field rendered as dense directional strokes — cursor parts the flow.

- **Docs:** https://www.mellowui.com/components/flow-field
- **Markdown:** https://www.mellowui.com/components/flow-field.md
- **Registry:** https://www.mellowui.com/r/flow-field.json
- **Tool prompt:** https://www.mellowui.com/api/prompt/flow-field
- **Categories:** background, canvas, interactive, animation
- **Dependencies:** none

## AI prompt

Add a FlowField background component from the mellow library. Dense short strokes are oriented along a slowly-evolving curl noise vector field. Moving the cursor attracts nearby strokes like parting water. Key props: strokeColor, spacing, strokeLength, speed, influenceRadius.

## Install

```bash
npx shadcn@latest add https://mellowui.com/r/flow-field.json
```

## Props

| Prop | Type | Default | Description |
|---|---|---|---|
| `strokeColor` | `string` | `"rgba(var(--ink-rgb),0.12)"` | Base color of the field strokes. |
| `spacing` | `number` | `22` | Distance between stroke origins in pixels. |
| `strokeLength` | `number` | `16` | Length of each stroke in pixels. |
| `speed` | `number` | `1` | Field evolution speed multiplier. |
| `influenceRadius` | `number` | `130` | Cursor attraction radius in pixels. |
| `className` | `string` | — | Additional CSS classes. |
| `children` | `React.ReactNode` | — | Content rendered above the canvas. |

## Design notes

- Client component (`"use client"`) — required for animation and refs.
- Uses Mellow design tokens: `--ink`, `--ink-rgb`, `--background`, `--background-rgb`, `--rule`, `--font-sans`, `--font-serif`, `--font-mono`. Tokens auto-flip between light and dark themes — never hardcode colors.
- Respects `prefers-reduced-motion: reduce` where applicable.
- Self-contained — no shared utilities. Copy-paste, not a package.

## Source

```tsx
"use client";

import React, { useEffect, useRef, useCallback, useState } from "react";

function useInkRgb(): string {
  const [rgb, setRgb] = useState<string>("244, 241, 236");
  useEffect(() => {
    const read = () => {
      const v = getComputedStyle(document.documentElement)
        .getPropertyValue("--ink-rgb")
        .trim();
      if (v) setRgb(v);
    };
    read();
    const obs = new MutationObserver(read);
    obs.observe(document.documentElement, {
      attributes: true,
      attributeFilter: ["class", "style", "data-theme"],
    });
    return () => obs.disconnect();
  }, []);
  return rgb;
}

function valueNoise(x: number, y: number): number {
  const ix = Math.floor(x);
  const iy = Math.floor(y);
  const fx = x - ix;
  const fy = y - iy;
  const ux = fx * fx * (3 - 2 * fx);
  const uy = fy * fy * (3 - 2 * fy);
  function r(xi: number, yi: number): number {
    return Math.abs(Math.sin(xi * 127.1 + yi * 311.7) * 43758.5453) % 1;
  }
  const a = r(ix, iy);
  const b = r(ix + 1, iy);
  const c = r(ix, iy + 1);
  const d = r(ix + 1, iy + 1);
  return a + (b - a) * ux + (c - a) * uy + (a - b - c + d) * ux * uy;
}

function flowAngle(x: number, y: number, t: number): number {
  const s = 0.003;
  const n1 = valueNoise(x * s, y * s + t * 0.4);
  const n2 = valueNoise(x * s + 5.2, y * s + 1.3 + t * 0.3);
  return (n1 - 0.5) * Math.PI * 4 + (n2 - 0.5) * Math.PI * 2;
}

export interface FlowFieldProps {
  strokeColor?: string;
  spacing?: number;
  strokeLength?: number;
  speed?: number;
  influenceRadius?: number;
  style?: React.CSSProperties;
  className?: string;
  children?: React.ReactNode;
}

export function FlowField({
  strokeColor,
  spacing = 22,
  strokeLength = 16,
  speed = 1,
  influenceRadius = 130,
  style,
  className,
  children,
}: FlowFieldProps) {
  const inkRgb = useInkRgb();
  const resolvedStrokeColor = strokeColor ?? `rgba(${inkRgb}, 0.12)`;
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const rafRef = useRef<number>(0);
  const timeRef = useRef(0);
  const mouseRef = useRef<{ x: number; y: number } | null>(null);
  const sizeRef = useRef({ w: 0, h: 0 });

  const draw = useCallback(
    (timestamp: number) => {
      const canvas = canvasRef.current;
      if (!canvas) return;
      const ctx = canvas.getContext("2d");
      if (!ctx) return;

      const { w: W, h: H } = sizeRef.current;
      if (!W || !H) {
        rafRef.current = requestAnimationFrame(draw);
        return;
      }

      ctx.clearRect(0, 0, canvas.width, canvas.height);

      timeRef.current += 0.004 * speed;
      const t = timeRef.current;
      const mouse = mouseRef.current;
      const half = strokeLength / 2;

      const cols = Math.ceil(W / spacing) + 1;
      const rows = Math.ceil(H / spacing) + 1;
      const offX = (W % spacing) / 2;
      const offY = (H % spacing) / 2;

      ctx.beginPath();
      ctx.strokeStyle = resolvedStrokeColor;
      ctx.lineWidth = 0.7;

      type ActiveStroke = { x: number; y: number; angle: number; inf: number };
      const active: ActiveStroke[] = [];

      for (let row = 0; row < rows; row++) {
        for (let col = 0; col < cols; col++) {
          const x = offX + col * spacing;
          const y = offY + row * spacing;

          let angle = flowAngle(x, y, t);
          let inf = 0;

          if (mouse) {
            const dx = x - mouse.x;
            const dy = y - mouse.y;
            const dist = Math.hypot(dx, dy);
            if (dist < influenceRadius) {
              inf = Math.pow(1 - dist / influenceRadius, 2);
              const attract = Math.atan2(mouse.y - y, mouse.x - x);
              let da = attract - angle;
              while (da > Math.PI) da -= 2 * Math.PI;
              while (da < -Math.PI) da += 2 * Math.PI;
              angle += da * inf * 0.75;
            }
          }

          const cx2 = Math.cos(angle);
          const cy2 = Math.sin(angle);

          if (inf > 0.02) {
            active.push({ x, y, angle, inf });
          } else {
            ctx.moveTo(x - cx2 * half, y - cy2 * half);
            ctx.lineTo(x + cx2 * half, y + cy2 * half);
          }
        }
      }
      ctx.stroke();

      for (const { x, y, angle, inf } of active) {
        const cx2 = Math.cos(angle);
        const cy2 = Math.sin(angle);
        ctx.beginPath();
        ctx.moveTo(x - cx2 * half, y - cy2 * half);
        ctx.lineTo(x + cx2 * half, y + cy2 * half);
        ctx.strokeStyle = `rgba(${inkRgb},${(0.12 + inf * 0.65).toFixed(3)})`;
        ctx.lineWidth = 0.7 + inf * 1.1;
        ctx.stroke();
      }

      rafRef.current = requestAnimationFrame(draw);
    },
    [resolvedStrokeColor, inkRgb, spacing, strokeLength, speed, influenceRadius]
  );

  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;

    const reduced = window.matchMedia(
      "(prefers-reduced-motion: reduce)"
    ).matches;

    const ro = new ResizeObserver((entries) => {
      for (const entry of entries) {
        const { width, height } = entry.contentRect;
        const dpr = Math.min(window.devicePixelRatio, 2);
        sizeRef.current = { w: width, h: height };
        canvas.width = width * dpr;
        canvas.height = height * dpr;
        const ctx = canvas.getContext("2d");
        if (ctx) ctx.scale(dpr, dpr);
        canvas.style.width = `${width}px`;
        canvas.style.height = `${height}px`;
      }
    });
    ro.observe(canvas.parentElement!);

    const onMouseMove = (e: MouseEvent) => {
      if (reduced) return;
      const rect = canvas.getBoundingClientRect();
      mouseRef.current = { x: e.clientX - rect.left, y: e.clientY - rect.top };
    };
    const onMouseLeave = () => {
      mouseRef.current = null;
    };

    canvas.parentElement?.addEventListener("mousemove", onMouseMove);
    canvas.parentElement?.addEventListener("mouseleave", onMouseLeave);

    if (!reduced) {
      rafRef.current = requestAnimationFrame(draw);
    }

    return () => {
      cancelAnimationFrame(rafRef.current);
      ro.disconnect();
      canvas.parentElement?.removeEventListener("mousemove", onMouseMove);
      canvas.parentElement?.removeEventListener("mouseleave", onMouseLeave);
    };
  }, [draw]);

  return (
    <div
      className={["relative overflow-hidden", className].filter(Boolean).join(" ")}
      style={style}
    >
      <canvas
        ref={canvasRef}
        aria-hidden="true"
        className="absolute inset-0 w-full h-full pointer-events-none"
      />
      {children && (
        <div className="absolute inset-0 z-1">{children}</div>
      )}
    </div>
  );
}

export default FlowField;

```

## Demo

```tsx
"use client";

import React from "react";
import { FlowField } from "../mellow/flow-field";

export default function FlowFieldDemo() {
  return (
    <FlowField className="w-full h-full">
      <div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 flex flex-col items-center gap-2 text-center">
        <p className="text-[rgba(var(--ink-rgb),0.55)] text-sm">
          Curl noise vector field — strokes follow the flow
        </p>
        <p className="text-[rgba(var(--ink-rgb),0.3)] text-xs">
          Move cursor to part the field
        </p>
      </div>
    </FlowField>
  );
}

```
