Dot Matrix Ticker
Turn plain text into a configurable LED matrix, one Tailwind layer at a time.
- Author
- Faisal Husain
- Published
- Reading time
- 3 min read
- Share
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.