Catalog

Edit Props (live)

IMAGE
TEXT
TEXT
config
config

Source

198 lines
// Inspired by hyperframes `app-showcase` (Apache-2.0). The original shows
// three phone mockups in a triptych; we simplify to a single phone on a
// colored gradient stage, with configurable screenshot + headline + CTA.
// Plays well both as a standalone hero and overlaid on a reel.

import React from 'react';
import {
  AbsoluteFill,
  interpolate,
  spring,
  useCurrentFrame,
  useVideoConfig,
} from 'remotion';

interface Props {
  screenshotUrl: string;
  headline: string;
  ctaText: string;
  bgColor: string;
  accentColor: string;
}

export default function AppShowcase({
  screenshotUrl,
  headline,
  ctaText,
  bgColor,
  accentColor,
}: Props) {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();
  const s = (seconds: number) => seconds * fps;

  const phoneEnter = spring({
    frame,
    fps,
    config: { damping: 12, stiffness: 90 },
  });
  const phoneY = (1 - phoneEnter) * 120;

  const headlineIn = interpolate(frame, [s(0.5), s(1.1)], [0, 1], {
    extrapolateLeft: 'clamp',
    extrapolateRight: 'clamp',
  });
  const headlineY = (1 - headlineIn) * 40;

  const ctaPulse =
    0.96 + 0.04 * Math.sin(((frame - s(1.5)) / fps) * Math.PI * 2);
  const ctaOpacity = interpolate(frame, [s(1.3), s(1.7)], [0, 1], {
    extrapolateLeft: 'clamp',
    extrapolateRight: 'clamp',
  });

  return (
    <AbsoluteFill
      style={{
        background: `radial-gradient(ellipse at top, ${accentColor}33 0%, ${bgColor} 55%)`,
        fontFamily: 'system-ui, sans-serif',
      }}
    >
      {/* Decorative halo behind the phone */}
      <div
        style={{
          position: 'absolute',
          top: '25%',
          left: '50%',
          width: 900,
          height: 900,
          transform: 'translate(-50%, -50%)',
          borderRadius: '50%',
          background: `radial-gradient(circle, ${accentColor}55 0%, transparent 60%)`,
          filter: 'blur(40px)',
          opacity: headlineIn,
        }}
      />

      {/* Phone frame */}
      <div
        style={{
          position: 'absolute',
          top: 260,
          left: '50%',
          transform: `translate(-50%, ${phoneY}px)`,
          opacity: phoneEnter,
          width: 540,
          height: 1080,
          borderRadius: 64,
          background: '#0a0a0a',
          padding: 16,
          boxShadow:
            '0 40px 120px rgba(0,0,0,0.6), inset 0 0 0 2px rgba(255,255,255,0.08)',
        }}
      >
        <div
          style={{
            position: 'relative',
            width: '100%',
            height: '100%',
            borderRadius: 48,
            overflow: 'hidden',
            background: '#111',
          }}
        >
          {/* Notch */}
          <div
            style={{
              position: 'absolute',
              top: 16,
              left: '50%',
              transform: 'translateX(-50%)',
              width: 140,
              height: 28,
              background: '#000',
              borderRadius: 14,
              zIndex: 2,
            }}
          />
          {screenshotUrl ? (
            // eslint-disable-next-line @next/next/no-img-element
            <img
              src={screenshotUrl}
              alt=''
              style={{
                width: '100%',
                height: '100%',
                objectFit: 'cover',
              }}
            />
          ) : (
            <div
              style={{
                width: '100%',
                height: '100%',
                background: `linear-gradient(180deg, ${accentColor} 0%, ${bgColor} 100%)`,
                display: 'flex',
                alignItems: 'center',
                justifyContent: 'center',
                fontSize: 48,
                color: '#fff',
                opacity: 0.45,
              }}
            >
              Your App
            </div>
          )}
        </div>
      </div>

      {/* Headline + CTA stacked above phone */}
      <div
        style={{
          position: 'absolute',
          top: 120,
          left: 0,
          right: 0,
          textAlign: 'center',
          opacity: headlineIn,
          transform: `translateY(${headlineY}px)`,
        }}
      >
        <div
          style={{
            fontSize: 72,
            fontWeight: 800,
            color: '#fff',
            letterSpacing: '-0.02em',
            lineHeight: 1.1,
            padding: '0 80px',
          }}
        >
          {headline}
        </div>
      </div>

      {/* CTA pinned near the bottom */}
      <div
        style={{
          position: 'absolute',
          bottom: 120,
          left: '50%',
          transform: `translate(-50%, 0) scale(${ctaPulse})`,
          opacity: ctaOpacity,
          padding: '22px 56px',
          borderRadius: 999,
          background: accentColor,
          fontSize: 32,
          fontWeight: 700,
          color: '#fff',
          letterSpacing: '0.04em',
          boxShadow: `0 12px 40px ${accentColor}99`,
        }}
      >
        {ctaText}
      </div>
    </AbsoluteFill>
  );
}

Schema (Zod)

import { z } from 'zod';

export const Schema = z.object({
  screenshotUrl: z.string().describe('$port:image App screenshot').default(''),
  headline: z.string().describe('$port:text Hero headline'),
  ctaText: z.string().describe('$port:text CTA button label').default('START NOW'),
  bgColor: z.string().default('#0a0a1a'),
  accentColor: z.string().default('#7c5cff'),
});

App Showcase

app_showcase

Single-phone hero with configurable screenshot, headline, and CTA. Phone spring-enters; CTA pulses. Simplified from hyperframes multi-phone triptych (Apache-2.0).

Workflow Inputs (3)

  • App screenshot
    screenshotUrl
    IMAGE
  • Hero headline
    headline
    TEXTRequired
  • CTA button label
    ctaText
    TEXT

Config Fields (2)

  • bgColor
  • accentColor

Meta

Updated
4/21/2026