Pixel Image
Pixelated image reveal component for React and Next.js applications. Built with TypeScript support, Tailwind CSS styling, and shadcn/ui design featuring grid animations, grayscale transitions, and customizable timing.
import { PixelImage } from '@/components/ui/shadcn-io/pixel-image';const Example = () => ( <div className="flex justify-center"> <PixelImage src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800&h=600&fit=crop&crop=center" grid="6x4" /> </div>);export default Example;
"use client";import { cn } from "@/lib/utils";import { useEffect, useMemo, useState } from "react";type Grid = { rows: number; cols: number;};const DEFAULT_GRIDS: Record<string, Grid> = { "6x4": { rows: 4, cols: 6 }, "8x8": { rows: 8, cols: 8 }, "8x3": { rows: 3, cols: 8 }, "4x6": { rows: 6, cols: 4 }, "3x8": { rows: 8, cols: 3 },};type PredefinedGridKey = keyof typeof DEFAULT_GRIDS;interface PixelImageProps { src: string; grid?: PredefinedGridKey; customGrid?: Grid; grayscaleAnimation?: boolean; pixelFadeInDuration?: number; // in ms maxAnimationDelay?: number; // in ms colorRevealDelay?: number; // in ms showReplayButton?: boolean;}export const PixelImage = ({ src, grid = "6x4", grayscaleAnimation = true, pixelFadeInDuration = 1000, maxAnimationDelay = 1200, colorRevealDelay = 1300, customGrid, showReplayButton = false,}: PixelImageProps) => { const [isVisible, setIsVisible] = useState(false); const [showColor, setShowColor] = useState(false); const [key, setKey] = useState(0); const MIN_GRID = 1; const MAX_GRID = 16; const { rows, cols } = useMemo(() => { const isValidGrid = (grid?: Grid) => { if (!grid) return false; const { rows, cols } = grid; return ( Number.isInteger(rows) && Number.isInteger(cols) && rows >= MIN_GRID && cols >= MIN_GRID && rows <= MAX_GRID && cols <= MAX_GRID ); }; return isValidGrid(customGrid) ? customGrid! : DEFAULT_GRIDS[grid]; }, [customGrid, grid]); const resetAnimation = () => { setIsVisible(false); setShowColor(false); setKey(prev => prev + 1); }; useEffect(() => { // Small delay to ensure proper animation on initial mount/refresh const startTimeout = setTimeout(() => { setIsVisible(true); }, 50); const colorTimeout = setTimeout(() => { setShowColor(true); }, colorRevealDelay + 50); return () => { clearTimeout(startTimeout); clearTimeout(colorTimeout); }; }, [colorRevealDelay, key]); const pieces = useMemo(() => { const total = rows * cols; return Array.from({ length: total }, (_, index) => { const row = Math.floor(index / cols); const col = index % cols; const clipPath = `polygon( ${col * (100 / cols)}% ${row * (100 / rows)}%, ${(col + 1) * (100 / cols)}% ${row * (100 / rows)}%, ${(col + 1) * (100 / cols)}% ${(row + 1) * (100 / rows)}%, ${col * (100 / cols)}% ${(row + 1) * (100 / rows)}% )`; // Use deterministic "random" based on index to avoid hydration issues const delay = ((index * 23) % total) * (maxAnimationDelay / total); return { clipPath, delay, }; }); }, [rows, cols, maxAnimationDelay]); return ( <div className="relative"> <div className="relative h-72 w-72 select-none md:h-96 md:w-96" key={key}> {pieces.map((piece, index) => ( <div key={index} className={cn( "absolute inset-0 transition-all ease-out", isVisible ? "opacity-100" : "opacity-0", )} style={{ clipPath: piece.clipPath, transitionDelay: `${piece.delay}ms`, transitionDuration: `${pixelFadeInDuration}ms`, }} > <img src={src} alt={`Pixel image piece ${index + 1}`} className={cn( "size-full object-cover rounded-xl", grayscaleAnimation && (showColor ? "grayscale-0" : "grayscale"), )} style={{ transition: grayscaleAnimation ? `filter ${pixelFadeInDuration}ms cubic-bezier(0.4, 0, 0.2, 1)` : "none", }} draggable={false} /> </div> ))} </div> {showReplayButton && ( <button onClick={resetAnimation} className="absolute top-2 right-2 z-10 rounded-lg bg-black/50 px-3 py-1 text-xs text-white backdrop-blur-sm transition-opacity hover:bg-black/70" > Replay </button> )} </div> );};
Installation
npx shadcn@latest add https://www.shadcn.io/registry/pixel-image.json
npx shadcn@latest add https://www.shadcn.io/registry/pixel-image.json
pnpm dlx shadcn@latest add https://www.shadcn.io/registry/pixel-image.json
bunx shadcn@latest add https://www.shadcn.io/registry/pixel-image.json
Features
- Pixelated reveals - Grid-based image animations with customizable patterns using CSS clip-path
- Grayscale transitions - Smooth color reveals for dramatic visual impact using JavaScript timing
- Custom grids - 5 predefined layouts plus custom dimensions (1x1 to 16x16) using TypeScript props
- Staggered timing - Configurable delays and durations for pixel appearance using React state
- Replay functionality - Optional button for restarting animations in Next.js applications
- Performance optimized - CSS transforms with GPU acceleration using Tailwind CSS utilities
- Accessibility features - Alt text and reduced motion support using shadcn/ui patterns
- Open source - Free component with deterministic randomization and responsive design
Examples
Basic Pixel Effect
import { PixelImage } from '@/components/ui/shadcn-io/pixel-image';const Example = () => ( <div className="flex justify-center"> <PixelImage src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800&h=600&fit=crop&crop=center" grid="6x4" /> </div>);export default Example;
"use client";import { cn } from "@/lib/utils";import { useEffect, useMemo, useState } from "react";type Grid = { rows: number; cols: number;};const DEFAULT_GRIDS: Record<string, Grid> = { "6x4": { rows: 4, cols: 6 }, "8x8": { rows: 8, cols: 8 }, "8x3": { rows: 3, cols: 8 }, "4x6": { rows: 6, cols: 4 }, "3x8": { rows: 8, cols: 3 },};type PredefinedGridKey = keyof typeof DEFAULT_GRIDS;interface PixelImageProps { src: string; grid?: PredefinedGridKey; customGrid?: Grid; grayscaleAnimation?: boolean; pixelFadeInDuration?: number; // in ms maxAnimationDelay?: number; // in ms colorRevealDelay?: number; // in ms showReplayButton?: boolean;}export const PixelImage = ({ src, grid = "6x4", grayscaleAnimation = true, pixelFadeInDuration = 1000, maxAnimationDelay = 1200, colorRevealDelay = 1300, customGrid, showReplayButton = false,}: PixelImageProps) => { const [isVisible, setIsVisible] = useState(false); const [showColor, setShowColor] = useState(false); const [key, setKey] = useState(0); const MIN_GRID = 1; const MAX_GRID = 16; const { rows, cols } = useMemo(() => { const isValidGrid = (grid?: Grid) => { if (!grid) return false; const { rows, cols } = grid; return ( Number.isInteger(rows) && Number.isInteger(cols) && rows >= MIN_GRID && cols >= MIN_GRID && rows <= MAX_GRID && cols <= MAX_GRID ); }; return isValidGrid(customGrid) ? customGrid! : DEFAULT_GRIDS[grid]; }, [customGrid, grid]); const resetAnimation = () => { setIsVisible(false); setShowColor(false); setKey(prev => prev + 1); }; useEffect(() => { // Small delay to ensure proper animation on initial mount/refresh const startTimeout = setTimeout(() => { setIsVisible(true); }, 50); const colorTimeout = setTimeout(() => { setShowColor(true); }, colorRevealDelay + 50); return () => { clearTimeout(startTimeout); clearTimeout(colorTimeout); }; }, [colorRevealDelay, key]); const pieces = useMemo(() => { const total = rows * cols; return Array.from({ length: total }, (_, index) => { const row = Math.floor(index / cols); const col = index % cols; const clipPath = `polygon( ${col * (100 / cols)}% ${row * (100 / rows)}%, ${(col + 1) * (100 / cols)}% ${row * (100 / rows)}%, ${(col + 1) * (100 / cols)}% ${(row + 1) * (100 / rows)}%, ${col * (100 / cols)}% ${(row + 1) * (100 / rows)}% )`; // Use deterministic "random" based on index to avoid hydration issues const delay = ((index * 23) % total) * (maxAnimationDelay / total); return { clipPath, delay, }; }); }, [rows, cols, maxAnimationDelay]); return ( <div className="relative"> <div className="relative h-72 w-72 select-none md:h-96 md:w-96" key={key}> {pieces.map((piece, index) => ( <div key={index} className={cn( "absolute inset-0 transition-all ease-out", isVisible ? "opacity-100" : "opacity-0", )} style={{ clipPath: piece.clipPath, transitionDelay: `${piece.delay}ms`, transitionDuration: `${pixelFadeInDuration}ms`, }} > <img src={src} alt={`Pixel image piece ${index + 1}`} className={cn( "size-full object-cover rounded-xl", grayscaleAnimation && (showColor ? "grayscale-0" : "grayscale"), )} style={{ transition: grayscaleAnimation ? `filter ${pixelFadeInDuration}ms cubic-bezier(0.4, 0, 0.2, 1)` : "none", }} draggable={false} /> </div> ))} </div> {showReplayButton && ( <button onClick={resetAnimation} className="absolute top-2 right-2 z-10 rounded-lg bg-black/50 px-3 py-1 text-xs text-white backdrop-blur-sm transition-opacity hover:bg-black/70" > Replay </button> )} </div> );};
Custom Grid Pattern
import { PixelImage } from '@/components/ui/shadcn-io/pixel-image';const Example = () => ( <div className="flex justify-center"> <PixelImage src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800&h=600&fit=crop&crop=center" customGrid={{ rows: 12, cols: 8 }} grayscaleAnimation={true} /> </div>);export default Example;
"use client";import { cn } from "@/lib/utils";import { useEffect, useMemo, useState } from "react";type Grid = { rows: number; cols: number;};const DEFAULT_GRIDS: Record<string, Grid> = { "6x4": { rows: 4, cols: 6 }, "8x8": { rows: 8, cols: 8 }, "8x3": { rows: 3, cols: 8 }, "4x6": { rows: 6, cols: 4 }, "3x8": { rows: 8, cols: 3 },};type PredefinedGridKey = keyof typeof DEFAULT_GRIDS;interface PixelImageProps { src: string; grid?: PredefinedGridKey; customGrid?: Grid; grayscaleAnimation?: boolean; pixelFadeInDuration?: number; // in ms maxAnimationDelay?: number; // in ms colorRevealDelay?: number; // in ms showReplayButton?: boolean;}export const PixelImage = ({ src, grid = "6x4", grayscaleAnimation = true, pixelFadeInDuration = 1000, maxAnimationDelay = 1200, colorRevealDelay = 1300, customGrid, showReplayButton = false,}: PixelImageProps) => { const [isVisible, setIsVisible] = useState(false); const [showColor, setShowColor] = useState(false); const [key, setKey] = useState(0); const MIN_GRID = 1; const MAX_GRID = 16; const { rows, cols } = useMemo(() => { const isValidGrid = (grid?: Grid) => { if (!grid) return false; const { rows, cols } = grid; return ( Number.isInteger(rows) && Number.isInteger(cols) && rows >= MIN_GRID && cols >= MIN_GRID && rows <= MAX_GRID && cols <= MAX_GRID ); }; return isValidGrid(customGrid) ? customGrid! : DEFAULT_GRIDS[grid]; }, [customGrid, grid]); const resetAnimation = () => { setIsVisible(false); setShowColor(false); setKey(prev => prev + 1); }; useEffect(() => { // Small delay to ensure proper animation on initial mount/refresh const startTimeout = setTimeout(() => { setIsVisible(true); }, 50); const colorTimeout = setTimeout(() => { setShowColor(true); }, colorRevealDelay + 50); return () => { clearTimeout(startTimeout); clearTimeout(colorTimeout); }; }, [colorRevealDelay, key]); const pieces = useMemo(() => { const total = rows * cols; return Array.from({ length: total }, (_, index) => { const row = Math.floor(index / cols); const col = index % cols; const clipPath = `polygon( ${col * (100 / cols)}% ${row * (100 / rows)}%, ${(col + 1) * (100 / cols)}% ${row * (100 / rows)}%, ${(col + 1) * (100 / cols)}% ${(row + 1) * (100 / rows)}%, ${col * (100 / cols)}% ${(row + 1) * (100 / rows)}% )`; // Use deterministic "random" based on index to avoid hydration issues const delay = ((index * 23) % total) * (maxAnimationDelay / total); return { clipPath, delay, }; }); }, [rows, cols, maxAnimationDelay]); return ( <div className="relative"> <div className="relative h-72 w-72 select-none md:h-96 md:w-96" key={key}> {pieces.map((piece, index) => ( <div key={index} className={cn( "absolute inset-0 transition-all ease-out", isVisible ? "opacity-100" : "opacity-0", )} style={{ clipPath: piece.clipPath, transitionDelay: `${piece.delay}ms`, transitionDuration: `${pixelFadeInDuration}ms`, }} > <img src={src} alt={`Pixel image piece ${index + 1}`} className={cn( "size-full object-cover rounded-xl", grayscaleAnimation && (showColor ? "grayscale-0" : "grayscale"), )} style={{ transition: grayscaleAnimation ? `filter ${pixelFadeInDuration}ms cubic-bezier(0.4, 0, 0.2, 1)` : "none", }} draggable={false} /> </div> ))} </div> {showReplayButton && ( <button onClick={resetAnimation} className="absolute top-2 right-2 z-10 rounded-lg bg-black/50 px-3 py-1 text-xs text-white backdrop-blur-sm transition-opacity hover:bg-black/70" > Replay </button> )} </div> );};
No Animation (Static)
import { PixelImage } from '@/components/ui/shadcn-io/pixel-image';const Example = () => ( <div className="flex justify-center"> <PixelImage src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800&h=600&fit=crop&crop=center" grid="8x8" grayscaleAnimation={false} maxAnimationDelay={0} /> </div>);export default Example;
"use client";import { cn } from "@/lib/utils";import { useEffect, useMemo, useState } from "react";type Grid = { rows: number; cols: number;};const DEFAULT_GRIDS: Record<string, Grid> = { "6x4": { rows: 4, cols: 6 }, "8x8": { rows: 8, cols: 8 }, "8x3": { rows: 3, cols: 8 }, "4x6": { rows: 6, cols: 4 }, "3x8": { rows: 8, cols: 3 },};type PredefinedGridKey = keyof typeof DEFAULT_GRIDS;interface PixelImageProps { src: string; grid?: PredefinedGridKey; customGrid?: Grid; grayscaleAnimation?: boolean; pixelFadeInDuration?: number; // in ms maxAnimationDelay?: number; // in ms colorRevealDelay?: number; // in ms showReplayButton?: boolean;}export const PixelImage = ({ src, grid = "6x4", grayscaleAnimation = true, pixelFadeInDuration = 1000, maxAnimationDelay = 1200, colorRevealDelay = 1300, customGrid, showReplayButton = false,}: PixelImageProps) => { const [isVisible, setIsVisible] = useState(false); const [showColor, setShowColor] = useState(false); const [key, setKey] = useState(0); const MIN_GRID = 1; const MAX_GRID = 16; const { rows, cols } = useMemo(() => { const isValidGrid = (grid?: Grid) => { if (!grid) return false; const { rows, cols } = grid; return ( Number.isInteger(rows) && Number.isInteger(cols) && rows >= MIN_GRID && cols >= MIN_GRID && rows <= MAX_GRID && cols <= MAX_GRID ); }; return isValidGrid(customGrid) ? customGrid! : DEFAULT_GRIDS[grid]; }, [customGrid, grid]); const resetAnimation = () => { setIsVisible(false); setShowColor(false); setKey(prev => prev + 1); }; useEffect(() => { // Small delay to ensure proper animation on initial mount/refresh const startTimeout = setTimeout(() => { setIsVisible(true); }, 50); const colorTimeout = setTimeout(() => { setShowColor(true); }, colorRevealDelay + 50); return () => { clearTimeout(startTimeout); clearTimeout(colorTimeout); }; }, [colorRevealDelay, key]); const pieces = useMemo(() => { const total = rows * cols; return Array.from({ length: total }, (_, index) => { const row = Math.floor(index / cols); const col = index % cols; const clipPath = `polygon( ${col * (100 / cols)}% ${row * (100 / rows)}%, ${(col + 1) * (100 / cols)}% ${row * (100 / rows)}%, ${(col + 1) * (100 / cols)}% ${(row + 1) * (100 / rows)}%, ${col * (100 / cols)}% ${(row + 1) * (100 / rows)}% )`; // Use deterministic "random" based on index to avoid hydration issues const delay = ((index * 23) % total) * (maxAnimationDelay / total); return { clipPath, delay, }; }); }, [rows, cols, maxAnimationDelay]); return ( <div className="relative"> <div className="relative h-72 w-72 select-none md:h-96 md:w-96" key={key}> {pieces.map((piece, index) => ( <div key={index} className={cn( "absolute inset-0 transition-all ease-out", isVisible ? "opacity-100" : "opacity-0", )} style={{ clipPath: piece.clipPath, transitionDelay: `${piece.delay}ms`, transitionDuration: `${pixelFadeInDuration}ms`, }} > <img src={src} alt={`Pixel image piece ${index + 1}`} className={cn( "size-full object-cover rounded-xl", grayscaleAnimation && (showColor ? "grayscale-0" : "grayscale"), )} style={{ transition: grayscaleAnimation ? `filter ${pixelFadeInDuration}ms cubic-bezier(0.4, 0, 0.2, 1)` : "none", }} draggable={false} /> </div> ))} </div> {showReplayButton && ( <button onClick={resetAnimation} className="absolute top-2 right-2 z-10 rounded-lg bg-black/50 px-3 py-1 text-xs text-white backdrop-blur-sm transition-opacity hover:bg-black/70" > Replay </button> )} </div> );};
With Replay Button
import { PixelImage } from '@/components/ui/shadcn-io/pixel-image';const Example = () => ( <div className="flex justify-center"> <PixelImage src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800&h=600&fit=crop&crop=center" grid="6x4" showReplayButton={true} /> </div>);export default Example;
"use client";import { cn } from "@/lib/utils";import { useEffect, useMemo, useState } from "react";type Grid = { rows: number; cols: number;};const DEFAULT_GRIDS: Record<string, Grid> = { "6x4": { rows: 4, cols: 6 }, "8x8": { rows: 8, cols: 8 }, "8x3": { rows: 3, cols: 8 }, "4x6": { rows: 6, cols: 4 }, "3x8": { rows: 8, cols: 3 },};type PredefinedGridKey = keyof typeof DEFAULT_GRIDS;interface PixelImageProps { src: string; grid?: PredefinedGridKey; customGrid?: Grid; grayscaleAnimation?: boolean; pixelFadeInDuration?: number; // in ms maxAnimationDelay?: number; // in ms colorRevealDelay?: number; // in ms showReplayButton?: boolean;}export const PixelImage = ({ src, grid = "6x4", grayscaleAnimation = true, pixelFadeInDuration = 1000, maxAnimationDelay = 1200, colorRevealDelay = 1300, customGrid, showReplayButton = false,}: PixelImageProps) => { const [isVisible, setIsVisible] = useState(false); const [showColor, setShowColor] = useState(false); const [key, setKey] = useState(0); const MIN_GRID = 1; const MAX_GRID = 16; const { rows, cols } = useMemo(() => { const isValidGrid = (grid?: Grid) => { if (!grid) return false; const { rows, cols } = grid; return ( Number.isInteger(rows) && Number.isInteger(cols) && rows >= MIN_GRID && cols >= MIN_GRID && rows <= MAX_GRID && cols <= MAX_GRID ); }; return isValidGrid(customGrid) ? customGrid! : DEFAULT_GRIDS[grid]; }, [customGrid, grid]); const resetAnimation = () => { setIsVisible(false); setShowColor(false); setKey(prev => prev + 1); }; useEffect(() => { // Small delay to ensure proper animation on initial mount/refresh const startTimeout = setTimeout(() => { setIsVisible(true); }, 50); const colorTimeout = setTimeout(() => { setShowColor(true); }, colorRevealDelay + 50); return () => { clearTimeout(startTimeout); clearTimeout(colorTimeout); }; }, [colorRevealDelay, key]); const pieces = useMemo(() => { const total = rows * cols; return Array.from({ length: total }, (_, index) => { const row = Math.floor(index / cols); const col = index % cols; const clipPath = `polygon( ${col * (100 / cols)}% ${row * (100 / rows)}%, ${(col + 1) * (100 / cols)}% ${row * (100 / rows)}%, ${(col + 1) * (100 / cols)}% ${(row + 1) * (100 / rows)}%, ${col * (100 / cols)}% ${(row + 1) * (100 / rows)}% )`; // Use deterministic "random" based on index to avoid hydration issues const delay = ((index * 23) % total) * (maxAnimationDelay / total); return { clipPath, delay, }; }); }, [rows, cols, maxAnimationDelay]); return ( <div className="relative"> <div className="relative h-72 w-72 select-none md:h-96 md:w-96" key={key}> {pieces.map((piece, index) => ( <div key={index} className={cn( "absolute inset-0 transition-all ease-out", isVisible ? "opacity-100" : "opacity-0", )} style={{ clipPath: piece.clipPath, transitionDelay: `${piece.delay}ms`, transitionDuration: `${pixelFadeInDuration}ms`, }} > <img src={src} alt={`Pixel image piece ${index + 1}`} className={cn( "size-full object-cover rounded-xl", grayscaleAnimation && (showColor ? "grayscale-0" : "grayscale"), )} style={{ transition: grayscaleAnimation ? `filter ${pixelFadeInDuration}ms cubic-bezier(0.4, 0, 0.2, 1)` : "none", }} draggable={false} /> </div> ))} </div> {showReplayButton && ( <button onClick={resetAnimation} className="absolute top-2 right-2 z-10 rounded-lg bg-black/50 px-3 py-1 text-xs text-white backdrop-blur-sm transition-opacity hover:bg-black/70" > Replay </button> )} </div> );};
API Reference
PixelImage
Prop | Type | Default | Description |
---|---|---|---|
src | string | required | The image source URL to display with pixelated reveal effect |
grid | "6x4" | "8x8" | "8x3" | "4x6" | "3x8" | "6x4" | Predefined grid pattern for pixelation. Each option creates different visual styles |
customGrid | { rows: number; cols: number } | undefined | Custom grid dimensions that override predefined grids. Values must be 1-16 |
grayscaleAnimation | boolean | true | Enables smooth transition from grayscale to full color during reveal |
pixelFadeInDuration | number | 1000 | Duration in milliseconds for each pixel's fade-in animation |
maxAnimationDelay | number | 1200 | Maximum stagger delay in milliseconds between pixel appearances |
colorRevealDelay | number | 1300 | Delay in milliseconds before grayscale-to-color transition begins |
showReplayButton | boolean | false | Shows an overlay replay button to restart the animation sequence |
Grid Constraints
- Custom grid rows and columns must be integers between 1 and 16
- Invalid custom grids will fallback to the selected predefined grid
- Higher grid values create more detailed pixelation effects
- Lower grid values create larger, more prominent pixels
Predefined Grids
- 6x4: 6 columns × 4 rows (24 pixels) - Default balanced grid
- 8x8: 8 columns × 8 rows (64 pixels) - High detail square grid
- 8x3: 8 columns × 3 rows (24 pixels) - Wide format grid
- 4x6: 4 columns × 6 rows (24 pixels) - Tall format grid
- 3x8: 3 columns × 8 rows (24 pixels) - Ultra-wide grid
Use Cases
- Creative portfolios - Artistic image reveals for photography and design showcases
- Gaming interfaces - Retro 8-bit and pixel art aesthetic implementations
- Loading animations - Engaging image reveals during content loading states
- Interactive galleries - Unique pixelated transitions for art and media displays
Implementation
Built with CSS clip-path for efficient rendering. Uses staggered animations with deterministic randomization. Supports custom grids and replay functionality. Optimized for performance with GPU acceleration.
Motion Highlight
Versatile highlight component with smooth animations for interactive UI elements. Perfect for React applications requiring dynamic visual feedback with Next.js integration and TypeScript support.
Theme Toggle Button
Animated dark mode switch with View Transitions API. Beautiful theme toggles for React applications with multiple animation effects and Next.js integration.