Dot Matrix Ticker
Turn plain text into a configurable LED matrix, one Tailwind layer at a time.

Faisal Husain
Published
Ship interfaces that feel fast.
Plain text
Begin with one readable monospace sentence.
Customize
Display online
DM–01Display controls
Tune the message, matrix, and movement.
Color
Matrix shape
Tailwind animation
// tailwind.config.ts
export default {
theme: {
extend: {
keyframes: {
'dot-matrix-ticker-scroll': {
to: { transform: 'translate3d(-50%, 0, 0)' },
},
},
animation: {
'dot-matrix-ticker':
'dot-matrix-ticker-scroll var(--ticker-duration) steps(var(--ticker-steps), end) infinite',
},
},
},
}Ready component
'use client'
import {
type CSSProperties,
useCallback,
useEffect,
useRef,
useState,
} from 'react'
const COLORS = ['#4ade80', '#fb923c', '#f87171', '#60a5fa', '#f8fafc']
type DotShape = 'circle' | 'square' | 'diamond'
type DotMatrixTickerProps = {
text: string
speed?: number
steps?: number
color?: string
glow?: number
dotShape?: DotShape
dotSize?: number
dotSpacing?: number
className?: string
}
type TickerStyle = CSSProperties & {
'--ticker-color': string
'--ticker-duration': string
'--ticker-steps': number
'--ticker-glow-shadow': string
}
export function DotMatrixTicker({
text,
speed = 2,
steps = 200,
color,
glow = 1,
dotShape = 'circle',
dotSize = 3,
dotSpacing = 1,
className = '',
}: DotMatrixTickerProps) {
const trackRef = useRef<HTMLDivElement>(null)
const [duration, setDuration] = useState(12)
const [colorIndex, setColorIndex] = useState(0)
const displayText = `${text.trim().toUpperCase() || 'DOT MATRIX TICKER'} `
const updateDuration = useCallback(() => {
const track = trackRef.current
if (!track) return
const normalizedSpeed = Math.min(Math.max(speed, 0), 10)
if (normalizedSpeed === 0) return
setDuration(track.scrollWidth / 2 / (normalizedSpeed * 100))
}, [speed])
useEffect(() => {
updateDuration()
const track = trackRef.current
if (!track) return
const observer = new ResizeObserver(updateDuration)
observer.observe(track)
return () => observer.disconnect()
}, [displayText, updateDuration])
const canChangeColor = color === undefined
const currentColor = color ?? COLORS[colorIndex]
const glowStrength = Math.min(Math.max(glow, 0), 3)
const glowShadow = glowStrength === 0
? 'none'
: `0 0 ${4 * glowStrength}px currentColor,
0 0 ${9 * glowStrength}px currentColor,
0 0 ${16 * glowStrength}px currentColor`
const size = Math.min(Math.max(dotSize, 1), 6)
const spacing = Math.min(Math.max(dotSpacing, 0.5), 4)
const center = size / 2
const masks: Record<DotShape, string> = {
circle: `radial-gradient(circle at ${center}px ${center}px,
transparent 0 ${center}px, #000 ${center + 0.2}px)`,
square: `conic-gradient(from 90deg at ${size}px ${size}px,
transparent 25%, #000 0)`,
diamond: `linear-gradient(45deg, #000 0 32%, transparent 32% 68%, #000 68%),
linear-gradient(-45deg, #000 0 32%, transparent 32% 68%, #000 68%)`,
}
const maskStyle: CSSProperties = {
backgroundColor: '#000',
maskImage: masks[dotShape],
WebkitMaskImage: masks[dotShape],
maskSize: `${size + spacing}px ${size + spacing}px`,
WebkitMaskSize: `${size + spacing}px ${size + spacing}px`,
}
const style: TickerStyle = {
'--ticker-color': currentColor,
'--ticker-duration': `${duration}s`,
'--ticker-steps': Math.max(1, Math.round(steps)),
'--ticker-glow-shadow': glowShadow,
}
const cycleColor = () => {
if (canChangeColor) {
setColorIndex((current) => (current + 1) % COLORS.length)
}
}
return (
<div
role={canChangeColor ? 'button' : 'img'}
tabIndex={canChangeColor ? 0 : undefined}
aria-label={`Ticker displaying “${text}”.`}
onClick={cycleColor}
onKeyDown={(event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault()
cycleColor()
}
}}
style={style}
className={`relative w-full overflow-hidden rounded-xl border
border-white/10 bg-black shadow-[inset_0_0_16px_rgba(0,0,0,0.9),
0_8px_30px_rgba(0,0,0,0.35)] outline-none
focus-visible:ring-2 focus-visible:ring-white/70 ${className}`}
>
<div className="relative overflow-hidden [mask-image:linear-gradient(to_right,transparent,black_12%,black_88%,transparent)]">
<div className="pointer-events-none absolute inset-0 z-20 bg-[linear-gradient(to_bottom,rgba(255,255,255,0.16),transparent_35%,transparent_65%,rgba(255,255,255,0.08))]" />
<div className="pointer-events-none absolute -left-[10%] -top-[80%] z-20 h-[220%] w-full -skew-x-[55deg] bg-[linear-gradient(10deg,transparent_35%,rgba(255,255,255,0.16),transparent_65%)] blur-xl" />
<div className="relative py-3 sm:py-4">
<div
ref={trackRef}
aria-hidden="true"
className={`flex w-max animate-dot-matrix-ticker whitespace-nowrap
text-[var(--ticker-color)] will-change-transform
motion-reduce:animate-none
${speed <= 0 ? '[animation-play-state:paused]' : ''}`}
>
{[0, 1].map((copy) => (
<span
key={copy}
className="pr-8 font-mono text-3xl font-medium leading-none tracking-[0.08em] [text-shadow:var(--ticker-glow-shadow)] sm:text-4xl"
>
{displayText}
</span>
))}
</div>
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0 z-10"
style={maskStyle}
/>
</div>
</div>
</div>
)
}Let's work together
Have a product problem worth solving?
I help teams build fast, dependable products with thoughtful interfaces and strong engineering foundations.