Showcase Frame

A presentation frame with eyebrow, title, caption, and a styled backdrop — wraps any other composition for hero-style component showcases.

Preview

Open editor

Usage

Use Showcase to feature any other scene as the centerpiece of a hero shot — landing page reels, "what we built this quarter" decks, marketing pages. Pick the child component, a backdrop style (gradient, radial glow, dotted grid, or solid), and tune the frame size with innerScale.

The eyebrow / title / caption all stagger in independently before the framed child reveals, so the storytelling reads top-down before the eye lands on the demo inside.

Props

NameTypeDefault
eyebrowstring"Featured component"
titlestring"Built for shipping reels."
captionstring"Wrap any scene inside a configurable showcase frame."
childCompositionIdstring (composition id)"BarChart"
frameStyle"video" | "browser" | "minimal" | "floating""video"
backdrop"dotted" | "grid" | "gradient" | "image" | "solid""dotted"
backdropImagestring (url)""
backdropColorAstring (hex)"#f6f8fa"
backdropColorBstring (hex)"#e9ecef"
borderColorstring (hex)"#e5e7eb"
cornerRadiusnumber20
innerScalenumber0.74
shadowIntensitynumber0.6

Composition

ID
Showcase
Resolution
1920×1080
FPS
60
Duration
4.0s

Source

Copy or download the React source — drop it into your own Remotion project. The only runtime dependency is remotion.

"use client";
import {
  AbsoluteFill,
  Easing,
  Img,
  interpolate,
  useVideoConfig,
} from "remotion";
import { type ClipStyle, resolveClipStyle } from "../../clip-style";
import { componentsByIdBase } from "../../componentsBase";
import { proxyExternalImg } from "../../proxy-image";
import { compositionsById } from "../../registry";
import { snap } from "../../snap";
import { useDesignFrame } from "../../use-design-frame";

export type ShowcaseFrameStyle = "video" | "browser" | "minimal" | "floating";

export type ShowcaseBackdrop =
  | "solid"
  | "dotted"
  | "grid"
  | "gradient"
  | "image";

export type ShowcaseProps = {
  eyebrow: string;
  title: string;
  caption: string;
  childCompositionId: string;
  frameStyle: ShowcaseFrameStyle;
  backdrop: ShowcaseBackdrop;
  backdropImage: string;
  backdropColorA: string;
  backdropColorB: string;
  innerScale: number;
  cornerRadius: number;
  shadowIntensity: number;
  borderColor: string;
  clipStyle?: ClipStyle;
};

const APPLE_EASE = Easing.bezier(0.16, 1, 0.3, 1);

