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:
Left to Right
Right to Left
Top to Bottom
Bottom to Top
< Lumidot pattern = "all" direction = "ltr" rows = { 3 } cols = { 5 } />
Direction types are defined as:
export type LumidotDirection = 'ltr' | 'rtl' | 'ttb' | 'btt' ;
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.