← Catalog
Edit Props (live)
TEXT
TEXT
IMAGE
config
Source
215 lines// Port of hyperframes `spotify-card` (Apache-2.0). Spotify "Now Playing"
// card — album art, track name, artist, progress bar, Spotify logo.
// 5s timeline with card slide-in, progress bar fill, slide-out.
import React from 'react';
import {
AbsoluteFill,
interpolate,
useCurrentFrame,
useVideoConfig,
} from 'remotion';
interface Props {
trackName: string;
artistName: string;
albumArtUrl: string;
durationSec: number;
}
export default function SpotifyCard({
trackName,
artistName,
albumArtUrl,
durationSec,
}: Props) {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const s = (seconds: number) => seconds * fps;
const slideInY = interpolate(frame, [s(0.1), s(0.7)], [400, 0], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
const fadeIn = interpolate(frame, [s(0.1), s(0.7)], [0, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
const slideOutY = interpolate(frame, [s(4.3), s(4.6)], [0, 400], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
const fadeOut = interpolate(frame, [s(4.3), s(4.6)], [1, 0], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
// Progress bar fills over the playing portion (0.7s → 4.3s = 3.6s)
const progress = interpolate(frame, [s(0.7), s(4.3)], [0, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
const elapsed = progress * durationSec;
return (
<AbsoluteFill
style={{
background:
'radial-gradient(ellipse at center, #1db95420 0%, #0a1f15 100%)',
fontFamily: 'system-ui, sans-serif',
}}
>
<div
style={{
position: 'absolute',
top: '50%',
left: '50%',
transform: `translate(-50%, calc(-50% + ${slideInY + slideOutY}px))`,
opacity: Math.min(fadeIn, fadeOut),
width: 880,
padding: 40,
borderRadius: 32,
background: 'linear-gradient(135deg, #1db954 0%, #0f6635 100%)',
boxShadow: '0 20px 60px rgba(0,0,0,0.5)',
}}
>
{/* Header with Spotify branding */}
<div
style={{
display: 'flex',
alignItems: 'center',
gap: 10,
marginBottom: 32,
}}
>
<svg width='32' height='32' viewBox='0 0 16 16' fill='#fff'>
<path d='M8 0a8 8 0 1 0 0 16A8 8 0 0 0 8 0zm3.669 11.538a.498.498 0 0 1-.686.165c-1.879-1.147-4.243-1.407-7.028-.77a.499.499 0 0 1-.222-.973c3.048-.696 5.662-.397 7.77.892a.5.5 0 0 1 .166.686zm.979-2.178a.624.624 0 0 1-.858.205c-2.15-1.321-5.428-1.704-7.972-.932a.625.625 0 0 1-.362-1.194c2.905-.881 6.517-.454 8.986 1.063a.624.624 0 0 1 .206.858zm.084-2.268C10.154 5.56 5.9 5.419 3.438 6.166a.748.748 0 1 1-.434-1.432c2.825-.857 7.523-.692 10.492 1.07a.747.747 0 1 1-.764 1.288z' />
</svg>
<span
style={{
fontSize: 22,
fontWeight: 700,
color: '#fff',
letterSpacing: 0.5,
}}
>
Now Playing
</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 32 }}>
<AlbumArt url={albumArtUrl} />
<div style={{ flex: 1, minWidth: 0 }}>
<div
style={{
fontSize: 44,
fontWeight: 700,
color: '#fff',
lineHeight: 1.2,
marginBottom: 8,
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
}}
>
{trackName}
</div>
<div
style={{
fontSize: 28,
color: 'rgba(255,255,255,0.75)',
lineHeight: 1.3,
}}
>
{artistName}
</div>
</div>
</div>
{/* Progress bar */}
<div style={{ marginTop: 32 }}>
<div
style={{
position: 'relative',
height: 6,
background: 'rgba(255,255,255,0.2)',
borderRadius: 3,
overflow: 'hidden',
}}
>
<div
style={{
position: 'absolute',
top: 0,
left: 0,
height: '100%',
width: `${progress * 100}%`,
background: '#fff',
borderRadius: 3,
}}
/>
</div>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
marginTop: 10,
fontSize: 18,
color: 'rgba(255,255,255,0.7)',
fontVariantNumeric: 'tabular-nums',
}}
>
<span>{formatTime(elapsed)}</span>
<span>{formatTime(durationSec)}</span>
</div>
</div>
</div>
</AbsoluteFill>
);
}
function AlbumArt({ url }: { url: string }) {
if (url) {
return (
// eslint-disable-next-line @next/next/no-img-element
<img
src={url}
alt=''
style={{
width: 200,
height: 200,
borderRadius: 12,
objectFit: 'cover',
flexShrink: 0,
boxShadow: '0 8px 24px rgba(0,0,0,0.4)',
}}
/>
);
}
return (
<div
style={{
width: 200,
height: 200,
borderRadius: 12,
flexShrink: 0,
background: 'linear-gradient(135deg, #0a1f15 0%, #1db954 100%)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxShadow: '0 8px 24px rgba(0,0,0,0.4)',
}}
>
<svg width='80' height='80' viewBox='0 0 24 24' fill='#fff'>
<circle cx='12' cy='12' r='4' opacity={0.85} />
</svg>
</div>
);
}
function formatTime(seconds: number): string {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${String(secs).padStart(2, '0')}`;
}
Schema (Zod)
import { z } from 'zod';
export const Schema = z.object({
trackName: z.string().describe('$port:text Track title'),
artistName: z.string().describe('$port:text Artist name'),
albumArtUrl: z.string().describe('$port:image Album art image URL').default(''),
durationSec: z.number().describe('Track duration (seconds)').default(215),
});
Spotify Now Playing
spotify_cardSpotify-branded now-playing card with album art, track/artist, animated progress bar, and matching green gradient. Ported from hyperframes (Apache-2.0).
Workflow Inputs (3)
- Track title
trackNameTEXTRequired - Artist name
artistNameTEXTRequired - Album art image URL
albumArtUrlIMAGE
Config Fields (1)
durationSec
Meta
- Updated
- 4/21/2026