LCD ClockA retro 7-segment LCD clock with pixel-grid overlay.
'use client'
import { useState, useEffect } from 'react'
// ─── Colors ───────────────────────────────────────────────────────────────────
const CYAN = '#55E8E2'
const CYAN_OFF = 'rgba(85,232,226,0.055)' // inactive digit segments — barely visible
const CYAN_IDLE = 'rgba(85,232,226,0.28)' // inactive text labels (AM/PM, days)
const CYAN_DIM = 'rgba(85,232,226,0.65)' // date text
const BG = '#060a0a'
const GLOW_SVG = `drop-shadow(0 0 3px ${CYAN}) drop-shadow(0 0 8px ${CYAN}99)`
const GLOW_TXT = `0 0 5px ${CYAN}, 0 0 11px ${CYAN}88`
// ─── 7-segment geometry (viewBox 0 0 42 80) ─────────────────────────────────
const VW = 42, VH = 80
const T = 6 // segment thickness
const BV = 3 // bevel at each segment tip
const GP = 2 // gap between segments and digit edges
function hPts(y: number): string {
const x1 = GP + BV, x2 = VW - GP - BV, cy = y + T / 2
return `${GP},${cy} ${x1},${y} ${x2},${y} ${VW-GP},${cy} ${x2},${y+T} ${x1},${y+T}`
}
function vPts(x: number, y1: number, y2: number): string {
const cx = x + T / 2
return `${cx},${y1} ${x+T},${y1+BV} ${x+T},${y2-BV} ${cx},${y2} ${x},${y2-BV} ${x},${y1+BV}`
}
const aY = GP, gY = VH / 2 - T / 2, dY = VH - GP - T
const lX = GP, rX = VW - GP - T
// Verticals start at aY and end at dY+T — same outer edges as a and d.
// This makes every digit span the full height, so "1" and "4" match "0" and "8".
const aEnd = aY, gTop = gY - GP
const gEnd = gY + T + GP, dTop = dY + T
const SHAPES = [
hPts(aY), // a — top horiz
vPts(rX, aEnd, gTop), // b — top-right vert
vPts(rX, gEnd, dTop), // c — bot-right vert
hPts(dY), // d — bot horiz
vPts(lX, gEnd, dTop), // e — bot-left vert
vPts(lX, aEnd, gTop), // f — top-left vert
hPts(gY), // g — mid horiz
]
// segment on/off per digit [a, b, c, d, e, f, g ]
const SEG: Record<string, boolean[]> = {
'0': [true, true, true, true, true, true, false],
'1': [false, true, true, false, false, false, false],
'2': [true, true, false, true, true, false, true ],
'3': [true, true, true, true, false, false, true ],
'4': [false, true, true, false, false, true, true ],
'5': [true, false, true, true, false, true, true ],
'6': [true, false, true, true, true, true, true ],
'7': [true, true, true, false, false, false, false],
'8': [true, true, true, true, true, true, true ],
'9': [true, true, true, true, false, true, true ],
}
// ─── Digit ────────────────────────────────────────────────────────────────────
function Digit({ char, size }: { char: string; size: number }) {
const segs = SEG[char] ?? SEG['8']
return (
<svg
viewBox={`0 0 ${VW} ${VH}`}
width={size}
height={Math.round(size * VH / VW)}
style={{ display: 'block', overflow: 'visible', flexShrink: 0 }}
>
{SHAPES.map((pts, i) => (
<polygon
key={i}
points={pts}
fill={segs[i] ? CYAN : CYAN_OFF}
style={segs[i] ? { filter: GLOW_SVG } : undefined}
/>
))}
</svg>
)
}
// ─── Colon dots ───────────────────────────────────────────────────────────────
function ColonDots({ dim, size }: { dim: boolean; size: number }) {
return (
<svg
viewBox={`0 0 ${VW * 0.44} ${VH}`}
width={Math.round(size * 0.44)}
height={Math.round(size * VH / VW)}
style={{ display: 'block', overflow: 'visible', flexShrink: 0 }}
>
{([0.30, 0.68] as const).map((yf, i) => (
<circle
key={i}
cx={VW * 0.22}
cy={VH * yf}
r={VW * 0.125}
fill={dim ? CYAN_OFF : CYAN}
style={dim ? undefined : { filter: GLOW_SVG }}
/>
))}
</svg>
)
}
// ─── Helpers ──────────────────────────────────────────────────────────────────
const pad2 = (n: number) => n.toString().padStart(2, '0')
const DAYS = ['SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT'] as const
function getNow() {
const d = new Date()
const h = d.getHours()
return {
h: pad2(h % 12 || 12),
m: pad2(d.getMinutes()),
s: pad2(d.getSeconds()),
isPM: h >= 12,
dow: d.getDay(),
date: d.toLocaleDateString('en-US', {
month: 'long', day: 'numeric', year: 'numeric',
}).toUpperCase(),
}
}
// ─── Size constants ───────────────────────────────────────────────────────────
const BIG = 50 // HH MM digit px width
const SML = 24 // SS digit px width (~half of BIG)
const GAP = 3 // gap between all flex children
const COLON_BIG = Math.round(BIG * 0.44) // = 22 (steady colon, same size as BIG)
const COLON_SML = Math.round(SML * 0.44) // = 11 (blinking colon, same size as SML)
// Width of the HH:MM:SS display (8 items, 7 gaps) — days row matches this exactly
const TIME_W = 4 * BIG + COLON_BIG + COLON_SML + 2 * SML + 7 * GAP
// = 200 + 22 + 11 + 48 + 21 = 302
// ─── NeonClock ────────────────────────────────────────────────────────────────
export default function NeonClock() {
const [now, setNow] = useState(getNow)
const [colonOn, setColonOn] = useState(true)
useEffect(() => {
const id = setInterval(() => {
setNow(getNow())
setColonOn(c => !c)
}, 1000)
return () => clearInterval(id)
}, [])
return (
<div
className="relative flex min-h-screen w-full select-none items-center justify-center overflow-hidden"
style={{ background: BG, fontFamily: '"Courier New", Courier, monospace' }}
>
{/* ── Left-anchored block — centered as a whole in the viewport ─────── */}
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start' }}>
{/* ── Time row — everything bottom-aligned ────────────────────────── */}
<div style={{ display: 'flex', alignItems: 'flex-end', gap: GAP }}>
<Digit char={now.h[0]} size={BIG} />
<Digit char={now.h[1]} size={BIG} />
<ColonDots dim={false} size={BIG} />
<Digit char={now.m[0]} size={BIG} />
<Digit char={now.m[1]} size={BIG} />
<ColonDots dim={!colonOn} size={SML} />
{/* Seconds + AM/PM stacked — AM/PM floats above the SS digits, right-aligned */}
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end' }}>
<div style={{ display: 'flex', gap: 6, fontSize: 13, letterSpacing: '0.08em', marginBottom: 3 }}>
<span style={{ color: !now.isPM ? CYAN : CYAN_IDLE, textShadow: !now.isPM ? GLOW_TXT : 'none' }}>AM</span>
<span style={{ color: now.isPM ? CYAN : CYAN_IDLE, textShadow: now.isPM ? GLOW_TXT : 'none' }}>PM</span>
</div>
<div style={{ display: 'flex', gap: GAP }}>
<Digit char={now.s[0]} size={SML} />
<Digit char={now.s[1]} size={SML} />
</div>
</div>
</div>
{/* ── Days row — spans exactly TIME_W, evenly distributed ──────────── */}
<div
style={{
display: 'flex',
justifyContent: 'space-between',
width: TIME_W,
marginTop: 13,
fontSize: 13,
letterSpacing: '0.05em',
}}
>
{DAYS.map((d, i) => (
<span
key={d}
style={{
color: i === now.dow ? CYAN : CYAN_IDLE,
textShadow: i === now.dow ? GLOW_TXT : 'none',
}}
>
{d}
</span>
))}
</div>
{/* ── Date — centered under the days row ───────────────────────────── */}
<div
style={{
width: TIME_W,
marginTop: 5,
textAlign: 'center',
fontSize: 13,
letterSpacing: '0.1em',
color: CYAN_DIM,
}}
>
{now.date}
</div>
</div>
{/* ── LCD pixel-grid overlay ───────────────────────────────────────────── */}
<div
className="pointer-events-none absolute inset-0"
style={{
backgroundImage: 'radial-gradient(circle, rgba(0,0,0,0.48) 1.3px, transparent 1.3px)',
backgroundSize: '3.8px 3.8px',
}}
/>
</div>
)
}
Add to your project
One command adds this component to your project.
Run the following command. New project? Run npx shadcn@latest init first to set up Tailwind and path aliases.
npx shadcn@latest add @aicanvas/neon-clockFor dark mode, add the dark class to your <html> element:
<html class="dark">Install with AI Canvas MCP
With AI Canvas MCP, your AI knows every component we ship. Ask for “a navigation component from AI Canvas” inside Claude Code, Codex, or Cursor and it can suggest you a few options, then install the one you like. Less typing, lower token cost, modern way to build.
Get MCPAbout LCD Clock
LCD Clock is a retro 7-segment clock built from CSS alone, with a faint pixel-grid overlay that gives the digits a real vintage-display feel. It shows live hours, minutes, and seconds with a blinking colon, plus AM/PM, a row for the day of the week, and the full date underneath. Because there is no canvas or external font, the whole thing renders instantly and styles cleanly with Tailwind. It is a strong fit for retro game UI, terminal-themed dashboards, or portfolio sites leaning on a nostalgic palette.


