Creating Custom Components

Learn how to integrate your existing React components into UI Builder. This focused guide shows you how to take any React component and make it available in the visual editor.

The Process: From React Component to UI Builder

Integrating a custom component into UI Builder is a straightforward 3-step process:

  1. Your React Component - Works as-is, no modifications needed
  2. Create Component Definition - Define schema and configuration
  3. Add to Registry - Include in your componentRegistry prop

Step 1: Your Existing React Component

UI Builder works with your existing React components without any modifications. Here's a realistic example:

tsx
1// components/ui/user-card.tsx 2interface UserCardProps { 3 className?: string; 4 children?: React.ReactNode; 5 name: string; 6 email: string; 7 role: 'admin' | 'user' | 'viewer'; 8 avatarUrl?: string; 9 isOnline?: boolean; 10} 11 12export function UserCard({ 13 className, 14 children, 15 name, 16 email, 17 role, 18 avatarUrl, 19 isOnline = false 20}: UserCardProps) { 21 return ( 22 <div className={cn("border rounded-lg p-4 bg-card", className)}> 23 <div className="flex items-center gap-3"> 24 {avatarUrl && ( 25 <img 26 src={avatarUrl} 27 alt={name} 28 className="w-10 h-10 rounded-full" 29 /> 30 )} 31 <div className="flex-1"> 32 <div className="flex items-center gap-2"> 33 <h3 className="font-semibold">{name}</h3> 34 {isOnline && ( 35 <div className="w-2 h-2 bg-green-500 rounded-full" /> 36 )} 37 </div> 38 <p className="text-sm text-muted-foreground">{email}</p> 39 <span className={cn( 40 "text-xs px-2 py-1 rounded", 41 role === 'admin' ? 'bg-red-100 text-red-700' : 42 role === 'user' ? 'bg-blue-100 text-blue-700' : 43 'bg-gray-100 text-gray-700' 44 )}> 45 {role} 46 </span> 47 </div> 48 </div> 49 {children && ( 50 <div className="mt-3 pt-3 border-t"> 51 {children} 52 </div> 53 )} 54 </div> 55 ); 56}

Key Requirements for UI Builder Components:

  • Accept className: For styling integration
  • Accept children: For content composition (optional)
  • Use TypeScript interfaces: Clear prop definitions help with schema creation
  • Follow your design system: Keep consistent with existing patterns

Step 2: Create the Component Definition

Next, create a definition that tells UI Builder how to work with your component:

