Skip to content

FreeIn reviewv1.0.0

Flow Lines

Dozens of thin lines of light riding one slow wave, bunching where they cross, with a glow that follows the pointer.
npx shadcn@latest add @vibebox/flow-lines

Controls

1
1
<FlowLines />

About

A loop over up to sixty-four lines. Each line is the curve y = f(x) of three sines with a phase that grows down the stack, its height set by a perspective-like depth so far lines are higher and finer; the distance from the pixel to the curve gives a Gaussian core and a wider soft halo. Colour comes from a three-stop ramp by the line's index. The pointer draws the nearest lines toward its height with a Gaussian in x. Everything is additive, so crossings brighten on their own.

Use it when

  • A software, fintech or data brand wants a calm, premium abstract hero.
  • A beauty or fashion drop wants silk-thread colour behind a headline.
  • A deck or report cover wants lines that suggest movement without noise.

Skip it when

  • The brand is warm and tactile; use Satin or Liquid Ink.
  • The page needs a literal place: use Ocean, Terrain or City Night.

Install

  1. 1Add the VibeBox registry to components.json:
    {
      "registries": {
        "@vibebox": "https://motion.vibeboxph.com/r/{name}.json"
      }
    }
  2. 2Add the component with the shadcn CLI:
    npx shadcn@latest add @vibebox/flow-lines
  3. 3Use it:
    import { FlowLines } from "@/components/vibebox/flow-lines";
    
    <FlowLines preset="aurora" className="min-h-[560px] rounded-xl">
      <div className="p-12 text-white">
        <h1 className="text-4xl font-semibold">Data that moves like this.</h1>
      </div>
    </FlowLines>

Code

3 files
components/vibebox/flow-lines/flow-lines.client.tsx
/**
 * VibeBox Motion · Flow Lines
 * Copyright (c) 2026 VibeBox. Licensed under the VibeBox Motion Free License:
 * https://motion.vibeboxph.com/license
 * Use it in unlimited personal, commercial and client projects.
 * Do not sell or redistribute it as a component, kit, template or builder.
 */
"use client";

import type { ReactNode } from "react";
import { BackdropCanvas, type PosterSpec } from "@/lib/vibebox/backdrop-canvas";
import { flowLinesPiece, presets, type FlowLinesPreset } from "./flow-lines.shader";

export type FlowLinesColor = "base" | "a" | "b" | "c";
export type FlowLinesValue = "lines" | "amp" | "freq" | "width" | "glow" | "tilt";

export type FlowLinesProps = {
  /** What sits on the scene: a heading, a call to action, a whole section. */
  children?: ReactNode;
  /** A named look: `aurora`, `ember`, `ocean`, `orchid`, `gold`, `ice`. */
  preset?: FlowLinesPreset;
  /** Overrides single palette colours with any CSS colour. */
  colors?: Partial<Record<FlowLinesColor, string>>;
  /** Multiplies the preset's speed (1 keeps it, 0 holds the scene still). */
  speed?: number;
  /** Multiplies the preset's brightness. */
  intensity?: number;
  /** The pointer and touch move the scene. */
  interactive?: boolean;
  /** Accessible name; leave it out when the scene is only a background. */
  label?: string;
  /** Hold one still frame until this is false (for a gallery card). */
  still?: boolean;
  className?: string;
} & Partial<Record<FlowLinesValue, number>>;

const POSTER: PosterSpec = { type: "linear", from: "base", to: "base", angle: 180, blobs: [{ color: "a", at: [28, 58], size: 52 }, { color: "c", at: [76, 42], size: 46 }] };

export function FlowLines({ children, preset, colors, speed, intensity, interactive = true, label, still, className, lines, amp, freq, width, glow, tilt }: FlowLinesProps) {
  return (
    <BackdropCanvas
      slug="flow-lines"
      piece={flowLinesPiece}
      presets={presets}
      fallback="aurora"
      preset={preset}
      colors={colors}
      values={{ lines, amp, freq, width, glow, tilt }}
      speed={speed}
      intensity={intensity}
      poster={POSTER}
      scale={0.6}
      stillTime={4}
      still={still}
      interactive={interactive}
      label={label}
      className={className}
    >
      {children}
    </BackdropCanvas>
  );
}
components/vibebox/flow-lines/flow-lines.shader.ts
/**
 * VibeBox Motion · Flow Lines
 * Copyright (c) 2026 VibeBox. Licensed under the VibeBox Motion Free License:
 * https://motion.vibeboxph.com/license
 * Use it in unlimited personal, commercial and client projects.
 * Do not sell or redistribute it as a component, kit, template or builder.
 */
