reelcn
GitHubBack to landing

Redact

Covers one or more rectangles with an opaque fill and a blur or pixelate effect on top. The opaque fill always renders, so the region is covered even if the effect layer doesn't.

Redact, defaults
Redact, defaults thumbnail, 16:9

Installation

npx shadcn add https://www.reelcn.dev/r/redact.json

Files land in src/reelcn/ with the items it builds on. Dependencies: remotion.

Usage

<BrowserWindow url="app.dev">
  <Dashboard />
  <Redact rects={[{ x: 62, y: 18, width: 22, height: 6 }]} mode="pixelate" />
</BrowserWindow>

Props

PropTypeDescription
delaynumberFrames to wait before entering.
durationnumberEnter duration in frames. Defaults to 0.6s. Some items reuse duration for an unrelated local timing. See that item's own prop docs.
exitnumber | booleanExit at the end of the parent `<Sequence>`. false keeps it on screen, a number sets the exit length in frames.
motionMotionPersonalityOverride the theme's motion personality.
posterbooleanRenders fully entered, with no exit: for a still frame or thumbnail. Defaults to false.
holdFramesnumberDelays the start of the exit window by this many frames, for a minimum on-screen hold. Defaults to 0.
rects*CalloutRect[]Areas to cover, in % of canvas, same shape callout's target uses. At most 8 (`redactRectBudget`).
mode"blur" | "pixelate" Default "blur".
cellSizenumberBlur radius, or pixelate cell size, in design units. Default 18.
styleCSSProperties
classNamestring

Built from

Use

  • Hiding an API key, email or account number in a real screenshot or screen recording
  • Covering a sensitive panel in a dashboard demo without cropping it out

Avoid

  • A region that should stay fully visible. This always covers, never partially

Guides

Components take their colors from a theme, their timing from motion props and their layout from the canvas.

Source

registry/items/redact.tsx
/**
 * @title Redact
 * @category product
 * @description Covers one or more rectangles with an opaque fill and a blur or pixelate effect on top. The opaque fill always renders, so the region is covered even if the effect layer doesn't.
 * @duration 1
 * @use Hiding an API key, email or account number in a real screenshot or screen recording
 * @use Covering a sensitive panel in a dashboard demo without cropping it out
 * @avoid A region that should stay fully visible. This always covers, never partially
 * @tags redact, blur, pixelate, privacy, censor
 * @example
 * <BrowserWindow url="app.dev">
 *   <Dashboard />
 *   <Redact rects={[{ x: 62, y: 18, width: 22, height: 6 }]} mode="pixelate" />
 * </BrowserWindow>
 */
import type React from "react";
import { useId } from "react";
import { AbsoluteFill } from "remotion";
import type { CalloutRect } from "./callout";
import { type MotionProps, redactRectBudget, useMotion, useTheme, useViewport } from "./core";

export type RedactProps = MotionProps & {
  /** Areas to cover, in % of canvas, same shape `callout`'s `target` uses. At most 8 (`redactRectBudget`). */
  rects: CalloutRect[];
  mode?: "pixelate" | "blur";
  /** Blur radius, or pixelate cell size, in design units. */
  cellSize?: number;
  style?: React.CSSProperties;
  className?: string;
};

export function Redact({ rects, mode = "blur", cellSize = 18, style, className, ...motion }: RedactProps) {
  redactRectBudget(rects.length);
  const theme = useTheme();
  const { u, width, height } = useViewport();
  const m = useMotion(motion);
  const filterId = `redact-pixelate-${useId().replace(/[^a-zA-Z0-9_-]/g, "")}`;
  const cellPx = u(cellSize);

  return (
    <AbsoluteFill className={className} style={{ pointerEvents: "none", opacity: m.presence, ...style }}>
      {mode === "pixelate" ? (
        <svg aria-hidden="true" width={0} height={0} style={{ position: "absolute" }}>
          <defs>
            <filter id={filterId} x="-20%" y="-20%" width="140%" height="140%">
              <feFlood x="0" y="0" width={cellPx} height={cellPx} />
              <feComposite width={cellPx * 2} height={cellPx * 2} />
              <feTile result="grid" />
              <feComposite in="SourceGraphic" in2="grid" operator="in" />
              <feMorphology operator="dilate" radius={cellPx / 2} />
            </filter>
          </defs>
        </svg>
      ) : null}
      {rects.map((rect, i) => (
        <div
          key={i}
          style={{
            position: "absolute",
            left: (rect.x / 100) * width,
            top: (rect.y / 100) * height,
            width: (rect.width / 100) * width,
            height: (rect.height / 100) * height,
            overflow: "hidden",
          }}
        >
          {/* Fail-opaque base: always rendered, always fully opaque. See Deviation 6 for the z-order spike
              this needs before the effect layer below can be trusted to show real content through it. */}
          <div style={{ position: "absolute", inset: 0, background: theme.colors.background }} />
          <div
            style={{
              position: "absolute",
              inset: 0,
              backdropFilter: mode === "blur" ? `blur(${cellPx}px)` : `url(#${filterId})`,
              WebkitBackdropFilter: mode === "blur" ? `blur(${cellPx}px)` : `url(#${filterId})`,
            }}
          />
        </div>
      ))}
    </AbsoluteFill>
  );
}

On this page