A dithered pixel-field background for React with a customizable color ramp. Great for hero sections and landing pages.
import { PixelBackground } from "@/components/pixel-background";
return (
<div className="relative h-80 overflow-hidden rounded-2xl">
<PixelBackground />
</div>
)Copy and paste the following code into your project. Each block is the same component with a different default color ramp.
import { useMemo } from "react";
import { cn } from "@/lib/utils";
const DEFAULT_COLUMNS = 56;
const DEFAULT_ROWS = 18;
const DEFAULT_GRAIN = 0.2;
const DEFAULT_ORIGIN_X = 0.52;
const DEFAULT_ORIGIN_Y = 0.42;
const RADIAL_STRETCH_X = 1.35;
const RADIAL_STRETCH_Y = 1.15;
const RADIAL_SCALE = 0.92;
const RADIAL_BIAS = 0.06;
const DEFAULT_COLORS = [
"#EAF4FF",
"#C9E4FF",
"#9DCEFF",
"#6BB3F7",
"#3D94E8",
"#2B78D4",
"#1F63B8",
] as const;
type PixelBackgroundProps = React.ComponentProps<"div"> & {
colors?: readonly string[];
columns?: number;
grain?: number;
originX?: number;
originY?: number;
rows?: number;
};
function PixelBackground({
className,
colors = DEFAULT_COLORS,
columns = DEFAULT_COLUMNS,
grain = DEFAULT_GRAIN,
originX = DEFAULT_ORIGIN_X,
originY = DEFAULT_ORIGIN_Y,
rows = DEFAULT_ROWS,
...props
}: PixelBackgroundProps) {
const safeColumns = Math.max(1, Math.round(columns));
const safeRows = Math.max(1, Math.round(rows));
const palette = colors.length > 0 ? colors : DEFAULT_COLORS;
const colorKey = palette.join(",");
const cells = useMemo(
() =>
buildPixelCells({
colors: colorKey.split(","),
columns: safeColumns,
grain,
originX,
originY,
rows: safeRows,
}),
[colorKey, grain, originX, originY, safeColumns, safeRows]
);
return (
<div
aria-hidden="true"
className={cn(
"pointer-events-none absolute inset-0 overflow-hidden",
className
)}
style={{
backgroundColor: palette[Math.floor(palette.length / 2)],
}}
{...props}
>
<svg
className="size-full"
preserveAspectRatio="none"
viewBox={`0 0 ${safeColumns} ${safeRows}`}
>
<title>Pixel background</title>
{cells.map((cell) => (
<rect
fill={cell.fill}
height={1}
key={`${cell.x}-${cell.y}`}
width={1}
x={cell.x}
y={cell.y}
/>
))}
</svg>
</div>
);
}
function buildPixelCells({
colors,
columns,
grain,
originX,
originY,
rows,
}: {
colors: readonly string[];
columns: number;
grain: number;
originX: number;
originY: number;
rows: number;
}) {
const lastCol = Math.max(columns - 1, 1);
const lastRow = Math.max(rows - 1, 1);
const lastStop = colors.length - 1;
return Array.from({ length: rows }, (_, y) =>
Array.from({ length: columns }, (_, x) => {
const nx = x / lastCol - originX;
const ny = y / lastRow - originY;
const radial = Math.hypot(nx * RADIAL_STRETCH_X, ny * RADIAL_STRETCH_Y);
const grainValue = ((x * 17 + y * 31) % 7) / 7;
const t = Math.min(
1,
Math.max(0, radial * RADIAL_SCALE + grainValue * grain - RADIAL_BIAS)
);
const stopIndex = Math.min(lastStop, Math.floor(t * colors.length));
return {
fill: colors[stopIndex] ?? colors[0],
x,
y,
};
})
).flat();
}
export { DEFAULT_COLORS, PixelBackground };
export type { PixelBackgroundProps };import { useMemo } from "react";
import { cn } from "@/lib/utils";
const DEFAULT_COLUMNS = 56;
const DEFAULT_ROWS = 18;
const DEFAULT_GRAIN = 0.2;
const DEFAULT_ORIGIN_X = 0.52;
const DEFAULT_ORIGIN_Y = 0.42;
const RADIAL_STRETCH_X = 1.35;
const RADIAL_STRETCH_Y = 1.15;
const RADIAL_SCALE = 0.92;
const RADIAL_BIAS = 0.06;
const DEFAULT_COLORS = [
"#FFE8D6",
"#FFC9A8",
"#F4A261",
"#E76F51",
"#C44536",
"#9B2226",
] as const;
type PixelBackgroundProps = React.ComponentProps<"div"> & {
colors?: readonly string[];
columns?: number;
grain?: number;
originX?: number;
originY?: number;
rows?: number;
};
function PixelBackground({
className,
colors = DEFAULT_COLORS,
columns = DEFAULT_COLUMNS,
grain = DEFAULT_GRAIN,
originX = DEFAULT_ORIGIN_X,
originY = DEFAULT_ORIGIN_Y,
rows = DEFAULT_ROWS,
...props
}: PixelBackgroundProps) {
const safeColumns = Math.max(1, Math.round(columns));
const safeRows = Math.max(1, Math.round(rows));
const palette = colors.length > 0 ? colors : DEFAULT_COLORS;
const colorKey = palette.join(",");
const cells = useMemo(
() =>
buildPixelCells({
colors: colorKey.split(","),
columns: safeColumns,
grain,
originX,
originY,
rows: safeRows,
}),
[colorKey, grain, originX, originY, safeColumns, safeRows]
);
return (
<div
aria-hidden="true"
className={cn(
"pointer-events-none absolute inset-0 overflow-hidden",
className
)}
style={{
backgroundColor: palette[Math.floor(palette.length / 2)],
}}
{...props}
>
<svg
className="size-full"
preserveAspectRatio="none"
viewBox={`0 0 ${safeColumns} ${safeRows}`}
>
<title>Pixel background</title>
{cells.map((cell) => (
<rect
fill={cell.fill}
height={1}
key={`${cell.x}-${cell.y}`}
width={1}
x={cell.x}
y={cell.y}
/>
))}
</svg>
</div>
);
}
function buildPixelCells({
colors,
columns,
grain,
originX,
originY,
rows,
}: {
colors: readonly string[];
columns: number;
grain: number;
originX: number;
originY: number;
rows: number;
}) {
const lastCol = Math.max(columns - 1, 1);
const lastRow = Math.max(rows - 1, 1);
const lastStop = colors.length - 1;
return Array.from({ length: rows }, (_, y) =>
Array.from({ length: columns }, (_, x) => {
const nx = x / lastCol - originX;
const ny = y / lastRow - originY;
const radial = Math.hypot(nx * RADIAL_STRETCH_X, ny * RADIAL_STRETCH_Y);
const grainValue = ((x * 17 + y * 31) % 7) / 7;
const t = Math.min(
1,
Math.max(0, radial * RADIAL_SCALE + grainValue * grain - RADIAL_BIAS)
);
const stopIndex = Math.min(lastStop, Math.floor(t * colors.length));
return {
fill: colors[stopIndex] ?? colors[0],
x,
y,
};
})
).flat();
}
export { DEFAULT_COLORS, PixelBackground };
export type { PixelBackgroundProps };import { useMemo } from "react";
import { cn } from "@/lib/utils";
const DEFAULT_COLUMNS = 56;
const DEFAULT_ROWS = 18;
const DEFAULT_GRAIN = 0.2;
const DEFAULT_ORIGIN_X = 0.52;
const DEFAULT_ORIGIN_Y = 0.42;
const RADIAL_STRETCH_X = 1.35;
const RADIAL_STRETCH_Y = 1.15;
const RADIAL_SCALE = 0.92;
const RADIAL_BIAS = 0.06;
const DEFAULT_COLORS = [
"#ECFDF3",
"#BBF7D0",
"#4ADE80",
"#22C55E",
"#15803D",
"#14532D",
] as const;
type PixelBackgroundProps = React.ComponentProps<"div"> & {
colors?: readonly string[];
columns?: number;
grain?: number;
originX?: number;
originY?: number;
rows?: number;
};
function PixelBackground({
className,
colors = DEFAULT_COLORS,
columns = DEFAULT_COLUMNS,
grain = DEFAULT_GRAIN,
originX = DEFAULT_ORIGIN_X,
originY = DEFAULT_ORIGIN_Y,
rows = DEFAULT_ROWS,
...props
}: PixelBackgroundProps) {
const safeColumns = Math.max(1, Math.round(columns));
const safeRows = Math.max(1, Math.round(rows));
const palette = colors.length > 0 ? colors : DEFAULT_COLORS;
const colorKey = palette.join(",");
const cells = useMemo(
() =>
buildPixelCells({
colors: colorKey.split(","),
columns: safeColumns,
grain,
originX,
originY,
rows: safeRows,
}),
[colorKey, grain, originX, originY, safeColumns, safeRows]
);
return (
<div
aria-hidden="true"
className={cn(
"pointer-events-none absolute inset-0 overflow-hidden",
className
)}
style={{
backgroundColor: palette[Math.floor(palette.length / 2)],
}}
{...props}
>
<svg
className="size-full"
preserveAspectRatio="none"
viewBox={`0 0 ${safeColumns} ${safeRows}`}
>
<title>Pixel background</title>
{cells.map((cell) => (
<rect
fill={cell.fill}
height={1}
key={`${cell.x}-${cell.y}`}
width={1}
x={cell.x}
y={cell.y}
/>
))}
</svg>
</div>
);
}
function buildPixelCells({
colors,
columns,
grain,
originX,
originY,
rows,
}: {
colors: readonly string[];
columns: number;
grain: number;
originX: number;
originY: number;
rows: number;
}) {
const lastCol = Math.max(columns - 1, 1);
const lastRow = Math.max(rows - 1, 1);
const lastStop = colors.length - 1;
return Array.from({ length: rows }, (_, y) =>
Array.from({ length: columns }, (_, x) => {
const nx = x / lastCol - originX;
const ny = y / lastRow - originY;
const radial = Math.hypot(nx * RADIAL_STRETCH_X, ny * RADIAL_STRETCH_Y);
const grainValue = ((x * 17 + y * 31) % 7) / 7;
const t = Math.min(
1,
Math.max(0, radial * RADIAL_SCALE + grainValue * grain - RADIAL_BIAS)
);
const stopIndex = Math.min(lastStop, Math.floor(t * colors.length));
return {
fill: colors[stopIndex] ?? colors[0],
x,
y,
};
})
).flat();
}
export { DEFAULT_COLORS, PixelBackground };
export type { PixelBackgroundProps };import { useMemo } from "react";
import { cn } from "@/lib/utils";
const DEFAULT_COLUMNS = 56;
const DEFAULT_ROWS = 18;
const DEFAULT_GRAIN = 0.2;
const DEFAULT_ORIGIN_X = 0.52;
const DEFAULT_ORIGIN_Y = 0.42;
const RADIAL_STRETCH_X = 1.35;
const RADIAL_STRETCH_Y = 1.15;
const RADIAL_SCALE = 0.92;
const RADIAL_BIAS = 0.06;
const DEFAULT_COLORS = [
"#F4F4F5",
"#D4D4D8",
"#A1A1AA",
"#71717A",
"#3F3F46",
"#18181B",
] as const;
type PixelBackgroundProps = React.ComponentProps<"div"> & {
colors?: readonly string[];
columns?: number;
grain?: number;
originX?: number;
originY?: number;
rows?: number;
};
function PixelBackground({
className,
colors = DEFAULT_COLORS,
columns = DEFAULT_COLUMNS,
grain = DEFAULT_GRAIN,
originX = DEFAULT_ORIGIN_X,
originY = DEFAULT_ORIGIN_Y,
rows = DEFAULT_ROWS,
...props
}: PixelBackgroundProps) {
const safeColumns = Math.max(1, Math.round(columns));
const safeRows = Math.max(1, Math.round(rows));
const palette = colors.length > 0 ? colors : DEFAULT_COLORS;
const colorKey = palette.join(",");
const cells = useMemo(
() =>
buildPixelCells({
colors: colorKey.split(","),
columns: safeColumns,
grain,
originX,
originY,
rows: safeRows,
}),
[colorKey, grain, originX, originY, safeColumns, safeRows]
);
return (
<div
aria-hidden="true"
className={cn(
"pointer-events-none absolute inset-0 overflow-hidden",
className
)}
style={{
backgroundColor: palette[Math.floor(palette.length / 2)],
}}
{...props}
>
<svg
className="size-full"
preserveAspectRatio="none"
viewBox={`0 0 ${safeColumns} ${safeRows}`}
>
<title>Pixel background</title>
{cells.map((cell) => (
<rect
fill={cell.fill}
height={1}
key={`${cell.x}-${cell.y}`}
width={1}
x={cell.x}
y={cell.y}
/>
))}
</svg>
</div>
);
}
function buildPixelCells({
colors,
columns,
grain,
originX,
originY,
rows,
}: {
colors: readonly string[];
columns: number;
grain: number;
originX: number;
originY: number;
rows: number;
}) {
const lastCol = Math.max(columns - 1, 1);
const lastRow = Math.max(rows - 1, 1);
const lastStop = colors.length - 1;
return Array.from({ length: rows }, (_, y) =>
Array.from({ length: columns }, (_, x) => {
const nx = x / lastCol - originX;
const ny = y / lastRow - originY;
const radial = Math.hypot(nx * RADIAL_STRETCH_X, ny * RADIAL_STRETCH_Y);
const grainValue = ((x * 17 + y * 31) % 7) / 7;
const t = Math.min(
1,
Math.max(0, radial * RADIAL_SCALE + grainValue * grain - RADIAL_BIAS)
);
const stopIndex = Math.min(lastStop, Math.floor(t * colors.length));
return {
fill: colors[stopIndex] ?? colors[0],
x,
y,
};
})
).flat();
}
export { DEFAULT_COLORS, PixelBackground };
export type { PixelBackgroundProps };Place it inside a relatively positioned parent with a defined height. The field fills the parent with absolute inset-0.
import { PixelBackground } from "@/components/pixel-background";
<div className="relative h-80 overflow-hidden rounded-2xl">
<PixelBackground />
</div>Pass a custom ramp from light (center) to dark (edges):
<PixelBackground
colors={["#FFE8D6", "#F4A261", "#E76F51", "#9B2226"]}
columns={48}
grain={0.35}
rows={16}
/>| Prop | Type | Default | Description |
|---|---|---|---|
columns? | number | 56 | Horizontal pixel count |
rows? | number | 18 | Vertical pixel count |
colors? | readonly string[] | sky blues | Color ramp from center (light) to edges (dark) |
grain? | number | 0.2 | Dither amount mixed into the radial falloff |
originX? | number | 0.52 | Radial light center on the X axis (0–1) |
originY? | number | 0.42 | Radial light center on the Y axis (0–1) |
className? | string | — | Additional classes for the background |