reelcn
GitHubBack to landing

Subscribe CTA

A subscribe button that presses itself, with a bell that rings once after the click.

SubscribeCta, defaults
SubscribeCta, defaults thumbnail, 16:9

Installation

npx shadcn add https://www.reelcn.dev/r/subscribe-cta.json

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

Usage

<Sequence from={120} durationInFrames={90}>
  <SubscribeCta label="Subscribe" note="New videos every Thursday" />
</Sequence>

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.
labelstring Default "Subscribe".
notestringLine under the button.
pressAtnumberFrame, relative to this component, when the button presses itself.
colorstring
backgroundstring
styleCSSProperties
classNamestring

Built from

Use

  • The last five seconds of a video, or right after a payoff
  • Any moment that asks for one specific action

Avoid

  • Several competing asks at once, show one call to action

Guides

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

Source

registry/items/subscribe-cta.tsx
/**
 * @title Subscribe CTA
 * @category social
 * @description A subscribe button that presses itself, with a bell that rings once after the click.
 * @duration 45
 * @use The last five seconds of a video, or right after a payoff
 * @use Any moment that asks for one specific action
 * @avoid Several competing asks at once, show one call to action
 * @tags cta, subscribe, bell, youtube
 * @example
 * <Sequence from={120} durationInFrames={90}>
 *   <SubscribeCta label="Subscribe" note="New videos every Thursday" />
 * </Sequence>
 */
import type React from "react";
import { AbsoluteFill } from "remotion";
import { alpha, type MotionProps, tween, useMotion, useTheme, useViewport } from "./core";

export type SubscribeCtaProps = MotionProps & {
  label?: string;
  /** Line under the button. */
  note?: string;
  /** Frame, relative to this component, when the button presses itself. */
  pressAt?: number;
  color?: string;
  background?: string;
  style?: React.CSSProperties;
  className?: string;
};

export function SubscribeCta({
  label = "Subscribe",
  note,
  pressAt,
  color,
  background,
  style,
  className,
  ...motion
}: SubscribeCtaProps) {
  const theme = useTheme();
  const { u, safe } = useViewport();
  const m = useMotion(motion);
  const press = m.delay + (pressAt ?? Math.round(m.fps * 0.9));
  const down = tween(m.frame, m.fps, { from: press, duration: Math.round(m.fps * 0.12), motion: "snappy" });
  const up = tween(m.frame, m.fps, {
    from: press + Math.round(m.fps * 0.12),
    duration: Math.round(m.fps * 0.2),
    motion: "bouncy",
  });
  const ring = tween(m.frame, m.fps, {
    from: press + Math.round(m.fps * 0.2),
    duration: Math.round(m.fps * 0.5),
    motion: "linear",
  });
  // One ring: the bell rocks twice and settles.
  const tilt = ring > 0 && ring < 1 ? Math.sin(ring * Math.PI * 4) * (1 - ring) * 16 : 0;
  const enter = Math.min(m.enter, 1);

  return (
    <AbsoluteFill
      className={className}
      style={{ alignItems: "center", justifyContent: "flex-end", padding: `0 ${safe.x}px ${safe.bottom}px`, ...style }}
    >
      <div
        style={{
          display: "flex",
          flexDirection: "column",
          alignItems: "center",
          gap: u(12),
          opacity: enter * (1 - m.exit),
          translate: `0 ${(1 - enter) * u(20)}px`,
        }}
      >
        <div
          style={{
            display: "flex",
            alignItems: "center",
            gap: u(14),
            padding: `${u(16)}px ${u(30)}px`,
            borderRadius: u(theme.radius),
            background: background ?? theme.colors.accent,
            color: color ?? theme.colors.accentForeground,
            fontFamily: theme.fonts.heading,
            fontWeight: theme.headingWeight,
            fontSize: u(36),
            boxShadow: `0 ${u(14)}px ${u(40)}px ${alpha("#000000", 0.28)}`,
            scale: String(1 - down * 0.06 + up * 0.06),
          }}
        >
          <svg
            width={u(34)}
            height={u(34)}
            viewBox="0 0 24 24"
            fill="none"
            aria-hidden="true"
            style={{ rotate: `${tilt}deg` }}
          >
            <path
              d="M6 9a6 6 0 1 1 12 0c0 4 1.2 5.4 2 6H4c.8-.6 2-2 2-6Z"
              stroke="currentColor"
              strokeWidth={2}
              strokeLinejoin="round"
            />
            <path d="M10 19a2 2 0 0 0 4 0" stroke="currentColor" strokeWidth={2} strokeLinecap="round" />
          </svg>
          <span>{label}</span>
        </div>
        {note ? <div style={{ fontSize: u(24), color: theme.colors.muted }}>{note}</div> : null}
      </div>
    </AbsoluteFill>
  );
}

On this page