Back to components

Scan Reveal

A horizontal or vertical scan reveals processed results over the source, from product image tags to recognized invoice fields.

motion-primitiveclip-path

Product tagging

Horizontal
Asset library / product-01.png
Watch identified in a product photograph
Watch
Product detected

Invoice review

Vertical
Accounts payable / invoice-0084.pdf
INVOICE#0084
Merchant · matched
Northline Studio
Invoice date · matched
18 Oct 2026
Amount due · matched
$128.00 USD
import type { ComponentProps, CSSProperties, ReactNode } from "react";

import { cn } from "@/lib/utils";

import "./scan-reveal.css";

export type ScanRevealProps = ComponentProps<"div"> & {
  /** Decorative starting layer. Keep controls in children, not here. */
  before: ReactNode;
  /** Sweep duration in milliseconds. Remount with a new key to replay. */
  duration?: number;
  /** Direction of travel: left to right, or top to bottom. */
  direction?: "horizontal" | "vertical";
  beforeClassName?: string;
  beamClassName?: string;
};

/** A moving edge reveals children over a decorative starting layer. */
export function ScanReveal({
  before,
  children,
  duration = 1400,
  direction = "horizontal",
  beforeClassName,
  beamClassName,
  className,
  style,
  ...props
}: ScanRevealProps) {
  return (
    <div
      {...props}
      data-slot="scan-reveal"
      data-direction={direction}
      className={cn("scan-reveal", className)}
      style={
        {
          "--scan-reveal-duration": `${Number.isFinite(duration) ? Math.max(0, duration) : 1400}ms`,
          ...style,
        } as CSSProperties
      }
    >
      <div
        inert
        aria-hidden="true"
        className={cn("scan-reveal-before", beforeClassName)}
      >
        {before}
      </div>
      <div className="scan-reveal-content">{children}</div>
      <div
        aria-hidden="true"
        className={cn("scan-reveal-beam", beamClassName)}
      />
    </div>
  );
}

The pattern

A scan turns source material into a visible result, one region at a time. Use it to reveal object labels over a product photo, or recognized fields over their original invoice. The moving edge explains where the transformation has happened.

The preview is just one example. The component carries the motion; you bring the content.

Make it yours

Put the source in before and the processed result in children. Choose direction="horizontal" to inspect an image across its width, or direction="vertical" to read down a document. Keep both layers the same size so they line up; the source is clipped away as the result appears, so neither layer needs an opaque background. Start the reveal once results are ready: duration is the visual sweep time in milliseconds, not backend progress. Change the React key to replay. The source layer is decorative and inert; put controls only in children. Reduced motion shows the result immediately.

example.tsx
"use client";

import { useState } from "react";
import { ScanReveal } from "@/components/ui/scan-reveal";

export function Example() {
  const [run, setRun] = useState(0);
  return (
    <div className="flex flex-col items-start gap-4">
      <ScanReveal
        key={run}
        direction="vertical" // "horizontal" scans left to right
        duration={1400}
        before={<div className="bg-muted p-6">Receipt text</div>}
        className="overflow-hidden rounded-xl border"
      >
        <div className="bg-background p-6">Extracted fields</div>
      </ScanReveal>
      <button type="button" onClick={() => setRun((value) => value + 1)}>
        Scan again
      </button>
    </div>
  );
}

Installation