Skip to main content
Glama
component-builders-guide.md21.7 kB
# Component Builder's Guide A comprehensive guide for creating new components in the chuk-motion system. ## Table of Contents 1. [Overview](#overview) 2. [Component Architecture](#component-architecture) 3. [File Structure](#file-structure) 4. [Step-by-Step Component Creation](#step-by-step-component-creation) 5. [Template Guidelines](#template-guidelines) 6. [Design Token Usage](#design-token-usage) 7. [Common Patterns](#common-patterns) 8. [Testing & Validation](#testing--validation) 9. [Troubleshooting](#troubleshooting) --- ## Overview Each component in this system consists of **5 required files** that work together to provide: - Python-based configuration and validation (Pydantic) - MCP tool registration for API exposure - Composition builder integration - TSX template for Remotion rendering ### Component Categories Components are organized into categories: - `charts/` - Data visualization components - `overlays/` - UI overlays (lower thirds, text, buttons) - `layouts/` - Layout components (grid, split screen, containers) - `code/` - Code display components - `animations/` - Animation components - `content/` - Content display components - `demo_realism/` - Realistic UI mockups for demos --- ## Component Architecture ``` src/chuk_motion/components/ └── [category]/ └── [ComponentName]/ ├── __init__.py # Component registration ├── schema.py # Pydantic schema ├── tool.py # MCP tool registration ├── builder.py # CompositionBuilder method └── template.tsx.j2 # React/Remotion TSX template ``` --- ## File Structure ### 1. `__init__.py` **Purpose**: Registers the component and exports its builder method ```python """[ComponentName] - [Brief description].""" from .builder import add_to_composition __all__ = ["add_to_composition"] ``` ### 2. `schema.py` **Purpose**: Defines Pydantic models for validation ```python """Pydantic schema for [ComponentName] component.""" from pydantic import BaseModel, Field from typing import Literal class [ComponentName]Props(BaseModel): """Props for [ComponentName] component.""" # Required props startFrame: int durationInFrames: int # Component-specific props title: str = Field(default="", description="Title text") variant: Literal["minimal", "standard", "bold"] = "standard" # Layout props position: Literal["center", "top-left", "top-right", ...] = "center" # Style props width: int = Field(default=800, ge=100, le=1920) height: int = Field(default=600, ge=100, le=1080) ``` **Key Points**: - Always include `startFrame` and `durationInFrames` - Use `Literal` for constrained string values - Use `Field()` for defaults and validation - Add helpful descriptions ### 3. `tool.py` **Purpose**: Registers the MCP tool for API exposure ```python """MCP tool registration for [ComponentName].""" import json from typing import TYPE_CHECKING if TYPE_CHECKING: from mcp.server import Server def register_tool(mcp, project_manager): """Register the [ComponentName] MCP tool.""" @mcp.tool async def remotion_add_[component_name]( startFrame: int, durationInFrames: int, title: str = "", variant: str = "standard", position: str = "center", width: int = 800, height: int = 600, # Use str for array/object parameters options: str = "{}", ) -> str: """Add a [ComponentName] component to the composition. Args: startFrame: Starting frame number durationInFrames: Duration in frames title: Title text variant: Style variant position: Position on screen width: Component width height: Component height options: JSON string for additional options Returns: Success message """ # Parse JSON parameters options_parsed = json.loads(options) if options else {} # Add to composition... return "✓ [ComponentName] added successfully" ``` **Critical Rules**: - **Never** use `@mcp.tool(name=..., description=..., schema=...)` - Use `@mcp.tool` decorator only - **Never** use `List[dict]` - use `str` and parse JSON - Always parse JSON string parameters - Use `TYPE_CHECKING` for type hints ### 4. `builder.py` **Purpose**: Provides fluent API method for CompositionBuilder ```python """Composition builder method for [ComponentName] component.""" from typing import TYPE_CHECKING if TYPE_CHECKING: from chuk_motion.generator.composition_builder import CompositionBuilder def add_to_composition( builder: "CompositionBuilder", start_time: float, duration: float, title: str = "", variant: str = "standard", position: str = "center", width: int = 800, height: int = 600, ) -> "CompositionBuilder": """Add a [ComponentName] component to the composition. Args: builder: The composition builder instance start_time: Start time in seconds duration: Duration in seconds title: Title text variant: Style variant position: Position on screen width: Component width height: Component height Returns: The builder instance for method chaining """ from chuk_motion.generator.composition_builder import ComponentInstance # Convert time to frames start_frame = builder.seconds_to_frames(start_time) duration_frames = builder.seconds_to_frames(duration) # Create component instance component = ComponentInstance( component_type="[ComponentName]", start_frame=start_frame, duration_frames=duration_frames, props={ "title": title, "variant": variant, "position": position, "width": width, "height": height, "start_time": start_time, "duration": duration, }, layer=5, # Adjust based on component type ) builder.components.append(component) return builder ``` **Key Points**: - Accept **seconds** not frames (`start_time`, `duration`) - Convert to frames using `builder.seconds_to_frames()` - Use `ComponentInstance` not Pydantic models - Append to `builder.components` - Return `builder` for chaining - Layer values: 0=background, 5=content, 10=overlays ### 5. `template.tsx.j2` **Purpose**: Jinja2 template that generates React/Remotion component ```tsx import React from 'react'; import { AbsoluteFill, useCurrentFrame, useVideoConfig, spring } from 'remotion'; interface [ComponentName]Props { startFrame: number; durationInFrames: number; title: string; variant: 'minimal' | 'standard' | 'bold'; position: string; width: number; height: number; } export const [ComponentName]: React.FC<[ComponentName]Props> = ({ startFrame, durationInFrames, title = '', variant = 'standard', position = 'center', width = 800, height = 600, }) => { const frame = useCurrentFrame(); const { fps } = useVideoConfig(); // Visibility check if (frame < startFrame || frame >= startFrame + durationInFrames) { return null; } const relativeFrame = frame - startFrame; // Position mapping const positionMap: Record<string, React.CSSProperties> = { center: { top: '50%', left: '50%', transform: 'translate(-50%, -50%)', }, 'top-left': { top: '[[ spacing.spacing.xl ]]', left: '[[ spacing.spacing.xl ]]' }, 'top-center': { top: '[[ spacing.spacing.xl ]]', left: '50%', transform: 'translateX(-50%)' }, // ... more positions }; // Entrance animation const entrance = spring({ frame: relativeFrame, fps, config: { damping: [[ motion.default_spring.config.damping ]], stiffness: [[ motion.default_spring.config.stiffness ]], mass: [[ motion.default_spring.config.mass ]], }, }); return ( <AbsoluteFill> <div style={{ position: 'absolute', ...positionMap[position], width, height, opacity: entrance, transform: `${positionMap[position]?.transform || ''} scale(${entrance})`, }} > {/* Component content */} <div style={{ background: '[[ colors.primary[0] ]]', color: '[[ colors.text ]]', padding: '[[ spacing.spacing.lg ]]', borderRadius: '[[ spacing.border_radius.lg ]]', fontFamily: "'[[ "', '".join(typography.body_font.fonts) ]]'", fontSize: 36, fontWeight: [[ typography.font_weights.bold ]], }}> {title} </div> </div> </AbsoluteFill> ); }; ``` --- ## Template Guidelines ### Jinja2 Delimiters **IMPORTANT**: Use custom delimiters to avoid JSX conflicts: - Variables: `[[ variable ]]` not `{{ variable }}` - Blocks: `[% if %] ... [% endif %]` not `{% if %}` ### Available Template Variables The following variables are available in templates: ```javascript // Component configuration (from props) config = {} // Empty - props come from VideoComposition // Theme data theme = { colors: {...}, typography: {...}, motion: {...} } colors = theme.colors typography = { ...theme.typography, font_sizes, font_weights, ... } motion = theme.motion spacing = SPACING_TOKENS font_sizes = TYPOGRAPHY_TOKENS['font_sizes']['video_1080p'] ``` ### Design Token Access #### ✅ CORRECT Usage ```tsx // Spacing - tokens already include "px" padding: '[[ spacing.spacing.lg ]]' // → "24px" borderRadius: '[[ spacing.border_radius.md ]]' // → "8px" // Colors background: '[[ colors.primary[0] ]]' // → "#007bff" color: '[[ colors.text ]]' // → "#ffffff" // Typography fontFamily: "'[[ "', '".join(typography.body_font.fonts) ]]'" fontWeight: [[ typography.font_weights.bold ]] // → 700 // Motion damping: [[ motion.default_spring.config.damping ]] // Font sizes - use hardcoded values fontSize: 36 // For 1080p ``` #### ❌ INCORRECT Usage ```tsx // DON'T add "px" to spacing tokens padding: '[[ spacing.xl ]]px' // Wrong! Becomes "32pxpx" // DON'T use template variables for fps fps: [[ fps ]] // Wrong! Not available in template // DON'T use smooth_spring damping: [[ motion.smooth_spring.config.damping ]] // Wrong! // DON'T use mono_font fontFamily: [[ typography.mono_font.fonts ]] // Wrong! Use code_font // DON'T use font_sizes template variable fontSize: [[ font_sizes.lg ]] // Wrong! Use hardcoded number ``` ### Getting FPS Always use `useVideoConfig()`: ```tsx const { fps } = useVideoConfig(); // Then use it const animation = spring({ frame: relativeFrame, fps, // ✅ Correct config: { ... } }); ``` ### Handling JSON String Props For props that receive JSON strings (arrays/objects): ```tsx interface ComponentProps { tabs?: TabConfig[] | string; // Accept both types // ... } export const Component: React.FC<ComponentProps> = ({ tabs, // ... }) => { // Parse if string const parsedTabs: TabConfig[] = typeof tabs === 'string' ? JSON.parse(tabs) : (tabs || []); // Use parsedTabs throughout component return ( <div> {parsedTabs.map((tab, i) => ...)} </div> ); }; ``` --- ## Design Token Usage ### Spacing Tokens ```python SPACING_TOKENS = { "spacing": { "xs": "8px", "sm": "12px", "md": "16px", "lg": "24px", "xl": "32px", "2xl": "48px", }, "border_radius": { "sm": "4px", "md": "8px", "lg": "12px", "xl": "16px", } } ``` **Access**: `spacing.spacing.lg` → "24px" ### Color Tokens ```python colors = { "primary": ["#007bff", "#0056b3", ...], # Gradient steps "accent": ["#ff4081", ...], "text": "#ffffff", "background": "#000000", } ``` **Access**: `colors.primary[0]` or `colors.text` ### Typography Tokens ```python typography = { "body_font": { "fonts": ["Inter", "SF Pro Text", ...] }, "code_font": { "fonts": ["JetBrains Mono", ...] }, "font_weights": { "regular": 400, "medium": 500, "semibold": 600, "bold": 700, } } ``` **Access**: - `typography.body_font.fonts` (join with ", ") - `typography.font_weights.bold` → 700 ### Motion Tokens ```python motion = { "default_spring": { "config": { "damping": 200, "stiffness": 200, "mass": 0.5, } } } ``` **Access**: `motion.default_spring.config.damping` --- ## Common Patterns ### Standard Component Structure ```tsx export const Component: React.FC<Props> = ({ startFrame, durationInFrames, ...props }) => { const frame = useCurrentFrame(); const { fps } = useVideoConfig(); // 1. Visibility check if (frame < startFrame || frame >= startFrame + durationInFrames) { return null; } const relativeFrame = frame - startFrame; // 2. Parse JSON props if needed const parsedData = typeof data === 'string' ? JSON.parse(data) : data; // 3. Position mapping const positionMap = { /* ... */ }; // 4. Entrance animation const entrance = spring({ frame: relativeFrame, fps, config: { damping: [[ motion.default_spring.config.damping ]], stiffness: [[ motion.default_spring.config.stiffness ]], mass: [[ motion.default_spring.config.mass ]], }, }); // 5. Render return ( <AbsoluteFill> {/* Component content */} </AbsoluteFill> ); }; ``` ### Position Mapping Pattern ```tsx const positionMap: Record<string, React.CSSProperties> = { center: { top: '50%', left: '50%', transform: 'translate(-50%, -50%)', }, 'top-left': { top: '[[ spacing.spacing.xl ]]', left: '[[ spacing.spacing.xl ]]' }, 'top-center': { top: '[[ spacing.spacing.xl ]]', left: '50%', transform: 'translateX(-50%)' }, 'top-right': { top: '[[ spacing.spacing.xl ]]', right: '[[ spacing.spacing.xl ]]' }, 'center-left': { top: '50%', left: '[[ spacing.spacing.xl ]]', transform: 'translateY(-50%)' }, 'center-right': { top: '50%', right: '[[ spacing.spacing.xl ]]', transform: 'translateY(-50%)' }, 'bottom-left': { bottom: '[[ spacing.spacing.xl ]]', left: '[[ spacing.spacing.xl ]]' }, 'bottom-center': { bottom: '[[ spacing.spacing.xl ]]', left: '50%', transform: 'translateX(-50%)' }, 'bottom-right': { bottom: '[[ spacing.spacing.xl ]]', right: '[[ spacing.spacing.xl ]]' }, }; ``` ### Spring Animation Pattern ```tsx // Entrance animation const entrance = spring({ frame: relativeFrame, fps, config: { damping: [[ motion.default_spring.config.damping ]], stiffness: [[ motion.default_spring.config.stiffness ]], mass: [[ motion.default_spring.config.mass ]], }, }); // Use in styles style={{ opacity: entrance, transform: `scale(${entrance})`, }} ``` ### Typing Animation Pattern ```tsx const typeText = (text: string, startFrame: number, speed: number = 0.05) => { const charsToShow = Math.floor((relativeFrame - startFrame) / (fps * speed)); return text.slice(0, Math.max(0, charsToShow)); }; // Usage const displayedText = typeText(content, 20, 0.04); ``` --- ## Step-by-Step Component Creation ### 1. Choose Category and Name ```bash # Decide on category and component name CATEGORY="overlays" COMPONENT="CallToAction" ``` ### 2. Create Directory Structure ```bash mkdir -p src/chuk_motion/components/$CATEGORY/$COMPONENT ``` ### 3. Create `__init__.py` ```python """CallToAction - Animated call-to-action button with customizable styles.""" from .builder import add_to_composition __all__ = ["add_to_composition"] ``` ### 4. Create `schema.py` Define your Pydantic model with all props and validation. ### 5. Create `tool.py` Register the MCP tool following the patterns above. ### 6. Create `builder.py` Create the CompositionBuilder method. ### 7. Create `template.tsx.j2` Create the React/Remotion component template. ### 8. Register Category (if new) If adding a new category, update: ```python # src/chuk_motion/generator/component_builder.py self.template_categories = [ "charts", "overlays", "layouts", "code", "animations", "content", "demo_realism", "your_new_category", # Add here ] ``` ### 9. Test Component Create a test example: ```python # examples/test_my_component.py from chuk_motion.utils.project_manager import ProjectManager from chuk_motion.generator.composition_builder import CompositionBuilder manager = ProjectManager() project = manager.create_project("test_component", theme="tech") manager.current_composition = CompositionBuilder(fps=30, width=1920, height=1080) manager.current_composition.add_call_to_action( start_time=1.0, duration=3.0, text="Click Me!", variant="bold", ) manager.generate_composition() ``` ### 10. Run and Validate ```bash python examples/test_my_component.py cd remotion-projects/test_component npm install npm run build ``` --- ## Testing & Validation ### Checklist - [ ] All 5 files created and properly structured - [ ] Pydantic schema validates correctly - [ ] MCP tool registers without errors (check decorator!) - [ ] Builder method uses time-based API (seconds) - [ ] Template uses correct Jinja2 delimiters `[[ ]]` - [ ] Design tokens accessed correctly (no double "px") - [ ] FPS obtained from `useVideoConfig()` - [ ] JSON props parsed if needed - [ ] Component renders without errors - [ ] Entrance animation works smoothly - [ ] Position mapping works for all positions - [ ] Component follows design system ### Common Test Cases 1. **Minimal props**: Test with only required props 2. **All props**: Test with all optional props set 3. **Edge cases**: Test extreme values (very long text, tiny dimensions) 4. **JSON props**: Test with both array and string formats 5. **Multiple instances**: Test with several instances at different times 6. **Layer conflicts**: Test with components on different layers --- ## Troubleshooting ### Build Errors #### "Module not found: Error: Can't resolve './components/MyComponent'" **Cause**: Component TSX file not generated **Fix**: Ensure category is registered in `component_builder.py` template_categories #### "TypeError: X.map is not a function" **Cause**: Array prop passed as JSON string but not parsed **Fix**: Add JSON parsing in component: ```tsx const parsed = typeof prop === 'string' ? JSON.parse(prop) : prop; ``` #### "Transform failed: Unexpected ','" **Cause**: Template variable rendered empty (e.g., `fps: ,`) **Fix**: Don't use template variables for runtime values like `fps`. Use `useVideoConfig()` instead. #### "'dict object' has no attribute 'X'" **Cause**: Incorrect token access in template **Fix**: Check token structure: - `spacing.spacing.lg` not `spacing.lg` - `motion.default_spring` not `motion.smooth_spring` - `typography.code_font` not `typography.mono_font` ### Template Rendering Issues #### "8pxpx" or double units **Cause**: Adding "px" to spacing tokens that already include it **Fix**: Use `[[ spacing.spacing.lg ]]` not `[[ spacing.spacing.lg ]]px` #### Font family not rendering **Cause**: Incorrect join syntax **Fix**: Use `"'[[ "', '".join(typography.body_font.fonts) ]]'"` ### MCP Registration Errors #### "unhashable type: 'list'" **Cause**: Using `List[dict]` in tool parameters **Fix**: Use `str` and parse JSON #### "unexpected keyword argument 'schema'" **Cause**: Using old decorator syntax **Fix**: Use only `@mcp.tool` without any parameters --- ## Best Practices 1. **Always validate props** with Pydantic schemas 2. **Use design tokens** instead of hardcoded values where possible 3. **Follow naming conventions**: PascalCase for components 4. **Document everything**: Add docstrings and comments 5. **Test thoroughly**: Check all variants and edge cases 6. **Keep templates simple**: Complex logic should be in helpers 7. **Reuse patterns**: Follow existing component structures 8. **Version control**: Commit each component separately 9. **Performance**: Avoid expensive calculations in render 10. **Accessibility**: Consider text contrast and sizing --- ## Reference Examples ### Simple Component See: `src/chuk_motion/components/overlays/TextOverlay/` ### Complex Component with JSON Props See: `src/chuk_motion/components/demo_realism/BrowserFrame/` ### Layout Component with Children See: `src/chuk_motion/components/layouts/Grid/` ### Chart Component See: `src/chuk_motion/components/charts/LineChart/` --- ## Quick Reference Card ``` Template Delimiters: [[ var ]] [% if %] Spacing: spacing.spacing.lg Border Radius: spacing.border_radius.md Colors: colors.primary[0] Typography: typography.body_font.fonts Font Weight: typography.font_weights.bold Motion: motion.default_spring.config.damping FPS: const { fps } = useVideoConfig() Font Size: 36 (hardcoded) Layer Values: 0 = Background 5 = Content 10 = Overlays Time API: start_time (seconds), duration (seconds) Frame Conversion: builder.seconds_to_frames(seconds) Component Creation: ComponentInstance(...) Builder Return: return builder ``` --- ## Additional Resources - [Token System Documentation](./token-system.md) - [Theme System](../src/chuk_motion/themes/youtube_themes.py) - [Remotion Documentation](https://www.remotion.dev/docs) - [Pydantic Documentation](https://docs.pydantic.dev/) --- **Last Updated**: November 2025 **Version**: 1.0.0

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/chrishayuk/chuk-mcp-remotion'

If you have feedback or need assistance with the MCP directory API, please join our Discord server