tsx
1import { z } from 'zod'; 2import { UserCard } from '@/components/ui/user-card'; 3import { classNameFieldOverrides, childrenFieldOverrides } from '@/lib/ui-builder/registry/form-field-overrides'; 4 5const userCardDefinition = { 6 // The React component itself 7 component: UserCard, 8 9 // Zod schema defining props for the auto-generated form 10 schema: z.object({ 11 className: z.string().optional(), 12 children: z.any().optional(), 13 name: z.string().default('John Doe'), 14 email: z.string().default('john@example.com'), 15 role: z.enum(['admin', 'user', 'viewer']).default('user'), 16 avatarUrl: z.string().optional(), 17 isOnline: z.boolean().default(false), 18 }), 19 20 // Import path for code generation 21 from: '@/components/ui/user-card', 22 23 // Custom form field overrides (optional) 24 fieldOverrides: { 25 className: (layer) => classNameFieldOverrides(layer), 26 children: (layer) => childrenFieldOverrides(layer), 27 email: (layer) => ({ 28 inputProps: { 29 type: 'email', 30 placeholder: 'user@example.com' 31 } 32 }), 33 avatarUrl: (layer) => ({ 34 inputProps: { 35 type: 'url', 36 placeholder: 'https://example.com/avatar.jpg' 37 } 38 }) 39 } 40};

Schema Design Tips:

  • Use .default() values for better user experience
  • Use z.coerce.number() for numeric inputs (handles string conversion)
  • Use z.coerce.date() for date inputs (handles string conversion)
  • Always include className and children props for flexibility
  • Use clear enum values that make sense to non-technical users

Step 3: Add to Your Component Registry

Include your definition in the componentRegistry prop:

tsx
1import UIBuilder from '@/components/ui/ui-builder'; 2import { primitiveComponentDefinitions } from '@/lib/ui-builder/registry/primitive-component-definitions'; 3import { complexComponentDefinitions } from '@/lib/ui-builder/registry/complex-component-definitions'; 4 5const myComponentRegistry = { 6 // Include pre-built components 7 ...primitiveComponentDefinitions, // div, span, img, etc. 8 ...complexComponentDefinitions, // Button, Badge, Card, etc. 9 10 // Add your custom components 11 UserCard: userCardDefinition, 12 // Add more custom components... 13}; 14 15export function App() { 16 return ( 17 <UIBuilder componentRegistry={myComponentRegistry} /> 18 ); 19}

That's it! Your UserCard component is now available in the UI Builder editor.

Advanced Features

Automatic Variable Bindings

Make components automatically bind to system data when added to the canvas:

tsx
1UserCard: { 2 component: UserCard, 3 schema: z.object({...}), 4 from: '@/components/ui/user-card', 5 6 // Auto-bind properties to variables when component is added 7 defaultVariableBindings: [ 8 { 9 propName: 'name', 10 variableId: 'current-user-name', 11 immutable: false // Users can unbind this 12 }, 13 { 14 propName: 'email', 15 variableId: 'current-user-email', 16 immutable: true // Locked for security 17 }, 18 { 19 propName: 'role', 20 variableId: 'current-user-role', 21 immutable: true // Prevent role tampering 22 } 23 ] 24}

Use Cases for Variable Bindings:

  • User profiles: Auto-bind to current user data
  • Multi-tenant apps: Bind to tenant-specific branding
  • System data: Connect to live counters, statuses, timestamps
  • A/B testing: Bind to feature flag variables

Immutable Bindings: Set immutable: true to prevent users from unbinding critical data like user IDs, brand colors, or security permissions.

Default Children Structure

Provide default child components when users add your component:

tsx
1UserCard: { 2 component: UserCard, 3 schema: z.object({...}), 4 from: '@/components/ui/user-card', 5 6 // Default children when component is added 7 defaultChildren: [ 8 { 9 id: 'user-actions', 10 type: 'div', 11 name: 'Action Buttons', 12 props: { 13 className: 'flex gap-2 mt-2' 14 }, 15 children: [ 16 { 17 id: 'edit-btn', 18 type: 'Button', 19 name: 'Edit Button', 20 props: { 21 variant: 'outline', 22 size: 'sm' 23 }, 24 children: 'Edit Profile' 25 }, 26 { 27 id: 'message-btn', 28 type: 'Button', 29 name: 'Message Button', 30 props: { 31 variant: 'default', 32 size: 'sm' 33 }, 34 children: 'Send Message' 35 } 36 ] 37 } 38 ] 39}

Important: All component types referenced in defaultChildren must exist in your componentRegistry (like Button and div in the example above).

Complete Example: Blog Post Card

Here's a complete example showing a blog post component with all advanced features:

tsx
1// components/ui/blog-post-card.tsx 2interface BlogPostCardProps { 3 className?: string; 4 children?: React.ReactNode; 5 title: string; 6 excerpt: string; 7 author: string; 8 publishedAt: Date; 9 readTime: number; 10 category: string; 11 featured?: boolean; 12} 13 14export function BlogPostCard({ 15 className, 16 children, 17 title, 18 excerpt, 19 author, 20 publishedAt, 21 readTime, 22 category, 23 featured = false 24}: BlogPostCardProps) { 25 return ( 26 <article className={cn( 27 "border rounded-lg p-6 bg-card hover:shadow-md transition-shadow", 28 featured && "border-primary bg-primary/5", 29 className 30 )}> 31 {featured && ( 32 <div className="text-xs text-primary font-medium mb-2"> 33 ⭐ Featured Post 34 </div> 35 )} 36 <div className="text-sm text-muted-foreground mb-2"> 37 {category}{readTime} min read 38 </div> 39 <h2 className="text-xl font-semibold mb-3">{title}</h2> 40 <p className="text-muted-foreground mb-4">{excerpt}</p> 41 <div className="flex items-center justify-between"> 42 <div className="text-sm"> 43 By {author}{publishedAt.toLocaleDateString()} 44 </div> 45 {children} 46 </div> 47 </article> 48 ); 49} 50 51// Component definition with all features 52const blogPostCardDefinition = { 53 component: BlogPostCard, 54 schema: z.object({ 55 className: z.string().optional(), 56 children: z.any().optional(), 57 title: z.string().default('Sample Blog Post Title'), 58 excerpt: z.string().default('A brief description of the blog post content...'), 59 author: z.string().default('John Author'), 60 publishedAt: z.coerce.date().default(new Date()), 61 readTime: z.coerce.number().default(5), 62 category: z.string().default('Technology'), 63 featured: z.boolean().default(false), 64 }), 65 from: '@/components/ui/blog-post-card', 66 fieldOverrides: { 67 className: (layer) => classNameFieldOverrides(layer), 68 children: (layer) => childrenFieldOverrides(layer), 69 excerpt: (layer) => ({ 70 fieldType: 'textarea', 71 inputProps: { 72 placeholder: 'Brief post description...' 73 } 74 }), 75 publishedAt: (layer) => ({ 76 fieldType: 'date' 77 }) 78 }, 79 defaultVariableBindings: [ 80 { 81 propName: 'author', 82 variableId: 'current-author-name', 83 immutable: false 84 } 85 ], 86 defaultChildren: [ 87 { 88 id: 'post-actions', 89 type: 'div', 90 name: 'Post Actions', 91 props: { className: 'flex gap-2' }, 92 children: [ 93 { 94 id: 'read-more', 95 type: 'Button', 96 name: 'Read More', 97 props: { 98 variant: 'outline', 99 size: 'sm' 100 }, 101 children: 'Read More' 102 } 103 ] 104 } 105 ] 106};

See It In Action

View the Immutable Bindings Example to see custom components with automatic variable bindings in action. This example demonstrates:

  • Custom components with system data bindings
  • Immutable bindings for security-sensitive data
  • Real-world component integration patterns

Testing Your Custom Components

After adding your component to the registry:

  1. Add to Canvas: Find your component in the component panel and add it
  2. Test Properties: Use the properties panel to configure all props
  3. Check Children: Verify children support works if implemented
  4. Test Variables: If using variable bindings, test the binding UI
  5. Export Code: Use the export feature to verify generated React code
  6. Render Test: Test with LayerRenderer to ensure runtime rendering works

Best Practices

Component Design

  • Keep props simple: Complex nested objects are hard to edit visually
  • Provide sensible defaults: Reduce setup friction for content creators
  • Use semantic prop names: Make properties self-explanatory
  • Handle edge cases: Always provide fallbacks for optional data
  • Follow accessibility guidelines: Ensure components work for all users

Schema Design

  • Use meaningful defaults: Help users understand expected values
  • Validate appropriately: Don't over-constrain creative usage
  • Group related props: Use nested objects for logical groupings (sparingly)
  • Provide helpful enums: Use descriptive enum values instead of codes
  • Consider the editing experience: Think about how non-technical users will configure props

Variable Bindings

  • Use immutable bindings for system data, security info, and brand consistency
  • Leave content editable by not binding text props that should be customizable
  • Provide meaningful variable names that clearly indicate their purpose
  • Test binding scenarios to ensure the editing experience is smooth

Performance

  • Minimize re-renders: Use React.memo if your component is expensive to render
  • Optimize images: Handle image loading states and errors gracefully
  • Consider bundle size: Avoid heavy dependencies in components used in the editor

With these patterns, your custom components will provide a seamless editing experience while maintaining the flexibility and power of your existing React components.