Web InnoventixFreeCode

Dither Effect

Original · free

A dithered duochrome effect resolves to the photo on hover.

byWeb InnoventixReact + Tailwind
ditherimageeffect
Open

Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.

npx shadcn@latest add https://webinnoventix.com/r/imgfx-dither.json
imgfx-dither.tsx
"use client";

import { useRef } from "react";
import { motion, useInView, useReducedMotion } from "motion/react";

export default function ImgfxDither() {
  const ref = useRef<HTMLDivElement>(null);
  const inView = useInView(ref, { once: true, margin: "-15% 0px" });
  const reduce = useReducedMotion();

  return (
    <section className="relative w-full overflow-hidden bg-neutral-50 px-6 py-20 dark:bg-neutral-950 sm:px-10 sm:py-28">
      <style>{`
        .imgfxdither-frame {
          --imgfxdither-shadow: #4338ca;
          --imgfxdither-light: #fb7185;
          --imgfxdither-dot: rgba(15, 23, 42, 0.55);
        }
        @media (prefers-color-scheme: dark) {
          .imgfxdither-frame {
            --imgfxdither-shadow: #6366f1;
            --imgfxdither-light: #fda4af;
            --imgfxdither-dot: rgba(2, 6, 23, 0.7);
          }
        }
        :where(.dark) .imgfxdither-frame {
          --imgfxdither-shadow: #6366f1;
          --imgfxdither-light: #fda4af;
          --imgfxdither-dot: rgba(2, 6, 23, 0.7);
        }

        .imgfxdither-img {
          display: block;
          width: 100%;
          height: 100%;
          object-fit: cover;
          filter: grayscale(1) contrast(1.22) brightness(1.06);
          transform: scale(1.015);
          transition: filter 720ms cubic-bezier(0.2, 0.72, 0.15, 1),
            transform 720ms cubic-bezier(0.2, 0.72, 0.15, 1);
          will-change: filter, transform;
        }
        .imgfxdither-duo {
          position: absolute;
          inset: 0;
          pointer-events: none;
          background: linear-gradient(
            135deg,
            var(--imgfxdither-shadow),
            var(--imgfxdither-light)
          );
          mix-blend-mode: color;
          opacity: 0.92;
          transition: opacity 640ms cubic-bezier(0.2, 0.72, 0.15, 1);
        }
        .imgfxdither-dots {
          position: absolute;
          inset: 0;
          pointer-events: none;
          background-image: radial-gradient(
              var(--imgfxdither-dot) 46%,
              transparent 48%
            ),
            radial-gradient(var(--imgfxdither-dot) 46%, transparent 48%);
          background-size: 3px 3px, 3px 3px;
          background-position: 0 0, 1.5px 1.5px;
          mix-blend-mode: soft-light;
          opacity: 0.85;
          transition: opacity 560ms cubic-bezier(0.2, 0.72, 0.15, 1);
          animation: imgfxdither-drift 5.5s linear infinite;
        }
        .imgfxdither-scan {
          position: absolute;
          inset: 0;
          pointer-events: none;
          background-image: repeating-linear-gradient(
            0deg,
            rgba(255, 255, 255, 0.08) 0,
            rgba(255, 255, 255, 0.08) 1px,
            transparent 1px,
            transparent 3px
          );
          mix-blend-mode: overlay;
          opacity: 0.6;
          transition: opacity 560ms cubic-bezier(0.2, 0.72, 0.15, 1);
        }

        .imgfxdither-frame:hover .imgfxdither-img,
        .imgfxdither-frame:focus-visible .imgfxdither-img {
          filter: none;
          transform: scale(1);
        }
        .imgfxdither-frame:hover .imgfxdither-duo,
        .imgfxdither-frame:focus-visible .imgfxdither-duo,
        .imgfxdither-frame:hover .imgfxdither-dots,
        .imgfxdither-frame:focus-visible .imgfxdither-dots,
        .imgfxdither-frame:hover .imgfxdither-scan,
        .imgfxdither-frame:focus-visible .imgfxdither-scan {
          opacity: 0;
        }

        @keyframes imgfxdither-drift {
          from {
            background-position: 0 0, 1.5px 1.5px;
          }
          to {
            background-position: 0 6px, 1.5px 7.5px;
          }
        }

        @media (prefers-reduced-motion: reduce) {
          .imgfxdither-img {
            transition-duration: 200ms;
            transform: none;
          }
          .imgfxdither-frame:hover .imgfxdither-img {
            transform: none;
          }
          .imgfxdither-dots {
            animation: none;
          }
        }
      `}</style>

      <motion.div
        ref={ref}
        initial={reduce ? false : { opacity: 0, y: 26 }}
        animate={inView ? { opacity: 1, y: 0 } : undefined}
        transition={{ duration: 0.7, ease: [0.2, 0.72, 0.15, 1] }}
        className="mx-auto flex max-w-3xl flex-col items-center"
      >
        <span className="mb-3 inline-flex items-center gap-2 rounded-full border border-indigo-300/60 bg-white/60 px-3 py-1 text-[0.7rem] font-semibold uppercase tracking-[0.22em] text-indigo-700 dark:border-indigo-400/30 dark:bg-white/5 dark:text-indigo-300">
          Image FX / Dither
        </span>
        <h2 className="text-center text-2xl font-semibold tracking-tight text-neutral-900 dark:text-neutral-50 sm:text-3xl">
          Duochrome dither, resolved on touch
        </h2>

        <div
          tabIndex={0}
          role="img"
          aria-label="City rooftops at dusk, rendered as an ordered-dither duochrome that resolves to full color on hover"
          className="imgfxdither-frame group relative mt-10 aspect-[4/5] w-full overflow-hidden rounded-2xl bg-neutral-200 shadow-2xl shadow-indigo-950/20 ring-1 ring-black/10 outline-none transition-shadow duration-500 focus-visible:ring-2 focus-visible:ring-indigo-500 dark:bg-neutral-800 dark:ring-white/10 sm:aspect-[3/4]"
        >
          {/* eslint-disable-next-line @next/next/no-img-element */}
          <img
            src="/img/gallery/g07.webp"
            alt="City rooftops fading into an amber and violet dusk sky"
            loading="lazy"
            draggable={false}
            className="imgfxdither-img"
          />
          <span className="imgfxdither-duo" aria-hidden="true" />
          <span className="imgfxdither-dots" aria-hidden="true" />
          <span className="imgfxdither-scan" aria-hidden="true" />

          <span
            aria-hidden="true"
            className="pointer-events-none absolute bottom-4 left-4 right-4 flex items-center justify-between rounded-lg bg-black/35 px-3 py-2 text-xs font-medium text-white/90 backdrop-blur-sm transition-opacity duration-500 group-hover:opacity-0 group-focus-visible:opacity-0"
          >
            <span className="tracking-wide">g07.webp</span>
            <span className="tracking-[0.18em] uppercase text-indigo-200">
              dithered
            </span>
          </span>
        </div>

        <p className="mt-5 max-w-md text-center text-sm text-neutral-500 dark:text-neutral-400">
          Hover, or focus with the keyboard, to dissolve the ordered-dither
          duochrome and reveal the photograph in full.
        </p>
      </motion.div>
    </section>
  );
}

Dependencies

motion

Licence

Built by Web Innoventix. Free for personal and commercial use, no attribution required.

Built by Web Innoventix

Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.

Get a free quote

Similar components

Browse all →