Activation Morph Inline

Inline morph slider that tweens between two activation functions (default ReLU → GELU). One range input drives the morph t ∈ [0, 1]; the active curve is a lerp(fromFn, toFn, t) of both f(x) and f′(x). A draggable vertical probe sweeps the x-axis and reads out the blended value at the current x. Two faint ghost outlines pin the morph endpoints in place so the learner can see exactly how the active curve relates to its pure-endpoint shapes, and a kink-zone wash sits behind the origin to show where the hard-corner of the fromFn smooths out as you approach toFn.

Activation morph explorer from ReLU to GELU.
ReLUGELU
ReLU
f(0.0) = 0.0000
f′(0.0) = 0.0000← hard kink
Customize
Endpoints
ReLU
GELU
Morph
0.00
Overlays

Installation

npx shadcn@latest add https://craftbits.dev/r/activation-morph-inline.json

Usage

import { ActivationMorphInline } from "@craft-bits/viz/activation-morph-inline";
 
<ActivationMorphInline />

Morph between any pair of built-in activations:

<ActivationMorphInline fromFn="Sigmoid" toFn="Tanh" />

Hide the kink wash and the ghost outlines for a clean comparison:

<ActivationMorphInline
  fromFn="ReLU"
  toFn="LeakyReLU"
  highlightKink={false}
  showGhosts={false}
/>

Seed the slider in the middle and listen for both changes:

<ActivationMorphInline
  initialMorph={0.5}
  onMorphChange={(t) => console.log("morph =", t)}
  onXChange={(x) => console.log("x =", x)}
/>

Understanding the component

  1. Six built-in activations. ReLU, GELU, Sigmoid, Tanh, LeakyReLU, ELU. Each defines both f(x) and f′(x) — five analytical, GELU via central finite differences against the tanh approximation.
  2. Linear interpolation of curves. Each sample on the active curve is lerp(fromFn.f(x), toFn.f(x), t), and the same for the derivative. Because the lerp is per-x, the morph is a true visual blend — not a stitched-together piecewise composition.
  3. Shared axis. Active curve, derivative, and both ghost endpoints render on the same x ∈ [−4, 4], y ∈ [−1.5, 4] plot at 200 samples each. Output is clipped to the visible y-domain.
  4. Probe spring. A useMotionValue tracks the raw pixel-x of the probe; useSpring against SPRINGS.snap produces the visible position. Drag updates the raw value; the spring chases.
  5. Kink-zone wash. A faint rect at x ∈ [−0.3, 0.3] is drawn in the fromColor with opacity 0.06 × (1 − t). At t = 0 it's faintly visible behind the corner; by t = 1 it has fully faded.
  6. Color mix. The active curve uses a CSS color-mix(in oklch, fromColor, toColor) so the hue smoothly transitions in perceptual space as the slider moves.
  7. Readout card. Tabular-mono f(x) and (optionally) f′(x) of the blended curve at the probe. When the probe is near the kink (|x| < 0.15), a "hard kink" tag appears at low t and a "smooth transition" tag appears at high t.

Props

PropTypeDefaultDescription
fromFnActivationMorphInlineFnName"ReLU"Activation at the left end of the morph slider.
toFnActivationMorphInlineFnName"GELU"Activation at the right end of the morph slider.
fromColorstring"var(--cb-accent)"CSS colour for the fromFn curve, label, and ghost.
toColorstring"var(--cb-info)"CSS colour for the toFn curve, label, and ghost.
initialMorphnumber0Initial slider position in [0, 1].
initialXnumber0Initial probe position. Clamped to [−4, 4].
showDerivativebooleantrueOverlay the blended f′(x) curve as a dashed line.
showGhostsbooleantrueRender the pure-endpoint outlines behind the active curve.
highlightKinkbooleantrueWash the kink region in fromColor with t-fading opacity.
captionReactNodeCaption rendered below the readout card.
transitionTransitionSPRINGS.snapOverride the spring used by the probe and value dot.
onXChange(x: number) => voidFires on every probe move.
onMorphChange(t: number) => voidFires on every morph-slider move.
classNamestringMerged onto the root via cn().

Accessibility

  • The plot SVG is role="img" with an aria-label listing the current morph state and probe position.
  • The drag surface is role="slider" with aria-valuemin / aria-valuemax / aria-valuenow / aria-valuetext reflecting the probe.
  • The morph input is a native <input type="range"> with an aria-label that includes the active morph label.
  • Keyboard: on the probe — / nudge by 0.1, Shift+← / Shift+→ nudge by 0.5, Home / End jump to the bounds. On the morph slider — native range-input behaviour (arrows, PageUp/PageDown, Home/End).
  • Decorative shapes (grid dots, ghost outlines, kink wash, dashed derivative, value label, legend) are tagged aria-hidden so screen readers narrate only the slider value and the readout card.
  • Colour is never the only signal — the readout card prints the numeric values in tabular monospace and the morph label includes the activation name.
  • Motion respects prefers-reduced-motion: reduce automatically — useSpring from motion/react snaps to target instead of animating.

Credits

  • Extracted from: craftingattention (app/src/lessons/primitives/viz/ActivationMorphInline.tsx). Dropped the lesson chrome (SvgLabel), remapped per-lesson palette tokens to var(--cb-*) semantic tokens so consumer themes repaint freely, swapped the project-local SPRINGS.snug for the canonical SPRINGS.snap, generalised the hardcoded ReLU/GELU pair into a fromFn / toFn matrix over six built-in activations with overridable colours, exposed initialMorph / initialX / showDerivative / showGhosts / highlightKink / caption / transition / onXChange / onMorphChange props, wrapped in forwardRef + cn() + ...props spread, and added Home / End keyboard bounds.