export const Showcase: React.FC<ShowcaseProps> = ({
  eyebrow,
  title,
  caption,
  childCompositionId,
  frameStyle,
  backdrop,
  backdropImage,
  backdropColorA,
  backdropColorB,
  innerScale,
  cornerRadius,
  shadowIntensity,
  borderColor,
  clipStyle,
}) => {
  const frame = useDesignFrame();
  const { durationInFrames } = useVideoConfig();
  const s = resolveClipStyle(clipStyle, {
    background: "#ffffff",
    color: "#0f1014",
    fontFamily:
      "-apple-system, BlinkMacSystemFont, 'SF Pro Display', Inter, sans-serif",
    accent: "#00bbff",
  });

  const titleProgress = interpolate(frame, [4, 28], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
    easing: APPLE_EASE,
  });
  const captionProgress = interpolate(frame, [12, 40], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
    easing: APPLE_EASE,
  });
  const frameProgress = interpolate(frame, [16, 52], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
    easing: APPLE_EASE,
  });

  const Child = componentsByIdBase[childCompositionId];
  const childInfo = compositionsById[childCompositionId];
  const childProps = (childInfo?.defaultProps ?? {}) as Record<string, unknown>;
  const childRatio = childInfo
    ? `${childInfo.width} / ${childInfo.height}`
    : "16 / 9";

  const bgIsLight = isHexLight(s.background);
  const muted = bgIsLight ? "rgba(15,16,20,0.55)" : "rgba(255,255,255,0.65)";

  const playbackProgress = Math.min(
    1,
    frame / Math.max(1, durationInFrames - 1),
  );

  const showCaption = caption.trim().length > 0;
  const showHeader = title.trim().length > 0 || eyebrow.trim().length > 0;

  return (
    <AbsoluteFill
      style={{
        background: s.background,
        color: s.color,
        fontFamily: s.fontFamily,
        padding: 0,
        overflow: "hidden",
      }}
    >
      <Backdrop
        kind={backdrop}
        image={backdropImage}
        colorA={backdropColorA}
        colorB={backdropColorB}
        accent={s.accent}
      />

      <AbsoluteFill
        style={{
          padding: 72,
          display: "flex",
          flexDirection: "column",
          alignItems: "center",
        }}
      >
        {showHeader && (
          <div
            style={{
              textAlign: "center",
              maxWidth: 1200,
              marginBottom: 36,
              opacity: titleProgress,
              transform: `translate3d(0, ${snap((1 - titleProgress) * 14)}px, 0)`,
            }}
          >
            {eyebrow.trim() && (
              <div
                style={{
                  fontSize: 16,
                  fontWeight: 600,
                  color: s.accent,
                  letterSpacing: "0.16em",
                  textTransform: "uppercase",
                  marginBottom: 10,
                }}
              >
                {eyebrow}
              </div>
            )}
            {title.trim() && (
              <div
                style={{
                  fontSize: 72,
                  fontWeight: 700,
                  letterSpacing: "-0.03em",
                  lineHeight: 1.05,
                  color: s.color,
                }}
              >
                {title}
              </div>
            )}
            {showCaption && (
              <div
                style={{
                  marginTop: 14,
                  fontSize: 22,
                  fontWeight: 400,
                  color: muted,
                  opacity: captionProgress,
                }}
              >
                {caption}
              </div>
            )}
          </div>
        )}

        <div
          style={{
            position: "relative",
            flex: 1,
            width: "100%",
            display: "flex",
            alignItems: "center",
            justifyContent: "center",
            opacity: frameProgress,
            transform: `translate3d(0, ${snap((1 - frameProgress) * 24)}px, 0) scale(${0.96 + frameProgress * 0.04})`,
          }}
        >
          <FrameWrapper
            style={frameStyle}
            cornerRadius={cornerRadius}
            shadowIntensity={shadowIntensity}
            borderColor={borderColor}
            innerScale={innerScale}
            childRatio={childRatio}
            playbackProgress={playbackProgress}
            bgIsLight={bgIsLight}
            accent={s.accent}
          >
            {Child ? (
              <Child {...childProps} />
            ) : (
              <div
                style={{
                  width: "100%",
                  height: "100%",
                  display: "flex",
                  alignItems: "center",
                  justifyContent: "center",
                  color: muted,
                  fontSize: 18,
                }}
              >
                Pick a child composition in the Inspector
              </div>
            )}
          </FrameWrapper>
        </div>
      </AbsoluteFill>
    </AbsoluteFill>
  );
};

