Depth Decay Slider
A draggable layer-count slider paired with a signal bar that shrinks as depth grows. The gradient magnitude is computed as decay^n (default 0.5^n); the bar width is a mild blend of linear and sqrt so mid-range depths stay legible while the exponential collapse at deep depths still feels visceral. The learner watches the bar practically vanish, making the vanishing-gradient problem felt rather than computed.
2 layers. Gradient: 0.250. Signal healthy.
2 layers. Gradient = 0.250 (25.0%). Still strong enough for every layer to learn.
Customize
Range
2
20
State
2
0.50
Layout
Installation
npx shadcn@latest add https://craftbits.dev/r/depth-decay-slider.jsonUsage
Uncontrolled — the component owns its own layer count:
import { DepthDecaySlider } from "@craft-bits/viz/depth-decay-slider";
<DepthDecaySlider defaultLayers={2} />Controlled — pair layers with onLayersChange:
const [layers, setLayers] = useState(2);
<DepthDecaySlider layers={layers} onLayersChange={setLayers} />Override the decay constant (e.g. a higher-floor activation) or the depth range:
<DepthDecaySlider minLayers={1} maxLayers={40} decay={0.7} />Suppress the narration block:
<DepthDecaySlider renderNarration={null} />Understanding the component
- Decay model. Each layer multiplies the gradient by
decay. After n layers the magnitude isdecay^n. With the defaultdecay = 0.5, two layers leaves a healthy 25%, ten layers leaves about 0.1%, twenty layers leaves a number you cannot train against. - Bar width. A pure exponential collapses too fast to read. The bar width blends the linear fraction and its square root 30 / 70 so the 4–8 layer range stays visible, but the exponential decay still wins by layer 10+.
- Phase colouring. Layer counts fall into one of four phases (healthy, weakening, vanishing, dead) driven by
phaseCutoffs. Each phase maps to a semantic CSS-var so the colour follows the user's theme. - Layer stack. Below the bar, one small box renders per layer with a small multiplication label inside (hidden once there are too many boxes to fit). The stack reinforces the multiplicative model — every box is one multiplication step.
- Drag plus keyboard. The transparent hit target spans the slider track; pointer events drag the thumb, and arrow keys step by 1 (Shift+Arrow steps by 5). Home jumps to
minLayers, End jumps tomaxLayers. - Idle pulse. When the slider sits at
minLayers, the bar's outline pulses gently to invite interaction. The pulse drops underprefers-reduced-motion. - Reduced motion. The bar tween, glow tween, and mount animation all collapse to instant under
prefers-reduced-motion: reduce. The component still updates synchronously — only the spring drops.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
layers | number | — | Current layer count (controlled). |
defaultLayers | number | minLayers | Initial layer count (uncontrolled). |
onLayersChange | (next: number) => void | — | Fired when the user drags or arrows the slider. |
minLayers | number | 2 | Minimum selectable layer count. |
maxLayers | number | 20 | Maximum selectable layer count. |
decay | number | 0.5 | Per-layer multiplier. The signal decays as decay^n. |
ticks | readonly number[] | [2, 5, 10, 15, 20] | Tick marks rendered under the track. |
phaseCutoffs | DepthDecaySliderPhaseCutoffs | { healthy: 4, weakening: 8, vanishing: 14 } | Cutoffs deciding which phase a layer count falls into. |
renderNarration | ((args) => ReactNode) | null | default narration | Narration override. Pass null to suppress the narration block. |
transition | Transition | SPRINGS.snap | Bar-fill animation transition. Reduced-motion users snap regardless. |
ariaLabel | string | derived | Accessible name override. |
className | string | — | Merged onto the root via cn(). |
Accessibility
- The slider hit target carries
role="slider",aria-valuemin/aria-valuemax/aria-valuenow, plus anaria-valuetextthat includes the formatted gradient — so screen readers hear both the layer count and the magnitude. - Keyboard support covers Arrow / Shift+Arrow / Home / End; focus is visible via the thumb ring (stroke plus radius bump).
- The SVG is
role="img"with a comprehensivearia-label; an additionalaria-live="polite"status region announces phase transitions in plain language. - Motion respects
prefers-reduced-motion: reduce— the bar tween, glow tween, mount animation, and idle pulse all collapse to instant. - The root carries
data-cb-viz="depth-decay-slider"anddata-phaseso consumer apps can hook custom styles or assistive tooling.
Credits
- Extracted from:
craftingattention(app/src/lessons/primitives/math/DepthDecaySlider.tsx). The source hardwired lesson-specific colour vars, a projectSvgLabelprimitive, and aca-narrationchrome class. The craft-bits version maps the four phases onto semanticcb-*tokens, replaces the project-specific label primitive with bare<text>carrying the canonicalcb-*font and size, exposes the decay constant and phase cutoffs as props, and adds proper controlled / uncontrolled state plumbing.