UI Components Medium
Flickering Grid
A canvas-based animated grid where cells randomly flicker and pulse their opacity, creating a living, breathing background texture.
Open in Lab
MCP
css javascript vue svelte
Targets: TS JS HTML React Vue Svelte
Code
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: system-ui, -apple-system, sans-serif;
min-height: 100vh;
background: #000;
}
/* --- Container --- */
.flicker-container {
position: relative;
width: 100%;
height: 100vh;
display: grid;
place-items: center;
background: #000;
overflow: hidden;
}
/* --- Canvas fills the container --- */
.flicker-canvas {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
display: block;
}
/* --- Centered content overlay --- */
.flicker-content {
position: relative;
z-index: 1;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
gap: 1.25rem;
padding: 2rem;
}
.flicker-glow {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 400px;
height: 400px;
border-radius: 50%;
background: radial-gradient(circle, rgba(16, 185, 129, 0.08) 0%, transparent 70%);
pointer-events: none;
}
.flicker-title {
font-size: 2.75rem;
font-weight: 800;
letter-spacing: -0.03em;
line-height: 1.1;
color: #fafafa;
}
.flicker-description {
font-size: 1rem;
line-height: 1.7;
color: #71717a;
max-width: 400px;
}
.flicker-stats {
display: flex;
align-items: center;
gap: 1.5rem;
margin-top: 0.5rem;
}
.flicker-stat {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.2rem;
}
.flicker-stat-value {
font-size: 1.25rem;
font-weight: 700;
color: #10b981;
}
.flicker-stat-label {
font-size: 0.7rem;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #52525b;
}
.flicker-stat-divider {
width: 1px;
height: 32px;
background: rgba(255, 255, 255, 0.08);
}// Flickering Grid — Canvas-based animated grid with randomly flickering cells.
(function () {
"use strict";
const canvas = document.querySelector(".flicker-canvas");
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
// Configuration
const CELL_SIZE = 24;
const GAP = 2;
const BASE_OPACITY = 0.06;
const MAX_OPACITY = 0.35;
const FLICKER_CHANCE = 0.005; // chance per cell per frame
const LERP_SPEED = 0.04;
const COLOR = { r: 16, g: 185, b: 129 }; // emerald
let cols = 0;
let rows = 0;
let cells = [];
let animId = null;
let dpr = 1;
function resize() {
dpr = window.devicePixelRatio || 1;
const rect = canvas.parentElement.getBoundingClientRect();
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
canvas.style.width = rect.width + "px";
canvas.style.height = rect.height + "px";
ctx.scale(dpr, dpr);
cols = Math.ceil(rect.width / (CELL_SIZE + GAP));
rows = Math.ceil(rect.height / (CELL_SIZE + GAP));
// Re-initialize cells
cells = [];
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
cells.push({
x: c * (CELL_SIZE + GAP),
y: r * (CELL_SIZE + GAP),
opacity: BASE_OPACITY + Math.random() * 0.03,
target: BASE_OPACITY,
});
}
}
}
function animate() {
const w = canvas.width / dpr;
const h = canvas.height / dpr;
ctx.clearRect(0, 0, w, h);
for (let i = 0; i < cells.length; i++) {
const cell = cells[i];
// Random chance to start a flicker
if (Math.random() < FLICKER_CHANCE) {
cell.target = BASE_OPACITY + Math.random() * (MAX_OPACITY - BASE_OPACITY);
}
// Lerp opacity toward target
cell.opacity += (cell.target - cell.opacity) * LERP_SPEED;
// Fade target back to base
cell.target += (BASE_OPACITY - cell.target) * 0.01;
// Draw
ctx.fillStyle = `rgba(${COLOR.r}, ${COLOR.g}, ${COLOR.b}, ${cell.opacity})`;
ctx.beginPath();
ctx.roundRect(cell.x, cell.y, CELL_SIZE, CELL_SIZE, 2);
ctx.fill();
}
animId = requestAnimationFrame(animate);
}
// Initialize
resize();
animate();
// Handle window resize
let resizeTimer;
window.addEventListener("resize", () => {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => {
cancelAnimationFrame(animId);
ctx.setTransform(1, 0, 0, 1, 0, 0);
resize();
animate();
}, 100);
});
// Cleanup on page unload
window.addEventListener("beforeunload", () => {
cancelAnimationFrame(animId);
});
})();<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Flickering Grid</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="flicker-container">
<canvas class="flicker-canvas"></canvas>
<div class="flicker-content">
<div class="flicker-glow"></div>
<h1 class="flicker-title">Flickering Grid</h1>
<p class="flicker-description">
Canvas-based animated grid where cells randomly pulse opacity, creating
a living, breathing background.
</p>
<div class="flicker-stats">
<div class="flicker-stat">
<span class="flicker-stat-value">60</span>
<span class="flicker-stat-label">FPS</span>
</div>
<div class="flicker-stat-divider"></div>
<div class="flicker-stat">
<span class="flicker-stat-value"><2%</span>
<span class="flicker-stat-label">CPU</span>
</div>
<div class="flicker-stat-divider"></div>
<div class="flicker-stat">
<span class="flicker-stat-value">Canvas</span>
<span class="flicker-stat-label">2D API</span>
</div>
</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>import { type CSSProperties, type ReactNode, useCallback, useEffect, useRef } from "react";
interface FlickeringGridProps {
/** Grid cell size in pixels */
cellSize?: number;
/** Gap between cells in pixels */
gap?: number;
/** Base (resting) opacity of each cell */
baseOpacity?: number;
/** Maximum opacity a cell can flicker to */
maxOpacity?: number;
/** Chance per cell per frame to start flickering (0-1) */
flickerChance?: number;
/** Color as {r, g, b} (0-255) */
color?: { r: number; g: number; b: number };
/** Extra CSS class names */
className?: string;
/** Overlay children on top of the grid */
children?: ReactNode;
}
interface Cell {
x: number;
y: number;
opacity: number;
target: number;
}
export function FlickeringGrid({
cellSize = 24,
gap = 2,
baseOpacity = 0.06,
maxOpacity = 0.35,
flickerChance = 0.005,
color = { r: 16, g: 185, b: 129 },
className = "",
children,
}: FlickeringGridProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const animRef = useRef<number>(0);
const cellsRef = useRef<Cell[]>([]);
const lerpSpeed = 0.04;
const setup = useCallback(() => {
const canvas = canvasRef.current;
const container = containerRef.current;
if (!canvas || !container) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
const dpr = window.devicePixelRatio || 1;
const rect = container.getBoundingClientRect();
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
canvas.style.width = rect.width + "px";
canvas.style.height = rect.height + "px";
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
const cols = Math.ceil(rect.width / (cellSize + gap));
const rows = Math.ceil(rect.height / (cellSize + gap));
const cells: Cell[] = [];
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
cells.push({
x: c * (cellSize + gap),
y: r * (cellSize + gap),
opacity: baseOpacity + Math.random() * 0.03,
target: baseOpacity,
});
}
}
cellsRef.current = cells;
const w = rect.width;
const h = rect.height;
function animate() {
ctx!.clearRect(0, 0, w, h);
for (const cell of cellsRef.current) {
if (Math.random() < flickerChance) {
cell.target = baseOpacity + Math.random() * (maxOpacity - baseOpacity);
}
cell.opacity += (cell.target - cell.opacity) * lerpSpeed;
cell.target += (baseOpacity - cell.target) * 0.01;
ctx!.fillStyle = `rgba(${color.r}, ${color.g}, ${color.b}, ${cell.opacity})`;
ctx!.beginPath();
ctx!.roundRect(cell.x, cell.y, cellSize, cellSize, 2);
ctx!.fill();
}
animRef.current = requestAnimationFrame(animate);
}
cancelAnimationFrame(animRef.current);
animate();
}, [cellSize, gap, baseOpacity, maxOpacity, flickerChance, color]);
useEffect(() => {
setup();
let timer: ReturnType<typeof setTimeout>;
const handleResize = () => {
clearTimeout(timer);
timer = setTimeout(setup, 100);
};
window.addEventListener("resize", handleResize);
return () => {
cancelAnimationFrame(animRef.current);
window.removeEventListener("resize", handleResize);
clearTimeout(timer);
};
}, [setup]);
const containerStyle: CSSProperties = {
position: "relative",
width: "100%",
height: "100vh",
display: "grid",
placeItems: "center",
background: "#000",
overflow: "hidden",
};
const canvasStyle: CSSProperties = {
position: "absolute",
inset: 0,
display: "block",
};
const contentStyle: CSSProperties = {
position: "relative",
zIndex: 1,
};
return (
<div ref={containerRef} className={className} style={containerStyle}>
<canvas ref={canvasRef} style={canvasStyle} />
<div style={contentStyle}>{children}</div>
</div>
);
}
// Demo usage
export default function FlickeringGridDemo() {
return (
<FlickeringGrid
cellSize={24}
gap={2}
baseOpacity={0.06}
maxOpacity={0.35}
flickerChance={0.005}
color={{ r: 16, g: 185, b: 129 }}
>
<div
style={{
textAlign: "center",
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: "1.25rem",
padding: "2rem",
}}
>
<div
style={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: 400,
height: 400,
borderRadius: "50%",
background: "radial-gradient(circle, rgba(16,185,129,0.08) 0%, transparent 70%)",
pointerEvents: "none",
}}
/>
<h1
style={{
fontSize: "2.75rem",
fontWeight: 800,
letterSpacing: "-0.03em",
lineHeight: 1.1,
color: "#fafafa",
fontFamily: "system-ui, -apple-system, sans-serif",
}}
>
Flickering Grid
</h1>
<p
style={{
fontSize: "1rem",
lineHeight: 1.7,
color: "#71717a",
maxWidth: 400,
fontFamily: "system-ui, -apple-system, sans-serif",
}}
>
Canvas-based animated grid where cells randomly pulse opacity, creating a living,
breathing background.
</p>
<div style={{ display: "flex", alignItems: "center", gap: "1.5rem" }}>
{[
{ value: "60", label: "FPS" },
{ value: "<2%", label: "CPU" },
{ value: "Canvas", label: "2D API" },
].map((stat, i, arr) => (
<div key={stat.label} style={{ display: "contents" }}>
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: "0.2rem",
}}
>
<span
style={{
fontSize: "1.25rem",
fontWeight: 700,
color: "#10b981",
}}
>
{stat.value}
</span>
<span
style={{
fontSize: "0.7rem",
fontWeight: 500,
textTransform: "uppercase",
letterSpacing: "0.08em",
color: "#52525b",
}}
>
{stat.label}
</span>
</div>
{i < arr.length - 1 && (
<div
style={{
width: 1,
height: 32,
background: "rgba(255,255,255,0.08)",
}}
/>
)}
</div>
))}
</div>
</div>
</FlickeringGrid>
);
}<script setup>
import { ref, onMounted, onUnmounted } from "vue";
const props = defineProps({
cellSize: { type: Number, default: 24 },
gap: { type: Number, default: 2 },
baseOpacity: { type: Number, default: 0.06 },
maxOpacity: { type: Number, default: 0.35 },
flickerChance: { type: Number, default: 0.005 },
color: { type: Object, default: () => ({ r: 16, g: 185, b: 129 }) },
});
const containerEl = ref(null);
const canvasEl = ref(null);
let animId = 0;
let cells = [];
const lerpSpeed = 0.04;
function setup() {
const canvas = canvasEl.value;
const container = containerEl.value;
if (!canvas || !container) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
const dpr = window.devicePixelRatio || 1;
const rect = container.getBoundingClientRect();
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
canvas.style.width = rect.width + "px";
canvas.style.height = rect.height + "px";
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
const cols = Math.ceil(rect.width / (props.cellSize + props.gap));
const rows = Math.ceil(rect.height / (props.cellSize + props.gap));
cells = [];
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
cells.push({
x: c * (props.cellSize + props.gap),
y: r * (props.cellSize + props.gap),
opacity: props.baseOpacity + Math.random() * 0.03,
target: props.baseOpacity,
});
}
}
const w = rect.width;
const h = rect.height;
function animate() {
ctx.clearRect(0, 0, w, h);
for (const cell of cells) {
if (Math.random() < props.flickerChance) {
cell.target = props.baseOpacity + Math.random() * (props.maxOpacity - props.baseOpacity);
}
cell.opacity += (cell.target - cell.opacity) * lerpSpeed;
cell.target += (props.baseOpacity - cell.target) * 0.01;
ctx.fillStyle = `rgba(${props.color.r}, ${props.color.g}, ${props.color.b}, ${cell.opacity})`;
ctx.beginPath();
ctx.roundRect(cell.x, cell.y, props.cellSize, props.cellSize, 2);
ctx.fill();
}
animId = requestAnimationFrame(animate);
}
cancelAnimationFrame(animId);
animate();
}
let resizeTimer;
function handleResize() {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(setup, 100);
}
onMounted(() => {
setup();
window.addEventListener("resize", handleResize);
});
onUnmounted(() => {
cancelAnimationFrame(animId);
window.removeEventListener("resize", handleResize);
clearTimeout(resizeTimer);
});
const stats = [
{ value: "60", label: "FPS" },
{ value: "<2%", label: "CPU" },
{ value: "Canvas", label: "2D API" },
];
</script>
<template>
<div ref="containerEl" class="flickering-container">
<canvas ref="canvasEl" class="flickering-canvas"></canvas>
<div class="flickering-content">
<slot>
<div class="demo-inner">
<div class="demo-glow"></div>
<h1 class="demo-title">Flickering Grid</h1>
<p class="demo-desc">
Canvas-based animated grid where cells randomly pulse opacity, creating
a living, breathing background.
</p>
<div class="demo-stats">
<template v-for="(stat, i) in stats" :key="stat.label">
<div class="stat-item">
<span class="stat-value">{{ stat.value }}</span>
<span class="stat-label">{{ stat.label }}</span>
</div>
<div v-if="i < stats.length - 1" class="stat-divider"></div>
</template>
</div>
</div>
</slot>
</div>
</div>
</template>
<style scoped>
.flickering-container {
position: relative;
width: 100%;
height: 100vh;
display: grid;
place-items: center;
background: #000;
overflow: hidden;
}
.flickering-canvas {
position: absolute;
inset: 0;
display: block;
}
.flickering-content {
position: relative;
z-index: 1;
}
.demo-inner {
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
gap: 1.25rem;
padding: 2rem;
position: relative;
}
.demo-glow {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 400px;
height: 400px;
border-radius: 50%;
background: radial-gradient(circle, rgba(16,185,129,0.08) 0%, transparent 70%);
pointer-events: none;
}
.demo-title {
font-size: 2.75rem;
font-weight: 800;
letter-spacing: -0.03em;
line-height: 1.1;
color: #fafafa;
font-family: system-ui, -apple-system, sans-serif;
}
.demo-desc {
font-size: 1rem;
line-height: 1.7;
color: #71717a;
max-width: 400px;
font-family: system-ui, -apple-system, sans-serif;
}
.demo-stats {
display: flex;
align-items: center;
gap: 1.5rem;
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.2rem;
}
.stat-value { font-size: 1.25rem; font-weight: 700; color: #10b981; }
.stat-label {
font-size: 0.7rem;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #52525b;
}
.stat-divider { width: 1px; height: 32px; background: rgba(255,255,255,0.08); }
</style><script>
import { onMount, onDestroy } from "svelte";
export let cellSize = 24;
export let gap = 2;
export let baseOpacity = 0.06;
export let maxOpacity = 0.35;
export let flickerChance = 0.005;
export let color = { r: 16, g: 185, b: 129 };
let containerEl;
let canvasEl;
let animId = 0;
let cells = [];
const lerpSpeed = 0.04;
function setup() {
if (!canvasEl || !containerEl) return;
const ctx = canvasEl.getContext("2d");
if (!ctx) return;
const dpr = window.devicePixelRatio || 1;
const rect = containerEl.getBoundingClientRect();
canvasEl.width = rect.width * dpr;
canvasEl.height = rect.height * dpr;
canvasEl.style.width = rect.width + "px";
canvasEl.style.height = rect.height + "px";
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
const cols = Math.ceil(rect.width / (cellSize + gap));
const rows = Math.ceil(rect.height / (cellSize + gap));
cells = [];
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
cells.push({
x: c * (cellSize + gap),
y: r * (cellSize + gap),
opacity: baseOpacity + Math.random() * 0.03,
target: baseOpacity,
});
}
}
const w = rect.width;
const h = rect.height;
function animate() {
ctx.clearRect(0, 0, w, h);
for (const cell of cells) {
if (Math.random() < flickerChance) {
cell.target = baseOpacity + Math.random() * (maxOpacity - baseOpacity);
}
cell.opacity += (cell.target - cell.opacity) * lerpSpeed;
cell.target += (baseOpacity - cell.target) * 0.01;
ctx.fillStyle = `rgba(${color.r}, ${color.g}, ${color.b}, ${cell.opacity})`;
ctx.beginPath();
ctx.roundRect(cell.x, cell.y, cellSize, cellSize, 2);
ctx.fill();
}
animId = requestAnimationFrame(animate);
}
cancelAnimationFrame(animId);
animate();
}
let resizeTimer;
function handleResize() {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(setup, 100);
}
onMount(() => {
setup();
window.addEventListener("resize", handleResize);
});
onDestroy(() => {
cancelAnimationFrame(animId);
window.removeEventListener("resize", handleResize);
clearTimeout(resizeTimer);
});
</script>
<div bind:this={containerEl} class="flickering-container">
<canvas bind:this={canvasEl} class="flickering-canvas"></canvas>
<div class="flickering-content">
<slot>
<div class="demo-inner">
<div class="demo-glow"></div>
<h1 class="demo-title">Flickering Grid</h1>
<p class="demo-desc">
Canvas-based animated grid where cells randomly pulse opacity, creating
a living, breathing background.
</p>
<div class="demo-stats">
{#each [{ value: '60', label: 'FPS' }, { value: '<2%', label: 'CPU' }, { value: 'Canvas', label: '2D API' }] as stat, i}
<div class="stat-item">
<span class="stat-value">{stat.value}</span>
<span class="stat-label">{stat.label}</span>
</div>
{#if i < 2}
<div class="stat-divider"></div>
{/if}
{/each}
</div>
</div>
</slot>
</div>
</div>
<style>
.flickering-container {
position: relative;
width: 100%;
height: 100vh;
display: grid;
place-items: center;
background: #000;
overflow: hidden;
}
.flickering-canvas {
position: absolute;
inset: 0;
display: block;
}
.flickering-content {
position: relative;
z-index: 1;
}
.demo-inner {
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
gap: 1.25rem;
padding: 2rem;
position: relative;
}
.demo-glow {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 400px;
height: 400px;
border-radius: 50%;
background: radial-gradient(circle, rgba(16,185,129,0.08) 0%, transparent 70%);
pointer-events: none;
}
.demo-title {
font-size: 2.75rem;
font-weight: 800;
letter-spacing: -0.03em;
line-height: 1.1;
color: #fafafa;
font-family: system-ui, -apple-system, sans-serif;
}
.demo-desc {
font-size: 1rem;
line-height: 1.7;
color: #71717a;
max-width: 400px;
font-family: system-ui, -apple-system, sans-serif;
}
.demo-stats {
display: flex;
align-items: center;
gap: 1.5rem;
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.2rem;
}
.stat-value { font-size: 1.25rem; font-weight: 700; color: #10b981; }
.stat-label {
font-size: 0.7rem;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #52525b;
}
.stat-divider { width: 1px; height: 32px; background: rgba(255,255,255,0.08); }
</style>Flickering Grid
A canvas-based animated grid where individual cells randomly pulse and flicker their opacity, producing a mesmerizing, living background texture.
How it works
- A
<canvas>element fills its container. - On each animation frame, each cell has a small random chance of beginning a flicker transition.
- Cells lerp their opacity toward a random target, then fade back, creating an organic breathing rhythm.
requestAnimationFramekeeps the animation smooth and battery-friendly.
Performance
The animation uses a cell-based approach rather than per-pixel rendering, keeping the draw count low. Each frame only redraws cells whose opacity has changed, making it lightweight even on large screens.
When to use it
- Background for hero sections or dashboards
- Decorative layer behind modals or panels
- Ambient texture for creative portfolios
- Generative art experiments