Sequence Unroll

The diagram every recurrent-network lesson opens with. The same RNN cell drawn numSteps times — left-to-right — with hidden-state arrows flowing between cells, input arrows feeding each cell from below, and output arrows leaving above. The active step paints with the accent colour; earlier steps stay accented at lower opacity so the trace reads as a walk; later steps stay muted. Pair with LSTMGateViz — that one is the per-step primitive; this one is the temporal primitive.

Unrolled RNN with 6 timesteps. Active step 1 of 6.
RNN unroll · 1 / 6paused
h₀y1RNNt=1x1y2RNNt=2x2y3RNNt=3x3y4RNNt=4x4y5RNNt=5x5y6RNNt=6x6
Customize
Shape
6 steps
step 1
Playback
700 ms
Display

Installation

npx shadcn@latest add https://craftbits.dev/r/sequence-unroll.json

Usage

import { SequenceUnroll } from "@craft-bits/viz/sequence-unroll";
 
<SequenceUnroll numSteps={6} defaultCurrentStep={0} />;

Drive the step externally and autoplay it from outside:

const [step, setStep] = useState(0);
const [playing, setPlaying] = useState(false);
 
<SequenceUnroll
  numSteps={6}
  currentStep={step}
  onCurrentStepChange={setStep}
  playing={playing}
  onPlayingChange={setPlaying}
  playSpeed={500}
/>;

Read-only skeleton — no header, no active step:

<SequenceUnroll numSteps={4} defaultCurrentStep={-1} showHeader={false} />

Understanding the component

  1. One cell, drawn many times. The same RNN cell appears numSteps times across the row. Each cell is the same recurrent function — the visual is unrolled, not stacked. t=1, t=2, … underneath each box reminds the reader that time, not depth, is the axis.
  2. Hidden-state arrows. Between every pair of cells sits a horizontal arrow representing the hidden state being passed forward. A short dashed stub on the left labelled h₀ feeds the first cell — the conventional zero initial state. Once the active step crosses an arrow it switches to --cb-accent and full opacity; arrows past the active step stay at --cb-border-strong.
  3. Inputs in, outputs out. Each cell carries a vertical input arrow rising into it from below (x₁, x₂, …) and a vertical output arrow leaving above (y₁, y₂, …). They mirror the standard textbook unroll: input below, output above, recurrence left-to-right.
  4. Active step highlight. When currentStep is t, cell t paints with the accent muted fill and an accent border. Cells at indices < t keep the accent border but drop to the elevated surface fill, so the row reads as this is where we are, this is what already fired. Cells at indices > t paint with the muted surface and the default border.
  5. Controlled + uncontrolled, twice over. Both currentStep and playing follow the Radix pattern: pass currentStep to control externally (pair with onCurrentStepChange) or defaultCurrentStep to let the component own it. Same for playing / defaultPlaying / onPlayingChange. Autoplay is a setInterval torn down on pause, unmount, or reaching the last step.
  6. SPRINGS.smooth everywhere. Highlight fades, arrow opacity changes, and cell-fill swaps all ride SPRINGS.smooth from @craft-bits/core/motion. prefers-reduced-motion: reduce parks the active step at the last index and collapses every transition to an instant swap.

Props

PropTypeDefaultDescription
numStepsnumber6How many timesteps to render. Reads best at 4–8.
currentStepnumberControlled active step in [-1, numSteps - 1]. -1 = skeleton. Pair with onCurrentStepChange.
defaultCurrentStepnumber-1Uncontrolled initial active step.
onCurrentStepChange(step) => voidFires when the active step changes (autoplay or external).
playingbooleanControlled autoplay flag. Pair with onPlayingChange.
defaultPlayingbooleanfalseUncontrolled initial autoplay flag.
onPlayingChange(playing) => voidFires when autoplay starts, pauses, or hits the end.
playSpeednumber700Milliseconds between auto-advanced steps. Minimum 120ms.
showHeaderbooleantrueRender the step k / N eyebrow above the diagram.
transitionTransitionSPRINGS.smoothSpring for highlight / arrow transitions.
classNamestringMerged onto the root <div> via cn().

Accessibility

  • The whole figure is role="figure" with aria-labelledby naming "RNN unroll · k / N" and an aria-describedby aria-live="polite" summary that announces the active step whenever it changes.
  • The SVG carries role="img" and is aria-labelledby the same heading id.
  • Colour is never the only signal — the active step also gets a thicker stroke and the eyebrow restates step k / N numerically.
  • data-state="playing" | "paused" and data-step={k} are exposed on the root for screen-reader hooks and CSS overrides.
  • prefers-reduced-motion: reduce collapses every transition to an instant swap and parks the active step at the last index so the picture reads as the final unrolled state rather than animating.

Credits

  • Extracted from: craftingattention (app/src/lessons/primitives/viz/SequenceUnroll.tsx). The source was a Widget-hosted Explore / Predict lesson driven by ModeStrip, ChallengeBtn, FeedbackBadge, usePredictRounds, and useWidgetHistory — six hard-coded predict scenarios about tanh saturation / memory decay / sign-flip, an explore mode with W_xh, W_hh, bias, and four-element input sliders, a setTimeout autoplay, plain-English narration that switched between five rule branches, an SVG that painted hidden-state badges using a custom oklch scale and rendered the trajectory as a translucent polyline, and the source's --color-accent-* / --color-ink-* / --color-surface-* CSS variables. The library version drops the Widget chrome, the predict / challenge state machines, the narration generator, the four-element fixed input array, the weight sliders, the bookmark presets, the hidden-state badges and trajectory polyline, the useWidgetHistory undo / redo stack, and every project-specific CSS variable. Reframed as the temporal primitive every Recurrent lesson needs: a row of identical RNN cells with hidden-state arrows between them, an h₀ stub on the left, vertical input / output arrows on each cell, and a controlled / uncontrolled currentStep + playing pair with optional autoplay. Switched all colour to --cb-* semantic tokens, replaced inline springs with SPRINGS.smooth from @craft-bits/core/motion, and exposed numSteps so the same component handles 4-step and 8-step unrolls without forking the SVG geometry. Sits next to LSTMGateViz in ML Viz → Recurrent as the time-axis counterpart to the per-step primitive.