Brand reel
A brand's logo, palette, type specimen, image collage and lockup, entirely driven by one brand kit and the theme system.
BrandReel, defaults
Installation
npx shadcn add https://www.reelcn.dev/r/brand-reel.jsonFiles land in src/reelcn/ with the items it builds on. Dependencies: remotion, zod.
Register the composition
<Composition
id="BrandReel"
component={BrandReel}
schema={brandReelSchema}
defaultProps={brandReelDefaults}
calculateMetadata={brandReelMetadata}
width={1920} height={1080} fps={30} durationInFrames={1}
/>Render
npx remotion render BrandReel # 16:9
npx remotion render BrandReel --width=1080 --height=1920 # 9:16Props
PropTypeDescription
brand*{ accent?: string; logo?: string; font?: string; }
theme"daylight" | "midnight" | "mono" | "ledger" | "afterglow" | "chromewave" | "neon" | "paper" | "sunset"Theme preset. Inherits the surrounding `ThemeProvider` when omitted.
sfxbooleanPlays a soft whoosh as each scene starts.
musicstringBackground music file, looped under everything at 25% volume.
typeSamplestringA short phrase set in the brand's own heading font, at display size.
Built from
Use
- A short agency or brand-kit showcase
- Introducing a visual identity before a product launch
Avoid
- A product feature tour. Use
product-launch
Make it with an agent
With the reelcn agent skill installed, ask for the video in plain words, such as "make a launch video for Relay with reelcn". The skill picks a template, sets its props, reviews a contact sheet and renders.
Guides
Components take their colors from a theme, their timing from motion props and their layout from the canvas.
Source
registry/items/brand-reel.tsx
/**
* @title Brand Reel
* @category templates
* @description A brand's logo, palette, type specimen, image collage and lockup, entirely driven by one brand kit and the theme system.
* @duration data-driven
* @use A short agency or brand-kit showcase
* @use Introducing a visual identity before a product launch
* @avoid A product feature tour. Use `product-launch`
* @tags brand, identity, palette, collage, template
* @example
* <Composition
* id="BrandReel"
* component={BrandReel}
* schema={brandReelSchema}
* defaultProps={brandReelDefaults}
* calculateMetadata={brandReelMetadata}
* width={1920} height={1080} fps={30} durationInFrames={1}
* />
*/
import type { CalculateMetadataFunction } from "remotion";
import { z } from "zod";
import { useTheme, useViewport } from "./core";
import { LayoutMorph } from "./layout-morph";
import type { CustomScene, Scene, Story } from "./story";
import { brandSchema, storyFrames } from "./story";
import { defineScene, Storyboard, templateSchema, templateStory } from "./storyboard";
export const brandReelSchema = templateSchema.extend({
brand: brandSchema,
/** A short phrase set in the brand's own heading font, at display size. */
typeSample: z.string().optional(),
/** Collage image URLs, shown in a `layout-morph` grid-to-mosaic morph. */
collage: z.array(z.string()),
});
export type BrandReelProps = z.infer<typeof brandReelSchema>;
export const brandReelDefaults: BrandReelProps = {
brand: { accent: "#2F5BFF", font: "Schibsted Grotesk" },
typeSample: "Hexhaus",
collage: [],
};
const brandPaletteScene = defineScene({
type: "brand-palette",
schema: z.object({}),
component: function BrandPaletteScene() {
const theme = useTheme();
const { u } = useViewport();
const swatches = [
theme.colors.accent,
theme.colors.success,
theme.colors.warning,
theme.colors.danger,
theme.colors.foreground,
];
return (
<div style={{ display: "flex", height: "100%", alignItems: "center", justifyContent: "center", gap: u(24) }}>
{swatches.map((color, i) => (
<div key={i} style={{ width: u(140), height: u(140), borderRadius: u(theme.radius), background: color }} />
))}
</div>
);
},
duration: () => 2.5,
});
const brandCollageScene = defineScene({
type: "brand-collage",
schema: z.object({ collage: z.array(z.string()) }),
component: ({ collage }: { collage: string[] }) => (
<LayoutMorph
items={collage.map((src) => ({ src }))}
layouts={[
{ at: 0, layout: "grid" },
{ at: 1.4, layout: "mosaic" },
]}
/>
),
duration: () => 3,
});
export function brandReelStory(props: BrandReelProps): Story {
const { brand, typeSample, collage } = props;
const scenes: (Scene | CustomScene)[] = [
{ type: "logo", text: "", src: brand.logo },
{ type: "brand-palette" } as CustomScene,
{ type: "title", title: typeSample ?? "", background: "gradient-mesh" },
collage.length > 0 ? ({ type: "brand-collage", collage } as CustomScene) : { type: "title", title: "" },
{ type: "cta", title: typeSample ?? "" },
];
// `templateStory`'s own param type is `Scene[]` (narrower than `Story["scenes"]`'s real
// `(Scene | CustomScene)[]`); every element above is already checked against its real shape
// (`CustomScene`, not `any`), so this one array-level cast is the only place type safety is relaxed.
return templateStory(props, scenes as Scene[]);
}
const brandReelScenes = [brandPaletteScene, brandCollageScene];
export function BrandReel(props: BrandReelProps) {
return <Storyboard story={brandReelStory(props)} scenes={brandReelScenes} />;
}
// Not `templateMetadata(brandReelStory)`: that helper calls `storyFrames(story)` with no custom scene
// rules, and this story carries the two template-local `defineScene` types above ("brand-palette",
// "brand-collage"). Without their rules, `sceneSeconds` falls through to `Scene`'s own switch, matches
// no case, and returns `undefined`, making `durationInFrames` NaN (same failure mode documented in
// product-launch.tsx).
export const brandReelMetadata: CalculateMetadataFunction<BrandReelProps> = ({ props }) => ({
durationInFrames: storyFrames(brandReelStory(props), brandReelScenes),
});