Skip to main content
Lumidot has two distinct animation modes that are automatically selected based on the pattern: wave mode and sequence mode.

Mode Selection

The mode is determined by the getPatternFrames function in index.tsx. Patterns that return multiple frames use sequence mode, while single-frame patterns use wave mode:
const frames = React.useMemo(
  () => getPatternFrames(pattern, rows, cols, direction),
  [pattern, rows, cols, direction],
);
const isSequence = frames.length > 1;
The component automatically sets a data attribute:
data-lumidot-mode={isSequence ? 'sequence' : 'wave'}

Wave Mode

Wave mode applies staggered delays to create a propagating wave effect across all active dots.

Patterns Using Wave Mode

  • All single-frame patterns (most patterns)
  • all, wave-lr, wave-rl, wave-tb, wave-bt
  • Line patterns (line-h-top, line-v-left, etc.)
  • Shape patterns (l-tl, t-top, frame-sync, etc.)
  • Solo patterns (solo-center, solo-tl, solo-br)

How Wave Mode Works

Wave mode uses CSS custom properties to apply animation delays:
return {
  ...base,
  backgroundColor: on ? color : 'transparent',
  boxShadow: on ? shadow : 'none',
  '--lumidot-delay': `${on ? (isSync ? 0 : waveDelay(i, waveDir, duration, cols, rows)) : 0}s`,
  '--lumidot-duration': `${duration}s`,
} as React.CSSProperties;

Wave Delay Calculation

The waveDelay function calculates delays based on position and direction:
function waveDelay(
  index: number,
  direction: LumidotDirection | LumidotWaveDirection,
  duration: number,
  cols: number,
  rows: number,
): number {
  const col = index % cols;
  const row = Math.floor(index / cols);
  const step = duration / Math.max(5, cols + rows - 2);
  const maxCol = cols - 1;
  const maxRow = rows - 1;

  switch (direction) {
    case 'ltr':
      return (col + row) * step;
    case 'rtl':
      return (maxCol - col + row) * step;
    case 'ttb':
      return (row + col) * step;
    case 'btt':
      return (maxRow - row + col) * step;
    case 'lr':
      return col * step;
    case 'rl':
      return (maxCol - col) * step;
    case 'tb':
      return row * step;
    case 'bt':
      return (maxRow - row) * step;
    default:
      return (col + row) * step;
  }
}

Wave Directions

For patterns with wave-* names, the direction is mapped using WAVE_DIRECTIONS:
const waveDir = (WAVE_DIRECTIONS as Partial<Record<LumidotPattern, LumidotWaveDirection>>)[pattern] ?? direction;
export const WAVE_DIRECTIONS = {
  'wave-lr': 'lr',
  'wave-rl': 'rl',
  'wave-tb': 'tb',
  'wave-bt': 'bt',
} as const;

Example: Wave Mode

import { Lumidot } from 'lumidot';

// Diagonal wave from top-left to bottom-right
<Lumidot 
  pattern="all" 
  direction="ltr" 
  duration={0.8}
  rows={4}
  cols={4}
  variant="blue"
/>

// Horizontal wave from left to right
<Lumidot 
  pattern="wave-lr" 
  duration={1.2}
  rows={3}
  cols={6}
  variant="emerald"
/>

// Vertical wave from bottom to top
<Lumidot 
  pattern="wave-bt" 
  duration={1.0}
  rows={6}
  cols={3}
  variant="purple"
/>

Sync Patterns

Some patterns skip wave delays entirely using SYNC_PATTERNS:
export const SYNC_PATTERNS = new Set<string>(['corners-sync', 'frame-sync']);
const isSync = SYNC_PATTERNS.has(pattern);
Sync patterns have --lumidot-delay: 0s for all dots, making them pulse together:
<Lumidot pattern="corners-sync" rows={4} cols={4} variant="cyan" />
<Lumidot pattern="frame-sync" rows={4} cols={4} variant="orange" />

Sequence Mode

Sequence mode animates through multiple frames, showing different dot configurations over time.

Patterns Using Sequence Mode

  • corners-only (4 frames: tl → tr → br → bl)
  • plus-hollow (4 frames: top → right → bottom → left)
  • spiral (4 frames: spiraling around perimeter)

How Sequence Mode Works

Sequence mode uses a state-based interval to cycle through frames:
const [frame, setFrame] = React.useState(0);
const reduced = useReducedMotion(isSequence);

