LSTM Gate Viz

A single LSTM time-step laid bare. Four circular meters — forget, input, output, candidate — feed the cell-state update C_t = f · C_{t-1} + i · g, with the resulting C_t rendered as a signed bar and the hidden state h_t = o · tanh(C_t) written underneath. No time axis, no narration: this is the per-step primitive that teaches what each gate does to the cell state.

LSTM cell update. Forget 0.70, input 0.40, output 0.60, candidate 0.80. Previous cell 1.00, new cell 1.02, hidden 0.46.
LSTM gates · one stepCt-1 = 1.00
0.70forget
0.40input
0.80candidate
0.60output
cell stateCt = 1.020
-30+3

Ct=0.700·1.000+0.400·0.800=1.020

ht=0.600·tanh(1.020)=0.462

0.70
0.40
0.60
0.80
Customize
Gates
0.70
0.40
0.60
0.80
Inputs
1.00
Layout

Installation

npx shadcn@latest add https://craftbits.dev/r/lstm-gate-viz.json

Usage

import { LSTMGateViz } from "@craft-bits/core";
 
<LSTMGateViz
  defaultForgetGate={0.7}
  defaultInputGate={0.4}
  defaultOutputGate={0.6}
  defaultCandidate={0.8}
  cellState={1}
/>;

Drive every gate from outside the component:

const [forget, setForget] = useState(0.5);
const [input, setInput] = useState(0.5);
const [output, setOutput] = useState(0.5);
const [candidate, setCandidate] = useState(0.5);
 
<LSTMGateViz
  forgetGate={forget}
  onForgetGateChange={setForget}
  inputGate={input}
  onInputGateChange={setInput}
  outputGate={output}
  onOutputGateChange={setOutput}
  candidate={candidate}
  onCandidateChange={setCandidate}
  cellState={1}
/>;

Read-only embed (no sliders, no formulas):

<LSTMGateViz
  forgetGate={0.95}
  inputGate={0.05}
  outputGate={0.6}
  candidate={0.4}
  cellState={2}
  showFormulas={false}
  showControls={false}
/>;

Understanding the component

  1. Four meters, one update. Each meter is a three-quarter arc that fills from the lower-left as the value rises. The forget meter paints --cb-warning, input paints --cb-success, output paints --cb-accent, and candidate paints --cb-info. The numeric value sits in the centre so it reads even on a small screen.
  2. Candidate is bipolar. Unlike the three sigmoid gates which live in [0, 1], the candidate is tanh-bounded — values can be negative. The arc fills from |candidate| so the picture stays legible at both ends, and the numeric readout carries the sign.
  3. Cell-state bar. The horizontal bar below the meters is centred on zero and ranges over ±3 for display. The translucent grey block is C_{t-1}; the coloured block on top is the new C_t = f · C_{t-1} + i · g. Each block grows leftward for negative values, rightward for positive — so the sign is visible at a glance without reading the number.
  4. Formulas. The two equations beneath the bar print the plain arithmetic each gate contributes: C_t = f · C_{t-1} + i · g and h_t = o · tanh(C_t). Hide them with showFormulas={false} for inline embeds.
  5. Controlled + uncontrolled per gate. Every meter exposes the Radix value / defaultValue pair (forgetGate / defaultForgetGate + onForgetGateChange, and the same shape for input / output / candidate). Mix-and-match — drive one gate externally while letting the others own their state.
  6. SPRINGS.smooth on every transition. Meter arcs, the cell-state bar, and the previous-state block all ride SPRINGS.smooth from @craft-bits/core/motion. prefers-reduced-motion: reduce collapses everything to an instant swap.

Props

PropTypeDefaultDescription
forgetGatenumberControlled forget gate in [0, 1]. Pair with onForgetGateChange.
defaultForgetGatenumber0.5Uncontrolled initial forget gate.
onForgetGateChange(v) => voidFires when the forget gate changes.
inputGatenumberControlled input gate in [0, 1]. Pair with onInputGateChange.
defaultInputGatenumber0.5Uncontrolled initial input gate.
onInputGateChange(v) => voidFires when the input gate changes.
outputGatenumberControlled output gate in [0, 1]. Pair with onOutputGateChange.
defaultOutputGatenumber0.5Uncontrolled initial output gate.
onOutputGateChange(v) => voidFires when the output gate changes.
candidatenumberControlled candidate in [-1, 1]. Pair with onCandidateChange.
defaultCandidatenumber0.5Uncontrolled initial candidate.
onCandidateChange(v) => voidFires when the candidate changes.
cellStatenumber1Previous cell state C_{t-1} used in the update.
showFormulasbooleantrueRender the inline C_t = ... and h_t = ... formulas.
showControlsbooleantrueRender the four sliders below the meters.
transitionTransitionSPRINGS.smoothSpring used for meter / bar transitions.
classNamestringMerged onto the root <div> via cn().

Accessibility

  • The whole figure is role="figure" with aria-labelledby naming "LSTM gates · one step" and an aria-describedby aria-live="polite" summary that announces every gate value, C_{t-1}, C_t, and h_t whenever any input changes.
  • Each meter SVG carries role="img" with a descriptive aria-label that names the gate and its current value.
  • Colour is never the only signal — every meter labels its numeric value in the centre, and the cell-state bar carries a data-state="positive" | "negative" attribute on the C_t readout for screen-reader hooks.
  • The sliders are LabeledSliders — native <input type="range"> underneath — so arrow keys nudge the value and screen readers narrate it.
  • prefers-reduced-motion: reduce collapses every meter / bar transition to an instant swap.

Credits

  • Extracted from: craftingattention (app/src/lessons/primitives/viz/LSTMGateViz.tsx). The source was a sequenced Widget-hosted Explore / Predict / Challenge lesson driven by ModeStrip, ChallengeBtn, FeedbackBadge, LabeledSlider, and useWidgetHistory (undo / redo / bookmark presets like "Remember everything" / "Forget everything" / "Strong input" / "Read only"), with the gates rendered as a 640×200 SVG pipeline of three coloured rectangles whose internal fill rose and fell as the user dragged sigmoid biases through LCG-seeded predict / challenge setups. The library version drops the Widget chrome, the predict / challenge state machines, the random-seed problem generator, the bias-and-tanh slider wiring, the bookmark presets, the eyebrow / premise / caption / formula bar, and the source's raw CSS variables. Reframed as the per-step primitive every Recurrent lesson actually needs: four circular meters (forget, input, output, candidate) over a signed cell-state bar with the C_t = f · C_{t-1} + i · g and h_t = o · tanh(C_t) equations written out. Switched all colour to --cb-* semantic tokens, replaced inline springs with SPRINGS.smooth from @craft-bits/core/motion, and exposed Radix controlled / uncontrolled pairs for all four gates plus the cellState input. Opens ML Viz → Recurrent as the first member of the subsection.