Skip to main content
The Matrix rain effect includes several real-time customization controls that allow you to personalize the visual experience without refreshing the page.

Control panel

All controls are located in a semi-transparent panel in the top-right corner of the screen:
<div className="absolute top-2 right-2 sm:top-4 sm:right-4 z-10 p-2 sm:p-3 bg-black bg-opacity-60 rounded-lg text-white font-[family-name:var(--font-geist-mono)] shadow-lg flex flex-col gap-2 sm:gap-3 w-auto">
  {/* Controls */}
</div>
The control panel uses responsive spacing and sizing to adapt to mobile and desktop screens. On smaller screens, controls are more compact.

Speed control

Adjust the animation speed from 0.1x to 3.0x using a range slider.

Implementation

const [sliderValue, setSliderValue] = useState(100);
const speedRef = useRef(1.0);

useEffect(() => {
  speedRef.current = sliderValue / 100.0;
}, [sliderValue]);

UI component

<input
  type="range"
  id="speedSlider"
  min="10"
  max="300"
  value={sliderValue}
  onChange={(e) => setSliderValue(Number(e.target.value))}
  className="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer accent-green-500"
/>
The speed value directly multiplies the drop position increment:
if (isFallingDownRef.current) {
  dropsRef.current[i] += currentSpeed;
} else {
  dropsRef.current[i] -= currentSpeed;
}
  • Speed 0.1x: Drops move 0.1 pixels per frame
  • Speed 1.0x: Drops move 1 pixel per frame (default)
  • Speed 3.0x: Drops move 3 pixels per frame

Speed ranges

Perfect for:
  • Background ambiance
  • Meditative viewing
  • Low-distraction environments
  • Reading individual characters

Color picker

Select any custom color for the falling characters when not in RGB mode.

Implementation

const [characterColor, setCharacterColor] = useState("#00FF00");
const colorRef = useRef("#00FF00");

useEffect(() => {
  colorRef.current = characterColor;
}, [characterColor]);

UI component

<input
  type="color"
  id="colorPicker"
  value={characterColor}
  onChange={(e) => setCharacterColor(e.target.value)}
  className="w-full h-7 sm:h-8 p-0 border-none rounded cursor-pointer bg-gray-700"
  disabled={isRgbMode}
/>
The color picker is automatically disabled when RGB mode is active, preventing conflicts between static and dynamic color modes.
  • #00FF00 - Classic Matrix green (default)
  • #FF0000 - Red for a danger/alert theme
  • #00FFFF - Cyan for a cyberpunk aesthetic
  • #FFFFFF - White for high contrast
  • #FFD700 - Gold for a premium look

RGB mode

Enable dynamic rainbow colors that cycle through the spectrum.

Implementation

const [isRgbMode, setIsRgbMode] = useState(false);
const isRgbModeRef = useRef(false);
const rgbEffectTimeRef = useRef(0);

useEffect(() => {
  isRgbModeRef.current = isRgbMode;
}, [isRgbMode]);

Color calculation

if (isRgbModeRef.current) {
  rgbEffectTimeRef.current = (rgbEffectTimeRef.current + 0.5) % 360;
}

for (let i = 0; i < columns; i++) {
  if (isRgbModeRef.current) {
    const hue = (dropsRef.current[i] * 2 + rgbEffectTimeRef.current) % 360;
    ctx.fillStyle = `hsl(${hue < 0 ? hue + 360 : hue}, 100%, 50%)`;
  } else {
    ctx.fillStyle = colorRef.current;
  }
  // ... render character
}
The RGB effect uses HSL (Hue, Saturation, Lightness) color space:
  1. Time offset: Increments by 0.5 each frame, creating global color shift
  2. Drop position: Each drop’s Y position influences its hue
  3. Hue calculation: (dropY * 2 + timeOffset) % 360
  4. Result: Vertical rainbow gradient that animates over time
The modulo 360 ensures hue values wrap around (red → violet → red).

UI component

<button
  onClick={() => setIsRgbMode(!isRgbMode)}
  className={`w-full px-2 sm:px-3 py-1.5 sm:py-2 text-xs sm:text-sm rounded-md transition-colors ${
    isRgbMode
      ? "bg-pink-600 hover:bg-pink-700"
      : "bg-blue-600 hover:bg-blue-700"
  }`}
>
  {isRgbMode ? "Disable RGB" : "Enable RGB"}
</button>
RGB mode is more computationally intensive than static color mode. On lower-end devices, consider disabling RGB mode if you experience performance issues.

Direction toggle

Switch between falling down and rising up animations.

Implementation

const [isFallingDown, setIsFallingDown] = useState(true);
const isFallingDownRef = useRef(true);

useEffect(() => {
  isFallingDownRef.current = isFallingDown;
  initializeDrops();
}, [isFallingDown, initializeDrops]);

Drop initialization

const initializeDrops = useCallback(() => {
  const { columns, height, fontSize } = canvasDimensionsRef.current;
  const newDrops: number[] = [];
  for (let i = 0; i < columns; i++) {
    if (isFallingDownRef.current) {
      newDrops[i] = Math.random() * -height / fontSize;
    } else {
      newDrops[i] = height / fontSize + (Math.random() * height / fontSize);
    }
  }
  dropsRef.current = newDrops;
}, []);
When direction changes, all drops are reinitialized with appropriate starting positions to prevent visual glitches. Falling drops start above the viewport, rising drops start below.

Movement logic

if (isFallingDownRef.current) {
  dropsRef.current[i] += currentSpeed;
  if (dropsRef.current[i] * fontSize > height && Math.random() > 0.975) {
    dropsRef.current[i] = 0;
  }
} else {
  dropsRef.current[i] -= currentSpeed;
  if (dropsRef.current[i] * fontSize < 0 && Math.random() > 0.975) {
    dropsRef.current[i] = height / fontSize;
  }
}
The Math.random() > 0.975 check creates a 2.5% chance per frame for a drop to reset. This prevents all drops from resetting simultaneously and maintains visual variety.

UI component

<button
  onClick={() => setIsFallingDown(!isFallingDown)}
  className="w-full px-2 sm:px-3 py-1.5 sm:py-2 text-xs sm:text-sm rounded-md bg-transparent hover:bg-gray-700/50 border border-gray-600/50 transition-colors"
  title="Toggle Rain Direction"
>
  {isFallingDown ? "Rain Up ↑" : "Rain Down ↓"}
</button>

State management with refs

All controls use a combination of React state and refs for optimal performance:
const [sliderValue, setSliderValue] = useState(100);
const speedRef = useRef(1.0);

useEffect(() => {
  speedRef.current = sliderValue / 100.0;
}, [sliderValue]);
This pattern provides two benefits:
  1. State triggers UI updates: Changing sliderValue re-renders the component to show the new value in the UI
  2. Refs avoid stale closures: The animation loop accesses speedRef.current which always contains the latest value, even though the loop function isn’t recreated
Without refs, the animation loop would capture stale values from when it was created.

Responsive design

All controls adapt to screen size:
<div className="w-32 sm:w-36 md:w-40">
  <label className="block text-xs sm:text-sm mb-1 select-none">
    Speed: {(sliderValue / 100).toFixed(1)}x
  </label>
  {/* Control */}
</div>
  • Control width: 128px (w-32)
  • Text size: 12px (text-xs)
  • Padding: 8px (p-2)
  • Top/right offset: 8px (top-2 right-2)

Build docs developers (and LLMs) love