import { compose, createScenePiece, type ScenePreset } from "@/lib/vibebox/engine/backdrops";

/*
 * Many thin lines of light that ride one slow wave. Each line is a curve of x with its own
 * phase, so together they bunch and fan like silk threads; they brighten where they converge,
 * take their colour from a three-stop ramp across the stack, and thin out toward the far edge so
 * the sheet reads as having depth. The pointer pulls the lines toward it.
 */

const SCENE = /* glsl */ `
uniform vec3 uBase;
uniform vec3 uA;
uniform vec3 uB;
uniform vec3 uC;
uniform float uLines;
uniform float uAmp;
uniform float uFreq;
uniform float uWidth;
uniform float uGlow;
uniform float uTilt;
uniform float uSpeed;
uniform float uIntensity;

float T;

void main() {
  vec2 uv = centred(gl_FragCoord.xy);
  T = uTime * uSpeed;
  float aspect = uResolution.x / uResolution.y;
  vec2 m = (uPointer - 0.5) * vec2(aspect, 1.0);
  int count = int(clamp(uLines, 8.0, 64.0));
  vec3 col = lin(uBase);
  // A faint floor glow so the dark is not flat.
  col += lin(uB) * 0.025 * exp(-pow(uv.y + 0.35, 2.0) * 6.0);

  float acc = 0.0;
  vec3 tint = vec3(0.0);
  for (int i = 0; i < 64; i++) {
    if (i >= count) break;
    float t = float(i) / float(max(count - 1, 1));
    // The stack's depth: nearer lines (t small) are lower and bigger, far ones higher and finer.
    float depth = mix(1.0, 0.4, t);
    float x = uv.x * uFreq;
    float ph = t * 3.2 + T * (0.22 + 0.05 * t);
    float y = mix(-0.34, 0.3, t) * uTilt + (-0.06 + 0.12 * t) * 0.0;
    float wave = sin(x * 1.1 + ph) * 0.55 + sin(x * 2.3 - ph * 1.7 + 1.3) * 0.28 + sin(x * 0.45 + ph * 0.6) * 0.5;
    y += wave * uAmp * depth * 0.22;
    // The pointer draws the lines toward it.
    float pull = exp(-pow((uv.x - m.x) * 1.6, 2.0)) * (0.5 + uHover * 0.9);
    y += (m.y - y) * pull * 0.14 * (1.0 - t);
    float d = abs(uv.y - y);
    float w = uWidth * 0.0036 * (0.55 + 0.7 * depth);
    float core = exp(-pow(d / w, 2.0));
    float halo = exp(-d / (w * 7.0)) * 0.18 * uGlow;
    float line = (core + halo) * (0.45 + 0.9 * depth);
    // Brighter where the stack bunches (neighbours close together).
    vec3 c = t < 0.5 ? mix(lin(uA), lin(uB), t * 2.0) : mix(lin(uB), lin(uC), (t - 0.5) * 2.0);
    tint += c * line;
    acc += line;
  }
  col += tint * 0.55;
  col += lin(uB) * acc * 0.03 * uGlow;

  col *= uIntensity;
  col *= 1.0 - 0.42 * smoothstep(0.4, 1.2, length(uv * vec2(0.75, 1.0)));
  fragColor = vec4(finish(col, gl_FragCoord.xy), 1.0);
}
`;

export const fragment = compose(SCENE);

export const PRESET_NAMES = ["aurora", "ember", "ocean", "orchid", "gold", "ice"] as const;
export type FlowLinesPreset = (typeof PRESET_NAMES)[number];