function FrameWrapper({
  style,
  cornerRadius,
  shadowIntensity,
  borderColor,
  innerScale,
  childRatio,
  playbackProgress,
  bgIsLight,
  accent,
  children,
}: {
  style: ShowcaseFrameStyle;
  cornerRadius: number;
  shadowIntensity: number;
  borderColor: string;
  innerScale: number;
  childRatio: string;
  playbackProgress: number;
  bgIsLight: boolean;
  accent: string;
  children: React.ReactNode;
}) {
  const shadow =
    shadowIntensity <= 0
      ? "none"
      : `0 ${20 + shadowIntensity * 60}px ${40 + shadowIntensity * 120}px rgba(0,0,0,${(bgIsLight ? 0.12 : 0.55) * Math.min(1, shadowIntensity)})`;

  const radius =
    style === "video" || style === "browser" ? cornerRadius : cornerRadius;

  // Browser style: top chrome bar with traffic lights; video style: bottom
  // progress bar overlay; minimal: nothing; floating: no chrome, big shadow,
  // pinned in a pill.
  const showChrome = style === "browser";
  const showPlayer = style === "video";

  return (
    <div
      style={{
        width: `${innerScale * 100}%`,
        aspectRatio: childRatio,
        borderRadius: radius,
        overflow: "hidden",
        boxShadow: shadow,
        border:
          style === "minimal" || style === "floating"
            ? "none"
            : `1px solid ${borderColor}`,
        background: "#000",
        position: "relative",
      }}
    >
      {showChrome && (
        <div
          style={{
            height: 36,
            background: bgIsLight ? "#f1f3f5" : "#1c1c22",
            display: "flex",
            alignItems: "center",
            padding: "0 14px",
            borderBottom: `1px solid ${borderColor}`,
            gap: 8,
          }}
        >
          <span
            style={{
              width: 12,
              height: 12,
              borderRadius: "50%",
              background: "#ff5f57",
            }}
          />
          <span
            style={{
              width: 12,
              height: 12,
              borderRadius: "50%",
              background: "#febc2e",
            }}
          />
          <span
            style={{
              width: 12,
              height: 12,
              borderRadius: "50%",
              background: "#28c840",
            }}
          />
        </div>
      )}
      <div
        style={{
          position: "relative",
          width: "100%",
          height: showChrome ? "calc(100% - 36px)" : "100%",
        }}
      >
        {children}
        {showPlayer && (
          <div
            style={{
              position: "absolute",
              left: 0,
              right: 0,
              bottom: 0,
              display: "flex",
              alignItems: "center",
              gap: 12,
              padding: "12px 18px",
              background:
                "linear-gradient(180deg, transparent 0%, rgba(0,0,0,0.55) 100%)",
              color: "#ffffff",
              fontSize: 13,
              fontVariantNumeric: "tabular-nums",
            }}
          >
            <svg
              width="14"
              height="14"
              viewBox="0 0 24 24"
              fill="currentColor"
              aria-hidden
            >
              <path d="M8 5v14l11-7z" />
            </svg>
            <div
              style={{
                flex: 1,
                height: 4,
                background: "rgba(255,255,255,0.25)",
                borderRadius: 2,
                overflow: "hidden",
              }}
            >
              <div
                style={{
                  width: `${playbackProgress * 100}%`,
                  height: "100%",
                  background: accent,
                  borderRadius: 2,
                }}
              />
            </div>
            <span style={{ minWidth: 42, textAlign: "right" }}>
              {(playbackProgress * 100).toFixed(0)}%
            </span>
          </div>
        )}
      </div>
    </div>
  );
}

function Backdrop({
  kind,
  image,
  colorA,
  colorB,
  accent,
}: {
  kind: ShowcaseBackdrop;
  image: string;
  colorA: string;
  colorB: string;
  accent: string;
}) {
  if (kind === "solid") return null;
  if (kind === "image" && image) {
    return (
      <AbsoluteFill>
        <Img
          src={proxyExternalImg(image)}
          crossOrigin="anonymous"
          style={{
            width: "100%",
            height: "100%",
            objectFit: "cover",
            filter: "saturate(1.05)",
          }}
        />
        <AbsoluteFill style={{ background: "rgba(0,0,0,0.25)" }} aria-hidden />
      </AbsoluteFill>
    );
  }
  if (kind === "gradient") {
    return (
      <AbsoluteFill
        style={{
          background: `linear-gradient(135deg, ${colorA} 0%, ${colorB} 100%)`,
        }}
      />
    );
  }
  if (kind === "dotted") {
    return (
      <AbsoluteFill
        style={{
          backgroundImage: `radial-gradient(${accent}33 1px, transparent 1.5px)`,
          backgroundSize: "28px 28px",
          maskImage:
            "radial-gradient(ellipse at center, black 35%, transparent 75%)",
          WebkitMaskImage:
            "radial-gradient(ellipse at center, black 35%, transparent 75%)",
        }}
      />
    );
  }
  // grid
  return (
    <AbsoluteFill
      style={{
        backgroundImage:
          "linear-gradient(rgba(15,16,20,0.06) 1px, transparent 1px), linear-gradient(90deg, rgba(15,16,20,0.06) 1px, transparent 1px)",
        backgroundSize: "48px 48px",
        maskImage:
          "radial-gradient(ellipse at center, black 35%, transparent 75%)",
        WebkitMaskImage:
          "radial-gradient(ellipse at center, black 35%, transparent 75%)",
      }}
    />
  );
}

function isHexLight(color: string): boolean {
  const c = color.trim().toLowerCase();
  if (c === "white" || c === "#fff" || c === "#ffffff") return true;
  if (c.startsWith("#") && c.length === 7) {
    const r = parseInt(c.slice(1, 3), 16);
    const g = parseInt(c.slice(3, 5), 16);
    const b = parseInt(c.slice(5, 7), 16);
    return (0.299 * r + 0.587 * g + 0.114 * b) / 255 > 0.6;
  }
  return false;
}
Save as Showcase/Showcase.tsx