← Catalog
No preview yet
Source
206 lines// KPI Callout Block — large headline number that counts up from zero,
// with a label above and a delta/sub-text below. Useful for stat
// reveals in tech, news, or marketing reels: "$500M saved", "30,000
// laid off", "65% AI-written code".
//
// The count-up easing is springy without a real spring (cheap cubic
// out interpolation) so it feels punchy without an extra dependency.
// Optional fancy formatter handles common cases — currency, percent,
// abbreviated thousands/millions/billions.
import {
AbsoluteFill,
Easing,
interpolate,
spring,
useCurrentFrame,
useVideoConfig,
} from 'remotion';
import { loadFont as loadInter } from '@remotion/google-fonts/Inter';
import { loadFont as loadJetBrains } from '@remotion/google-fonts/JetBrainsMono';
import { loadFont } from '@remotion/google-fonts/SpaceGrotesk';
const { fontFamily: SPACE_GROTESK } = loadFont();
const { fontFamily: INTER } = loadInter();
const { fontFamily: JETBRAINS } = loadJetBrains();
const FONT_MAP: Record<string, string> = {
SpaceGrotesk: SPACE_GROTESK,
Inter: INTER,
JetBrainsMono: JETBRAINS,
};
export type KpiCalloutProps = {
label: string;
targetValue: number;
prefix: string;
suffix: string;
format:
| 'plain'
| 'thousands'
| 'millions'
| 'billions'
| 'currency'
| 'percent';
sublabel: string;
countUpDurationS: number;
numberFont: 'SpaceGrotesk' | 'Inter' | 'JetBrainsMono';
numberColor: string;
numberFontSize: number;
labelColor: string;
labelFontSize: number;
sublabelColor: string;
sublabelFontSize: number;
backgroundColor: string;
accentColor: string;
};
function formatNumber(value: number, fmt: KpiCalloutProps['format']): string {
switch (fmt) {
case 'currency':
return value.toLocaleString('en-US', { maximumFractionDigits: 0 });
case 'thousands':
return Math.round(value / 1_000).toLocaleString('en-US') + 'K';
case 'millions':
return (value / 1_000_000).toFixed(1).replace(/\.0$/, '') + 'M';
case 'billions':
return (value / 1_000_000_000).toFixed(1).replace(/\.0$/, '') + 'B';
case 'percent':
return Math.round(value).toString();
case 'plain':
default:
return Math.round(value).toLocaleString('en-US');
}
}
export default function KpiCallout({
label = 'JOBS LOST',
targetValue = 30000,
prefix = '',
suffix = '',
format = 'plain',
sublabel = 'in 2026 alone',
countUpDurationS = 1.4,
numberFont = 'SpaceGrotesk',
numberColor = '#FFFFFF',
numberFontSize = 200,
labelColor = '#FFD84D',
labelFontSize = 32,
sublabelColor = '#A6A8B0',
sublabelFontSize = 28,
backgroundColor = '#0a0b0f',
accentColor = '#00F0FF',
}: Partial<KpiCalloutProps>) {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const numberFamily = FONT_MAP[numberFont] ?? SPACE_GROTESK;
const countFrames = Math.max(1, Math.round(countUpDurationS * fps));
const progress = interpolate(frame, [0, countFrames], [0, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
easing: Easing.out(Easing.cubic),
});
const currentValue = targetValue * progress;
const numberText = `${prefix}${formatNumber(currentValue, format)}${suffix}`;
const numberPop = spring({
frame,
fps,
config: { damping: 12, stiffness: 180, mass: 0.5 },
});
const labelOpacity = interpolate(frame, [0, 8], [0, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
const sublabelOpacity = interpolate(
frame,
[countFrames, countFrames + 6],
[0, 1],
{
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
}
);
const sublabelLift = interpolate(
frame,
[countFrames, countFrames + 8],
[12, 0],
{
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
easing: Easing.out(Easing.cubic),
}
);
return (
<AbsoluteFill
style={{
backgroundColor,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'column',
padding: '0 64px',
}}
>
{/* Top accent bar that grows out from center as the number lands */}
<div
style={{
width: `${progress * 80}px`,
height: 4,
backgroundColor: accentColor,
borderRadius: 2,
marginBottom: 28,
boxShadow: `0 0 20px ${accentColor}80`,
}}
/>
<div
style={{
color: labelColor,
fontFamily: INTER,
fontWeight: 600,
fontSize: labelFontSize,
letterSpacing: '0.18em',
textTransform: 'uppercase',
opacity: labelOpacity,
marginBottom: 18,
}}
>
{label}
</div>
<div
style={{
color: numberColor,
fontFamily: numberFamily,
fontWeight: 800,
fontSize: numberFontSize,
lineHeight: 1.0,
letterSpacing: '-0.04em',
fontVariantNumeric: 'tabular-nums',
transform: `scale(${numberPop})`,
textShadow: '0 6px 24px rgba(0,0,0,0.4)',
}}
>
{numberText}
</div>
{sublabel ? (
<div
style={{
color: sublabelColor,
fontFamily: INTER,
fontWeight: 500,
fontSize: sublabelFontSize,
marginTop: 24,
opacity: sublabelOpacity,
transform: `translateY(${sublabelLift}px)`,
textAlign: 'center',
}}
>
{sublabel}
</div>
) : null}
</AbsoluteFill>
);
}
Schema (Zod)
import { z } from 'zod';
export const Schema = z.object({
label: z.string().default('JOBS LOST').describe('Label above the number'),
targetValue: z.number().default(30000).describe('Number to count up to'),
prefix: z.string().default('').describe('e.g. "$"'),
suffix: z.string().default('').describe('e.g. "%"'),
format: z
.enum(['plain', 'thousands', 'millions', 'billions', 'currency', 'percent'])
.default('plain')
.describe('$style:select Number format'),
sublabel: z
.string()
.default('in 2026 alone')
.describe('Sub-line below the number'),
countUpDurationS: z
.number()
.default(1.4)
.describe('$style:scale Count-up duration (seconds)'),
numberFont: z
.enum(['SpaceGrotesk', 'Inter', 'JetBrainsMono'])
.default('SpaceGrotesk')
.describe('$style:font Number font'),
numberColor: z
.string()
.default('#FFFFFF')
.describe('$style:color Number color'),
numberFontSize: z
.number()
.default(200)
.describe('$style:size Number font size'),
labelColor: z
.string()
.default('#FFD84D')
.describe('$style:color Label color'),
labelFontSize: z.number().default(32).describe('$style:size Label font size'),
sublabelColor: z
.string()
.default('#A6A8B0')
.describe('$style:color Sublabel color'),
sublabelFontSize: z
.number()
.default(28)
.describe('$style:size Sublabel font size'),
backgroundColor: z
.string()
.default('#0a0b0f')
.describe('$style:color Background color'),
accentColor: z
.string()
.default('#00F0FF')
.describe('$style:color Accent bar color'),
});
KPI Callout
kpi_calloutBig stat reveal — number counts up from zero with springy pop, label above, sublabel below. Useful for "$500M saved", "30K layoffs", "65% AI-written" moments.
Workflow Inputs (0)
This block has no workflow-connected inputs.
Config Fields (16)
labelformatprefixsuffixsublabellabelColornumberFontaccentColornumberColortargetValuelabelFontSizesublabelColornumberFontSizebackgroundColorcountUpDurationSsublabelFontSize
Meta
- Updated
- 7/10/2026