Gradient Arrow Field

A 2D vector field showing the gradient of a scalar function f(x, y) at each point on a regular grid. Each arrow points in the direction of steepest ascent (or flip them with descent to point downhill — the direction gradient descent would step). Arrow length is proportional to gradient magnitude, normalised so the longest arrow fills roughly one cell.

Use it to teach why optimizers do what they do: every gradient-descent step follows the local arrow. Where the field swirls, optimizers struggle; where it's near-zero, they stall (saddle points, plateaus).

Gradient arrow field over a 12 by 12 grid showing the gradient (ascent) of f over x in [-2, 2] and y in [-2, 2]. Arrow length is proportional to gradient magnitude.
Customize
Function
bowl
Resolution
12
Display

Installation

npx shadcn@latest add https://craftbits.dev/r/gradient-arrow-field.json

Usage

import { GradientArrowField } from "@craft-bits/core";
 
<GradientArrowField />

Show the gradient-descent direction (arrows flipped to point downhill) over a custom function, overlaid on the function's own contour heatmap:

<GradientArrowField
  fn={(x, y) => Math.sin(x) * Math.cos(y) + 0.1 * (x * x + y * y)}
  descent
  showFunction
  colorScheme="viridis"
/>

Understanding the component

  1. Symmetric finite differences. The gradient is estimated numerically at each grid point with a centred difference: (f(x + h, y) - f(x - h, y)) / (2h) and similarly for y. The dh prop sets the step size; the default 0.01 is fine for any smooth function in the typical visible range. No symbolic differentiation required — drop in any pure function.
  2. Length normalisation. Each arrow's screen length is scaled by mag / maxMag so the longest arrow fills roughly 80% of a grid cell, regardless of the absolute scale of f. A near-flat region next to a steep one always reads as "small arrow vs big arrow," never "everything is tiny."
  3. Ascent vs descent. descent={false} (default) points each arrow in the +gradient direction (steepest ascent). descent={true} flips every arrow to -gradient — the direction a gradient-descent step would move from that point. The same plot tells the optimization story by flipping a single boolean.
  4. Optional contour heatmap. Set showFunction to render f itself as a gridSize x gridSize heatmap underneath the arrows. Three color schemes (accent, viridis, plasma) share the perceptual ramps used by LossLandscape, so the two components compose cleanly.
  5. Near-zero hiding. Arrows whose magnitude is below 2% of the maximum are faded out — keeps saddle points and local minima looking like calm spots rather than a jittery cluster of tiny arrows.
  6. Reduced-motion fallback. With prefers-reduced-motion: reduce, the arrows snap to their new pose on prop changes — no spring transitions.

Props

PropTypeDefaultDescription
fn(x: number, y: number) => numberbowl: (x, y) => x * x + y * yScalar field whose gradient is drawn.
xRangereadonly [number, number][-2, 2]Visible math-space x-range.
yRangereadonly [number, number][-2, 2]Visible math-space y-range.
gridSizenumber12Grid resolution in samples per side.
descentbooleanfalseWhen true, arrows point in the -gradient direction (gradient descent).
showFunctionbooleanfalseRender f as a contour heatmap underneath the arrows.
colorScheme'accent' | 'viridis' | 'plasma''accent'Color ramp for the contour heatmap.
dhnumber0.01Symmetric finite-difference step for the gradient estimate.
sizenumber360SVG side length in pixels.
transitionTransitionSPRINGS.smoothSpring used for arrow pose transitions.
classNamestringMerged onto the root via cn().

Accessibility

  • The figure is role="figure" with an aria-live="polite" summary that announces the grid resolution, the visible x and y ranges, and whether the field shows the ascent or descent direction.
  • Color is never the only signal — direction and magnitude are encoded in arrow geometry, so the figure remains readable under any palette.
  • prefers-reduced-motion: reduce snaps every arrow to its new pose with no spring transitions.

Credits

  • Extracted from: craftingattention (app/src/lessons/primitives/nn/GradientArrowField.tsx). The original was a tightly-scoped three-class softmax probability-bar interaction — it taught the gradient p - y for a specific cross-entropy lesson. The library version generalises to a true 2D vector field over any (x, y) -> number: finite-difference gradients on a regular grid, length-normalised arrows, a descent flip, and an optional contour heatmap that shares the color ramps used by LossLandscape. The two compose: drop a LossLandscape heatmap behind a GradientArrowField and you have a complete picture of "what the surface looks like" and "which way gradient descent would step."