shadcn/ui — Introducing shadcn/ui (gift cut)
Install this video
Adds the full composition, its remocn dependencies, and the prompt to your Remotion project via the shadcn registry.
$ pnpm dlx shadcn@latest add kapishdima/remocn-demo/introducing-shadcnRender it locally
Renders the MP4 on your machine with the Remotion CLI.
$ pnpm dlx remotion render introducing-shadcn out/introducing-shadcn.mp4 --scale=2 --crf=15 --x264-preset=slower --jpeg-quality=95 --gl=angleThe prompt
Reconstructed draftA reconstruction of the prompt this video was generated from.
Make a gift video introducing shadcn/ui, done in shadcn's own monochrome look — zinc-950 canvas, ink text, Geist at regular weight only, no color anywhere except muted shader covers sampled from shadcn's own X avatar. Open with a camera glide across a dense wall of real shadcn/ui components rendered live (pull actual cards from the ui.shadcn.com homepage — create account, calendar, command menu, and so on), then land the name, the "not a component library, it's how you build your own" creed, and a quick montage proving the breadth (blocks, charts, themes, colors). Show the install command turning into a component rolodex, then demo the preset builder — flipping style, base color, theme, and font live while a sign-in card reshapes — collapsing into a single preset code you can pass to init. Close with a big proof number (a million apps a month), a few logos of who's building on it, and a quiet outro lockup with "Open Source. Open Code." and no URL. Use remocn for the shader covers and typography beats.
The code
The exact source the AI wrote — the same files the install command puts in your project.
import React from "react";
import {
AbsoluteFill,
Easing,
Sequence,
Series,
interpolate,
useCurrentFrame,
useVideoConfig,
} from "remotion";
import {
TransitionSeries,
linearTiming,
type TransitionPresentation,
type TransitionPresentationComponentProps,
} from "@remotion/transitions";
import { loadFont as loadSans } from "@remotion/google-fonts/Geist";
import { loadFont as loadMono } from "@remotion/google-fonts/GeistMono";
import { ScaleDownFade } from "@/components/remocn/scale-down-fade";
import { ShortSlideRight } from "@/components/remocn/short-slide-right";
import { KineticCenterBuild } from "@/components/remocn/kinetic-center-build";
import { LineByLineSlide } from "@/components/remocn/line-by-line-slide";
import { whipPan } from "@/components/remocn/whip-pan";
import { ShaderSimplexNoise } from "@/components/remocn/shader-simplex-noise";
import { ShaderSwirl } from "@/components/remocn/shader-swirl";
import { ShaderDithering } from "@/components/remocn/shader-dithering";
import { ShaderSmokeRing } from "@/components/remocn/shader-smoke-ring";
import { Button } from "@/demos/_ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/demos/_ui/card";
import { Input } from "@/demos/_ui/input";
import { Label } from "@/demos/_ui/label";
import { Switch } from "@/demos/_ui/switch";
import { FieldScene, S_FIELD } from "./field";
import { CategoriesScene, categorySceneDuration } from "./categories";
import { VideoScopeStyle } from "@/demos/_ui/video-scope";
// Geist regular only — the whole video never goes above weight 400.
const { fontFamily: SANS_FAMILY } = loadSans("normal", {
subsets: ["latin"],
weights: ["400"],
});
const { fontFamily: MONO_FAMILY } = loadMono("normal", {
subsets: ["latin"],
weights: ["400"],
});
const SANS =
"var(--font-geist-sans), -apple-system, BlinkMacSystemFont, sans-serif";
const MONO = `${MONO_FAMILY}, ui-monospace, SFMono-Regular, monospace`;
// shadcn's own monochrome: zinc-950 canvas, ink foreground. The only color in
// the video lives inside shader backgrounds — muted tones sampled from
// shadcn's X avatar (deep plum, dark navy, dusty rose, dusty blue).
const ZINC = "#09090b";
const INK = "#fafafa";
const MUTED = "rgba(250,250,250,0.62)";
const FAINT = "rgba(250,250,250,0.4)";
const PLUM = "#3d2547";
const ROSE = "#6b4054";
const clampOpts = {
extrapolateLeft: "clamp" as const,
extrapolateRight: "clamp" as const,
};
// ---------------------------------------------------------------------------
// Scene timings (frames @ 30fps). Transitions overlap.
// ---------------------------------------------------------------------------
const S_MEET = 130; // "This is shadcn/ui"
const S_TAGLINE = 76; // "Beautifully designed components"
const S_CREED_A = 76; // "Not a component library"
const S_CREED_B = 96; // kinetic "How you build your own"
const S_PILLARS = 104; // three pillar lines
const S_INSTALL_TITLE = 70; // "Any component, one command"
const S_INSTALL_CMD = 150; // typed command + 3D name rolodex
const S_PRESET_TITLE = 66; // "Your whole design system, one preset"
const S_CREATE = 200; // shadcn create: settings flip, the preview reacts
const S_PRESET = 176; // init --preset: the code flips, the UI re-themes live
const S_SWITCH = 64; // "Switch it any time"
const S_YOURS = 70; // "And the code is yours"
const S_APPS = 156; // 1,000,000 apps: dive → wheel spins up → pan → whip out
const WHO_BEAT = 24; // one builder-name hard cut
const S_WHO = WHO_BEAT * 3 + 56; // Startups / YC / Fortune 500s / everyone
const S_ECO = 72; // "A massive ecosystem — and room for you to build"
const S_BEST = 58; // "The best part?"
const S_NEEDS = 96; // kinetic "Everyone needs UI"
const S_OUTRO = 150; // smoke ring blooms → wordmark + motto
const T_SWIRL = 104; // shader-swirl cover (twist 1→0 → hold → 0→1)
const T_DITHER = 40; // dusty-rose dither dissolve (held mid-frame)
const T_X = 14; // crossfade
const T_WHIP = 18; // whip-pan carrying the apps pan into the next scene
const T_BLUR = 16; // blur crossfade
// Readability scrim over a backdrop shader.
const Scrim: React.FC<{ strength?: number }> = ({ strength = 1 }) => (
<AbsoluteFill
style={{
background: `radial-gradient(120% 120% at 50% 42%, rgba(9,9,11,${
0.3 * strength
}) 0%, rgba(9,9,11,${0.78 * strength}) 100%)`,
}}
/>
);
// ===========================================================================
// Scene 2 — This is shadcn/ui. Static on entry (the swirl cover's exit
// reveals it with a z-axis scale), then it plays its own exit so the tagline
// starts on an empty canvas.
// ===========================================================================
const MeetScene: React.FC = () => {
const frame = useCurrentFrame();
const { durationInFrames } = useVideoConfig();
const exitP = interpolate(
frame,
[durationInFrames - 18, durationInFrames - 2],
[0, 1],
{ ...clampOpts, easing: Easing.in(Easing.cubic) },
);
return (
<AbsoluteFill style={{ alignItems: "center", justifyContent: "center" }}>
<span
style={{
fontFamily: SANS,
fontWeight: 400,
fontSize: 74,
color: INK,
opacity: 1 - exitP,
transform: `translateY(${exitP * -10}px) scale(${1 - exitP * 0.05})`,
filter: `blur(${exitP * 6}px)`,
}}
>
This is shadcn/ui
</span>
</AbsoluteFill>
);
};
// ===========================================================================
// Scene 3 — Tagline. Glides in only after the name has fully exited.
// ===========================================================================
const TaglineScene: React.FC = () => (
<AbsoluteFill>
<ShortSlideRight
text="Beautifully designed components"
fontSize={48}
fontWeight={400}
color={INK}
/>
</AbsoluteFill>
);
// ===========================================================================
// Scene 4 — The creed, first half. The negation lands solo.
// ===========================================================================
const CreedAScene: React.FC = () => (
<AbsoluteFill>
<Sequence from={20} durationInFrames={56}>
<ScaleDownFade
text="Not a component library"
fontSize={54}
fontWeight={400}
color={INK}
/>
</Sequence>
</AbsoluteFill>
);
// ===========================================================================
// Scene 5 — The creed, second half. The answer assembles word by word.
// ===========================================================================
const CreedBScene: React.FC = () => (
<AbsoluteFill>
<Sequence from={6}>
<KineticCenterBuild
text="How you build your own"
fontSize={62}
fontWeight={400}
color={INK}
/>
</Sequence>
</AbsoluteFill>
);
// The category montage lives in ./categories — real shadcn/ui blocks,
// charts, theme flips, and the full Tailwind palette, hard-cut like a
// montage. The last beat absorbs the dither overlap.
const S_CATEGORIES = categorySceneDuration(T_DITHER);
// ===========================================================================
// Scene 7 — The pillars. Three claims accumulate as a block.
// ===========================================================================
const PillarsScene: React.FC = () => (
<AbsoluteFill>
<Sequence from={24}>
<LineByLineSlide
text={
"Accessible by default\nComposable by design\nOpen code, always"
}
fontSize={50}
fontWeight={400}
color={INK}
/>
</Sequence>
</AbsoluteFill>
);
// ===========================================================================
// Scene 8a — Install title. Its own typographic beat; plays its own exit so
// the command scene starts on an empty canvas.
// ===========================================================================
const InstallTitleScene: React.FC = () => {
const frame = useCurrentFrame();
const { durationInFrames } = useVideoConfig();
const exitP = interpolate(
frame,
[durationInFrames - 18, durationInFrames - 2],
[0, 1],
{ ...clampOpts, easing: Easing.in(Easing.cubic) },
);
return (
<AbsoluteFill
style={{
opacity: 1 - exitP,
transform: `translateY(${exitP * -10}px) scale(${1 - exitP * 0.05})`,
filter: `blur(${exitP * 6}px)`,
}}
>
<ShortSlideRight
text="Any component, one command"
fontSize={50}
fontWeight={400}
color={INK}
/>
</AbsoluteFill>
);
};
// ===========================================================================
// Scene 8b — The command. It types itself; once typing lands, the component
// name becomes a 3D rolodex flipping through the registry — any component,
// same command.
// ===========================================================================
const CMD = "npx shadcn add ";
const PKG_NAMES = ["button", "dialog", "command", "tabs", "chart-area"];
const SIZER = PKG_NAMES.reduce((a, b) => (b.length > a.length ? b : a));
const CMD_START = 10;
const FLIP_START = 52; // first flip, after the typed command has settled
const FLIP_PER = 20; // one name every 20 frames
const FLIP_DUR = 10; // the 3D flip itself
const InstallCmdScene: React.FC = () => {
const frame = useCurrentFrame();
const full = CMD + PKG_NAMES[0];
const typed = Math.max(
0,
Math.min(full.length, Math.floor((frame - CMD_START) * 1.5)),
);
const visible = full.slice(0, typed);
const cmdOpacity = interpolate(
frame,
[CMD_START - 4, CMD_START],
[0, 1],
clampOpts,
);
const typingDone = typed >= full.length;
const caretOn = typingDone ? Math.floor(frame / 15) % 2 === 0 : true;
// The caret leaves just before the rolodex starts spinning.
const caretOpacity = interpolate(
frame,
[FLIP_START - 12, FLIP_START - 4],
[1, 0],
clampOpts,
);
const flipping = frame >= FLIP_START - 4;
return (
<AbsoluteFill
style={{
alignItems: "center",
justifyContent: "center",
opacity: cmdOpacity,
}}
>
<span style={{ fontFamily: MONO, fontSize: 28, color: MUTED }}>
<span style={{ color: FAINT }}>$ </span>
<span>{visible.slice(0, Math.min(typed, CMD.length))}</span>
<span
style={{
display: "inline-block",
width: `${SIZER.length}ch`,
textAlign: "left",
position: "relative",
verticalAlign: "bottom",
perspective: 420,
color: INK,
}}
>
{/* Invisible sizer keeps the container's height and baseline. */}
<span style={{ visibility: "hidden" }}>{SIZER}</span>
{!flipping ? (
<span
style={{
position: "absolute",
left: 0,
top: 0,
whiteSpace: "nowrap",
}}
>
{typed > CMD.length ? visible.slice(CMD.length) : ""}
<span
style={{
display: "inline-block",
width: 14,
height: 28,
marginLeft: 3,
verticalAlign: "-4px",
background: caretOn ? MUTED : "transparent",
opacity: caretOpacity,
}}
/>
</span>
) : (
PKG_NAMES.map((name, i) => {
// The outgoing name clears first; the incoming one follows
// half a flip later, so the two never sit on top of each other.
const inStart = FLIP_START + (i - 1) * FLIP_PER + 5;
const outStart = FLIP_START + i * FLIP_PER;
let rotate = 0;
let y = 0;
let opacity = 1;
if (i > 0) {
const pIn = interpolate(
frame,
[inStart, inStart + FLIP_DUR],
[0, 1],
{ ...clampOpts, easing: Easing.out(Easing.cubic) },
);
rotate = (1 - pIn) * -85;
y = (1 - pIn) * 20;
opacity = pIn;
}
if (i < PKG_NAMES.length - 1) {
const pOut = interpolate(
frame,
[outStart, outStart + FLIP_DUR],
[0, 1],
{ ...clampOpts, easing: Easing.in(Easing.cubic) },
);
rotate += pOut * 85;
y -= pOut * 20;
opacity *= 1 - pOut;
}
if (opacity <= 0.001) return null;
return (
<span
key={name}
style={{
position: "absolute",
left: 0,
top: 0,
whiteSpace: "nowrap",
opacity,
transform: `translateY(${y}px) rotateX(${rotate}deg)`,
transformOrigin: "50% 50%",
backfaceVisibility: "hidden",
}}
>
{name}
</span>
);
})
)}
</span>
</span>
</AbsoluteFill>
);
};
// ===========================================================================
// Scene 8c — Preset title. Same typographic setup as the install title:Showing the first 400 of 1651 lines. View the full file on GitHub.