export const presets: Record<FlowLinesPreset, ScenePreset> = {
  aurora: {
    colors: { base: "#04060f", a: "#33e0ff", b: "#6f5cff", c: "#ff4fd8" },
    values: { lines: 44, amp: 1, freq: 1.7, width: 1, glow: 1, tilt: 1, speed: 0.8, intensity: 1.1 },
  },
  ember: {
    colors: { base: "#0a0405", a: "#ff3b30", b: "#ff9a2e", c: "#ffe27a" },
    values: { lines: 40, amp: 1.05, freq: 1.5, width: 1, glow: 1.1, tilt: 1, speed: 0.75, intensity: 1.1 },
  },
  ocean: {
    colors: { base: "#020a10", a: "#00e0c0", b: "#1a8cff", c: "#a8f0ff" },
    values: { lines: 48, amp: 0.95, freq: 1.8, width: 0.9, glow: 1, tilt: 1, speed: 0.7, intensity: 1.1 },
  },
  orchid: {
    colors: { base: "#0a0412", a: "#ff6ad5", b: "#b24dff", c: "#6dc4ff" },
    values: { lines: 42, amp: 1.1, freq: 1.6, width: 1, glow: 1.1, tilt: 1, speed: 0.8, intensity: 1.1 },
  },
  gold: {
    colors: { base: "#0a0804", a: "#ffd978", b: "#f2a93c", c: "#fff3c8" },
    values: { lines: 46, amp: 0.9, freq: 1.5, width: 0.9, glow: 1, tilt: 1, speed: 0.6, intensity: 1.1 },
  },
  ice: {
    colors: { base: "#04060b", a: "#e8f4ff", b: "#9ec8ff", c: "#ffffff" },
    values: { lines: 38, amp: 0.9, freq: 1.6, width: 0.8, glow: 0.9, tilt: 1, speed: 0.6, intensity: 1.05 },
  },
};

export const flowLinesPiece = createScenePiece("flow-lines", fragment);
components/vibebox/flow-lines/index.tsx
/**
 * VibeBox Motion · Flow Lines
 * Copyright (c) 2026 VibeBox. Licensed under the VibeBox Motion Free License:
 * https://motion.vibeboxph.com/license
 * Use it in unlimited personal, commercial and client projects.
 * Do not sell or redistribute it as a component, kit, template or builder.
 */
export { FlowLines, type FlowLinesProps, type FlowLinesColor, type FlowLinesValue } from "./flow-lines.client";
export { PRESET_NAMES as FLOW_LINES_PRESETS, presets as flowLinesPresets, type FlowLinesPreset } from "./flow-lines.shader";

Props

PropTypeDefaultDescription
childrenReactNode—What sits on the scene: a heading, a call to action, a whole section.
preset"aurora" | "ember" | "ocean" | "orchid" | "gold" | "ice""aurora"The look: aurora, ember, ocean, orchid, gold, ice.
colors{ base?: string; a?: string; b?: string; c?: string }—Overrides single palette colours with any CSS colour: base (the dark behind the lines), a (the colour of the lines nearest the viewer), b (the colour of the lines in the middle), c (the colour of the lines furthest away).
speednumber1Multiplies the preset's speed; 0 holds the scene still.
intensitynumber1Multiplies the preset's brightness, 0.5 to 1.4.
linesnumber44How many lines are drawn (8 to 64) (8 to 64; each look sets its own).
ampnumber1How tall the wave is (0.2 to 2; each look sets its own).
freqnumber1.7How many swells fit across the frame (0.6 to 3; each look sets its own).
widthnumber1How thick each line is (0.4 to 2; each look sets its own).
glownumber1How much light spreads around each line (0 to 2; each look sets its own).
tiltnumber1How far the stack leans back (0 is flat) (0 to 2; each look sets its own).
interactivebooleantrueThe lines are drawn toward the pointer and pulled a little further when it is over the scene.
labelstring—Accessible name; leave it out when the scene is only a background.
stillbooleanfalseHold one composed frame until false (a gallery card that plays on hover).
classNamestring—Merged with cn(); give it a size and a radius.

Accessibility and motion

Keyboard
Not focusable; content on it keeps its own focus order.
Screen readers
Decorative by default; with label it is an image with that name.
Touch
A finger draws the lines toward it like the cursor does; scrolling is never blocked.
Reduced motion
One still frame of the lines; the wave does not drift.

Edge cases and limits

  • The crossing where the lines bunch is the brightest part: keep text off it.
  • At 64 lines on a small phone the lines nearly merge; the default 40 to 48 is tuned for hero size.
  • It is almost all black: on a light page, wrap it in a rounded card rather than full-bleed.
  • Deep violet pleated silk running out of a dark centre with lilac highlights and a fine grain

    Burst

    Free

    Pleated silk seen from its centre: folds run out of a dark middle like a drawn curtain, with a fine cloth grain.

    Backgrounds · WebGL2

  • Bands of cyan, lilac and pink colour flowing across a holographic foil surface

    A film of oil on water, or holographic foil: slow bands of shifting colour with a soft sheen that follows the pointer.

    Backgrounds · WebGL2

  • A soft spectrum gradient behind a heading and a button

    A soft spectrum mesh gradient that drifts behind your content and leans toward the cursor; still under reduced motion.

    Backgrounds · WebGL2

↑ ↓ to move↵ to openesc to close