React.useEffect(() => {
  if (!isSequence) return;
  setFrame(0);
  if (frames.length <= 1 || reduced) return;
  const id = window.setInterval(() => setFrame((prev) => (prev + 1) % frames.length), 1250);
  return () => window.clearInterval(id);
}, [frames, reduced, isSequence]);
Key characteristics:
  • Frame changes every 1250ms (1.25 seconds)
  • Uses CSS transitions for opacity and transform
  • Active dots have opacity: 1 and transform: scale(1)
  • Inactive dots have opacity: 0 and transform: scale(0.7)

Sequence Transitions

Sequence mode uses different transition timings for fade in/out:
const fadeIn = 37; // Fast fade in (37ms)

if (isSequence) {
  return {
    ...base,
    backgroundColor: allDots.has(i) ? color : 'transparent',
    boxShadow: allDots.has(i) ? shadow : 'none',
    opacity: on ? 1 : 0,
    transform: on ? 'scale(1)' : 'scale(0.7)',
    transition:
      frames.length > 1 && !reduced
        ? `opacity ${on ? fadeIn : 250}ms ${on ? 'ease-out' : 'ease-in'}, transform 250ms ease`
        : undefined,
  };
}
  • Fade in: 37ms ease-out (fast)
  • Fade out: 250ms ease-in (slower)
  • Scale: 250ms ease (both directions)

Example: Sequence Mode

import { Lumidot } from 'lumidot';

// Animates through each corner sequentially
<Lumidot 
  pattern="corners-only" 
  rows={4}
  cols={4}
  variant="fuchsia"
/>

// Animates through plus positions
<Lumidot 
  pattern="plus-hollow" 
  rows={3}
  cols={3}
  variant="yellow"
/>

// Spirals around the perimeter
<Lumidot 
  pattern="spiral" 
  rows={4}
  cols={4}
  variant="cyan"
/>

Sequence Frame Calculation

The active dots for the current frame:
const active = React.useMemo(() => {
  if (!isSequence) return new Set<number>(frames[0]);
  if (reduced || frames.length <= 1) return allDots;
  return new Set<number>(frames[frame % frames.length]);
}, [isSequence, frames, reduced, frame, allDots]);
If reduced motion is enabled, all dots from all frames show simultaneously.

Pattern Frame Examples

corners-only (4 frames)

case 'corners-only': {
  const tl = 0;
  const tr = cols - 1;
  const br = total - 1;
  const bl = (rows - 1) * cols;
  return [[tl], [tr], [br], [bl]];
}

spiral (4 frames)

case 'spiral': {
  const frames = spiralFrames(rows, cols);
  return rev ? [...frames].reverse() : frames;
}
function spiralFrames(rows: number, cols: number): number[][] {
  if (rows < 2 || cols < 2) return [[0]];
  return [
    topRow(rows, cols),
    rightCol(rows, cols),
    bottomRow(rows, cols).reverse(),
    leftCol(rows, cols).reverse()
  ];
}

Controlling Duration

The duration prop affects both modes differently:

Wave Mode

// Faster wave propagation
<Lumidot pattern="wave-lr" duration={0.5} rows={3} cols={6} />

// Slower wave propagation
<Lumidot pattern="wave-lr" duration={1.5} rows={3} cols={6} />
The duration is used in waveDelay to calculate the step size between dots.

Sequence Mode

// duration prop doesn't affect frame timing (fixed at 1250ms)
// but affects the CSS transition speed
<Lumidot pattern="corners-only" duration={0.5} />
In sequence mode, frame changes are fixed at 1250ms intervals. The duration prop is applied to CSS transitions but doesn’t control frame timing.

Direction Support

The direction prop controls wave propagation in wave mode:
<Lumidot pattern="all" direction="ltr" rows={3} cols={5} />
Direction types are defined as:
export type LumidotDirection = 'ltr' | 'rtl' | 'ttb' | 'btt';

Performance Considerations

Wave Mode

  • Uses CSS animations with custom properties
  • All dots render simultaneously
  • Efficient for most patterns

Sequence Mode

  • Uses React state updates every 1250ms
  • Re-renders component on frame change
  • More computationally intensive
  • Automatically respects prefers-reduced-motion
For maximum performance with many loaders on screen, prefer wave mode patterns over sequence mode patterns.

Build docs developers (and LLMs) love