Coral UI

@reallygoodwork/coral-to-react

Generate React component code from Coral specifications.

npm

Generate React component code from Coral specifications. This package transforms Coral JSON specifications into fully functional React components with TypeScript support, state management, event handlers, styling, and component composition.

Features

  • Complete React Components - Generate fully functional React components from Coral specs
  • TypeScript Support - Automatically generate TypeScript interfaces for props (supports both legacy and new props systems)
  • Component Formats - Choose between function declarations or arrow functions
  • Style Options - Output inline styles or separate CSS classes with Tailwind support
  • Variant Strategies - Support for CVA (Class Variance Authority), inline styles, or custom variant handling
  • Component Composition - Render component instances with prop/slot/event bindings
  • Package Generation - Generate entire component libraries from Coral packages
  • State Management - Generate React hooks (useState, useEffect, etc.)
  • Event Handlers - Create methods and event handler functions
  • Prettier Integration - Optional code formatting with Prettier
  • Import Management - Generate necessary import statements automatically

Installation

npm install @reallygoodwork/coral-to-react
pnpm add @reallygoodwork/coral-to-react
yarn add @reallygoodwork/coral-to-react

API Reference

coralToReact

Main function to convert a Coral specification into React component code.

Prop

Type

Returns: Promise<{ reactCode: string; cssCode: string }> - Object containing generated React code and CSS (if using className format)

Basic Example

import { coralToReact } from '@reallygoodwork/coral-to-react';
import type { CoralRootNode } from '@reallygoodwork/coral-core';

const spec: CoralRootNode = {
  name: 'Button',
  componentName: 'Button',
  elementType: 'button',
  componentProperties: {
    label: { type: 'string' },
    onClick: { type: 'function' }
  },
  styles: {
    padding: '10px 20px',
    backgroundColor: '#007bff',
    color: '#ffffff',
    border: 'none',
    borderRadius: '4px',
    cursor: 'pointer'
  },
  textContent: '{props.label}'
};

const { reactCode, cssCode } = await coralToReact(spec);

Generated Output:

import React from 'react'

interface ButtonProps {
  label: string
  onClick?: () => void
}

export function Button(props: ButtonProps) {
  return (
    <button style={{ padding: '10px 20px', backgroundColor: '#007bff', color: '#ffffff', border: 'none', borderRadius: '4px', cursor: 'pointer' }}>
      {props.label}
    </button>
  )
}

generatePackage

NEW - Generate an entire component library from a Coral package. This function processes all components in a package, resolves dependencies, and generates a complete React component library with proper imports and exports.

Prop

Type

Returns: Promise<PackageGenerationResult> - Object containing component files, CSS files, and index file

Package Generation Example

import { loadPackage } from '@reallygoodwork/coral-core';
import { generatePackage } from '@reallygoodwork/coral-to-react';
import * as fs from 'fs/promises';

// Load your Coral package
const pkg = await loadPackage('./coral.config.json', {
  readFile: (path) => fs.readFile(path, 'utf-8'),
});

// Generate entire component library
const result = await generatePackage(pkg, {
  componentFormat: 'arrow',
  styleFormat: 'className',
  includeTypes: true,
  variantStrategy: 'cva',
});

// Write all component files
for (const file of result.components) {
  await fs.writeFile(`./dist/${file.path}`, file.content);
}

// Write CSS files
for (const file of result.styles) {
  await fs.writeFile(`./dist/${file.path}`, file.content);
}

// Write index file
if (result.index) {
  await fs.writeFile(`./dist/${result.index.path}`, result.index.content);
}

The generated package will include:

  • All components in dependency order (dependencies generated first)
  • Proper import statements between components
  • CSS files (if using className format)
  • Index file with all exports
  • TypeScript type exports (if includeTypes is true)

Options

Configuration options for React generation.

Prop

Type

Component Format

Choose between function declaration or arrow function style:

Function Declaration (default):

export function Button(props: ButtonProps) {
  return <button>Click me</button>
}

Arrow Function:

export const Button = (props: ButtonProps) => {
  return <button>Click me</button>
}

Style Format

Inline Styles (default):

<button style={{ padding: '10px', backgroundColor: '#007bff' }}>
  Click me
</button>

CSS Classes:

// Component
<button className="coral-root-button">Click me</button>

// Separate CSS file
.coral-root-button {
  padding: 10px;
  background-color: #007bff;
}

Automatic CVA Detection

When using styleFormat: 'className' with components that have variants, CVA is automatically enabled even if variantStrategy is not explicitly set. The generator detects variants and uses CVA to manage them efficiently.

Variant Strategy

The variantStrategy option controls how component variants are handled:

Learn more: For comprehensive information about Coral's variant system, see the Component Variants guide and Component Variants API.

'inline' (default): Variants are applied as inline styles or CSS classes directly in the JSX. Use this when you want simple, direct style application without CVA.

'cva': Uses Class Variance Authority for variant management. Requires styleFormat: 'className' and Tailwind CSS. Automatically generates CVA configuration from Coral variant definitions. Automatically enabled when styleFormat: 'className' and variants are detected.

'custom': Allows custom variant handling (for advanced use cases).

When CVA is Used

CVA is automatically used when:

  • styleFormat: 'className' is set, AND
  • The component has componentVariants defined, AND
  • variantStrategy is 'cva' or not specified (defaults to auto-detection)

You can explicitly disable CVA by setting variantStrategy: 'inline'.

CVA Example

When using variantStrategy: 'cva' (or auto-detected) with variants:

// Coral spec with variants
const buttonSpec = {
  componentVariants: {
    axes: [
      { name: 'intent', values: ['primary', 'secondary'], default: 'primary' },
      { name: 'size', values: ['sm', 'md', 'lg'], default: 'md' }
    ],
    compounds: [
      {
        conditions: { intent: 'destructive', size: 'sm' },
        description: 'Small destructive buttons need extra emphasis'
      }
    ]
  },
  styles: {
    display: 'inline-flex',
    borderRadius: '6px',
    fontWeight: '500'
  },
  variantStyles: {
    intent: {
      primary: { backgroundColor: '#007bff', color: '#fff' },
      secondary: { backgroundColor: '#6c757d', color: '#fff' }
    },
    size: {
      sm: { padding: '4px 8px', fontSize: '12px' },
      md: { padding: '8px 16px', fontSize: '14px' },
      lg: { padding: '12px 24px', fontSize: '16px' }
    }
  },
  compoundVariantStyles: [
    {
      conditions: { intent: 'destructive', size: 'sm' },
      styles: { fontWeight: 'bold' }
    }
  }
};

const { reactCode } = await coralToReact(buttonSpec, {
  styleFormat: 'className',
  variantStrategy: 'cva', // or omit - auto-detected
  includeTypes: true
});

Generated React code:

import { cva } from 'class-variance-authority';
import { cn } from '@/lib/utils';

const buttonVariants = cva('inline-flex rounded font-medium', {
  variants: {
    intent: {
      primary: 'bg-blue-500 text-white',
      secondary: 'bg-gray-500 text-white'
    },
    size: {
      sm: 'px-2 py-1 text-xs',
      md: 'px-4 py-2 text-sm',
      lg: 'px-6 py-3 text-base'
    }
  },
  compoundVariants: [
    {
      intent: 'destructive',
      size: 'sm',
      class: 'font-bold'
    }
  ],
  defaultVariants: {
    intent: 'primary',
    size: 'md'
  }
});

interface ButtonProps {
  label: string
  intent?: 'primary' | 'secondary'
  size?: 'sm' | 'md' | 'lg'
  onClick?: () => void
}

export const Button = (props: ButtonProps) => {
  const className = buttonVariants({
    intent: props.intent,
    size: props.size
  });
  return (
    <button className={className} onClick={props.onClick}>
      {props.label}
    </button>
  );
};

Key Features:

  • Base styles are converted to Tailwind classes and included in the CVA base
  • Variant styles become CVA variant definitions
  • Compound variants are automatically converted to CVA compound variants
  • Default variants are set from Coral's default values
  • TypeScript props automatically include variant axes as optional props
  • Style conversion uses @reallygoodwork/style-to-tailwind to convert CSS to Tailwind classes

Note: CVA generation requires the @reallygoodwork/style-to-tailwind package to convert Coral styles to Tailwind classes. See the Style to Tailwind documentation for details.


Component Composition

NEW - Components can now embed other components using COMPONENT_INSTANCE type. This enables building complex UIs by composing smaller components together.

Learn more: For comprehensive information about component composition, prop bindings, slot bindings, and event bindings, see the Component Composition guide and Component Composition API.

Basic Component Instance

const cardSpec: CoralRootNode = {
  name: 'Card',
  elementType: 'div',
  props: {
    title: { type: 'string', required: true },
    onSave: { type: 'function' }
  },
  children: [
    {
      id: 'save-button',
      name: 'SaveButton',
      type: 'COMPONENT_INSTANCE',
      elementType: 'button',
      $component: {
        ref: './button.coral.json'
      },
      propBindings: {
        label: 'Save',
        intent: 'primary'
      },
      eventBindings: {
        onClick: { $event: 'onSave' }
      }
    }
  ]
};

const { reactCode } = await coralToReact(cardSpec);

Generated Output:

import { Button } from './Button';

export function Card(props: CardProps) {
  return (
    <div>
      <Button label="Save" intent="primary" onClick={props.onSave} />
    </div>
  );
}

Prop Bindings

Prop bindings can be static values or dynamic references to parent props:

Static Values:

{
  "propBindings": {
    "label": "Click me",
    "intent": "primary",
    "disabled": false
  }
}

Dynamic Prop References:

{
  "propBindings": {
    "label": { "$prop": "buttonLabel" },
    "disabled": { "$prop": "isDisabled" }
  }
}

This generates:

<Button label={props.buttonLabel} disabled={props.isDisabled} />

Event Bindings

Event bindings forward events from child components to parent handlers:

{
  "eventBindings": {
    "onClick": { "$event": "onSave" },
    "onChange": { "$event": "handleChange" }
  }
}

This generates:

<Button onClick={props.onSave} onChange={props.handleChange} />

Slot Bindings

Slot bindings pass content to component slots:

{
  "slotBindings": {
    "default": { "$prop": "children" },
    "header": [
      {
        "elementType": "h2",
        "textContent": "Card Title"
      }
    ]
  }
}

Variant Overrides

Override variant values for component instances:

{
  "variantOverrides": {
    "intent": "destructive",
    "size": "lg"
  }
}

This locks the component instance to specific variant values, regardless of parent props.

Flattening Composition

By default, component instances are rendered as separate React components with imports. If you want to flatten them (resolve instances to their actual node structures), use the flattenComposition option:

import { loadPackage } from '@reallygoodwork/coral-core';
import { coralToReact } from '@reallygoodwork/coral-to-react';

const pkg = await loadPackage('./coral.config.json', options);

const { reactCode } = await coralToReact(cardSpec, {
  flattenComposition: true,
  package: pkg  // Required for flattening
});

When flattenComposition: true, component instances are resolved and inlined into the parent component's JSX structure.

Note: For more details on component composition, see the Composition guide.


Props Interface Generation

The generatePropsInterface function supports both the legacy componentProperties format and the new props definition system.

Learn more: For comprehensive information about defining props and events, see the Props & Events guide and Props & Events API.

Legacy Props System

const spec = {
  componentProperties: {
    label: { type: 'string' },
    count: { type: 'number', optional: true }
  }
};

const interface = generatePropsInterface(spec.componentProperties, 'Button');
// interface ButtonProps {
//   label: string
//   count?: number
// }

New Props System

const spec = {
  props: {
    label: {
      type: 'string',
      required: true,
      description: 'Button text'
    },
    intent: {
      type: { enum: ['primary', 'secondary'] },
      default: 'primary'
    },
    disabled: {
      type: 'boolean',
      default: false
    }
  }
};

const interface = generatePropsInterface(spec, 'Button');
// interface ButtonProps {
//   label: string
//   intent?: 'primary' | 'secondary'
//   disabled?: boolean
// }

The function automatically detects which props system is being used and generates the appropriate TypeScript interface.

Note: For details on the new props system, see the Props & Events documentation.


Helper Functions

generateComponent

Lower-level function for generating a single component. Used internally by coralToReact and generatePackage.

import { generateComponent } from '@reallygoodwork/coral-to-react';

const { reactCode, cssCode } = await generateComponent(spec, options);

generateJSXElement

Generate JSX for a single Coral node.

import { generateJSXElement } from '@reallygoodwork/coral-to-react';

const jsx = generateJSXElement(node, indent, idMapping);
// '<div style={{ padding: "20px" }}>...</div>'

generatePropsInterface

Generate TypeScript props interface. Supports both legacy and new props systems.

import { generatePropsInterface } from '@reallygoodwork/coral-to-react';

// Legacy format
const interface = generatePropsInterface(
  { title: { type: 'string' } },
  'Card'
);

// New format (pass entire spec)
const interface = generatePropsInterface(spec, 'Card');

generateStateHooks

Generate React useState hooks from Coral state specifications.

import { generateStateHooks } from '@reallygoodwork/coral-to-react';

const hooks = generateStateHooks([
  { name: 'count', setter: 'setCount', initialValue: 0 }
]);
// 'const [count, setCount] = useState(0)'

generateMethods

Generate method/function code from Coral method specifications.

import { generateMethods } from '@reallygoodwork/coral-to-react';

const methods = generateMethods([
  { name: 'handleClick', parameters: [], body: 'console.log("clicked")' }
]);
// 'const handleClick = () => {\n  console.log("clicked")\n}'

generateCSS

Generate CSS code from Coral styles (used when styleFormat: 'className').

import { generateCSS } from '@reallygoodwork/coral-to-react';

const css = generateCSS(spec, idMapping);
// '.component-root { padding: 20px; }'

generateImports

Generate import statements from Coral import specifications.

import { generateImports } from '@reallygoodwork/coral-to-react';

const imports = generateImports([
  { source: 'react', specifiers: [{ name: 'useState' }] }
]);
// "import { useState } from 'react'"

stylesToInlineStyle

Convert Coral style objects to React inline style objects.

import { stylesToInlineStyle } from '@reallygoodwork/coral-to-react';

const inlineStyle = stylesToInlineStyle({ padding: '20px', margin: '10px' });
// 'style={{ padding: "20px", margin: "10px" }}'

generateCVA

Generate Class Variance Authority (CVA) configuration and code from Coral component variants. This function is used internally by coralToReact but can also be called directly for custom CVA generation.

import { generateCVA } from '@reallygoodwork/coral-to-react';

const result = generateCVA(buttonSpec, {
  includeStates: false, // Include state styles (hover, focus, etc.)
  nodeId: undefined, // Generate for specific node (defaults to root)
});

// Returns:
// {
//   config: CVAConfig, // CVA configuration object
//   code: string, // Generated CVA function call code
//   imports: string[], // Required import statements
//   stateConfigs?: Record<string, CVAConfig> // State-specific CVA configs
// }

Example:

const buttonSpec = {
  componentVariants: {
    axes: [
      { name: 'intent', values: ['primary', 'secondary'], default: 'primary' },
      { name: 'size', values: ['sm', 'md', 'lg'], default: 'md' }
    ]
  },
  variantStyles: {
    intent: {
      primary: { backgroundColor: '#007bff', color: '#fff' },
      secondary: { backgroundColor: '#6c757d', color: '#fff' }
    },
    size: {
      sm: { padding: '4px 8px', fontSize: '12px' },
      md: { padding: '8px 16px', fontSize: '14px' },
      lg: { padding: '12px 24px', fontSize: '16px' }
    }
  }
};

const result = generateCVA(buttonSpec);

// result.code:
// const componentVariants = cva('', {
//   variants: {
//     intent: {
//       primary: 'bg-blue-500 text-white',
//       secondary: 'bg-gray-500 text-white'
//     },
//     size: {
//       sm: 'px-2 py-1 text-xs',
//       md: 'px-4 py-2 text-sm',
//       lg: 'px-6 py-3 text-base'
//     }
//   },
//   defaultVariants: {
//     intent: 'primary',
//     size: 'md'
//   }
// })

generateCNHelper

Generate the cn utility function for merging Tailwind classes. This helper combines clsx and tailwind-merge for proper class merging and is automatically included when generating components with CVA support.

import { generateCNHelper } from '@reallygoodwork/coral-to-react';

const cnHelper = generateCNHelper();

// Returns:
// import { clsx, type ClassValue } from 'clsx'
// import { twMerge } from 'tailwind-merge'
//
// export function cn(...inputs: ClassValue[]) {
//   return twMerge(clsx(inputs))
// }

This utility is commonly used with CVA for merging variant classes with additional classes:

const className = cn(
  buttonVariants({ intent: props.intent, size: props.size }),
  props.className // Merge with additional classes
);

Complete Examples

Button with Variants and CVA

import { coralToReact } from '@reallygoodwork/coral-to-react';

const buttonSpec = {
  name: 'Button',
  componentName: 'Button',
  elementType: 'button',
  componentVariants: {
    axes: [
      { name: 'intent', values: ['primary', 'secondary'], default: 'primary' },
      { name: 'size', values: ['sm', 'md', 'lg'], default: 'md' }
    ]
  },
  props: {
    label: { type: 'string', required: true },
    disabled: { type: 'boolean', default: false }
  },
  events: {
    onClick: { description: 'Button click handler' }
  },
  styles: {
    display: 'inline-flex',
    alignItems: 'center',
    borderRadius: '6px',
    fontWeight: '500',
    border: 'none',
    cursor: 'pointer'
  },
  variantStyles: {
    intent: {
      primary: { backgroundColor: '#007bff', color: '#ffffff' },
      secondary: { backgroundColor: '#6c757d', color: '#ffffff' }
    },
    size: {
      sm: { padding: '4px 8px', fontSize: '12px' },
      md: { padding: '8px 16px', fontSize: '14px' },
      lg: { padding: '12px 24px', fontSize: '16px' }
    }
  },
  textContent: { $prop: 'label' },
  elementAttributes: {
    disabled: { $prop: 'disabled' }
  },
  eventHandlers: {
    onClick: { $event: 'onClick' }
  }
};

const { reactCode, cssCode } = await coralToReact(buttonSpec, {
  componentFormat: 'arrow',
  styleFormat: 'className',
  variantStrategy: 'cva',
  includeTypes: true
});

Card with Component Composition

const cardSpec = {
  name: 'Card',
  componentName: 'Card',
  elementType: 'div',
  props: {
    title: { type: 'string', required: true },
    description: { type: 'string' },
    onAction: { type: 'function' }
  },
  styles: {
    padding: '24px',
    borderRadius: '12px',
    backgroundColor: '#ffffff',
    boxShadow: '0 2px 8px rgba(0,0,0,0.1)'
  },
  children: [
    {
      elementType: 'h3',
      textContent: { $prop: 'title' },
      styles: { fontSize: '20px', fontWeight: 'bold', marginBottom: '8px' }
    },
    {
      elementType: 'p',
      textContent: { $prop: 'description' },
      styles: { color: '#666', lineHeight: '1.5' }
    },
    {
      id: 'action-button',
      type: 'COMPONENT_INSTANCE',
      $component: { ref: './button.coral.json' },
      propBindings: {
        label: 'Learn More',
        intent: 'primary'
      },
      eventBindings: {
        onClick: { $event: 'onAction' }
      }
    }
  ]
};

const { reactCode } = await coralToReact(cardSpec, {
  componentFormat: 'arrow',
  includeTypes: true
});

Component with State and Methods

const counterSpec = {
  name: 'Counter',
  componentName: 'Counter',
  elementType: 'div',
  stateHooks: [
    {
      name: 'count',
      setter: 'setCount',
      initialValue: 0,
      tsType: 'number',
      hookType: 'useState'
    }
  ],
  methods: [
    {
      name: 'increment',
      body: 'setCount(count + 1)',
      parameters: []
    },
    {
      name: 'decrement',
      body: 'setCount(count - 1)',
      parameters: []
    }
  ],
  children: [
    {
      elementType: 'button',
      elementAttributes: { onClick: '{increment}' },
      textContent: '+'
    },
    {
      elementType: 'span',
      textContent: '{count}'
    },
    {
      elementType: 'button',
      elementAttributes: { onClick: '{decrement}' },
      textContent: '-'
    }
  ]
};

const { reactCode } = await coralToReact(counterSpec);

Type Exports

Options

Configuration options for React generation.

Prop

Type

GeneratedFile

Represents a generated file in package generation.

Prop

Type

PackageGenerationResult

Result of package generation.

Prop

Type


Style Conversion

The package automatically converts Coral style objects to React-compatible formats:

Dimension Objects

// Coral
{ padding: { value: 20, unit: 'px' } }

// React (inline)
{ padding: '20px' }

// React (CSS)
padding: 20px;

Color Objects

// Coral
{ backgroundColor: { hex: '#007bff', rgb: {...}, hsl: {...} } }

// React (inline)
{ backgroundColor: '#007bff' }

// React (CSS)
background-color: #007bff;

Numeric Values

Numeric values are automatically converted:

  • Most properties get px units added
  • Unitless properties (like fontWeight, lineHeight, zIndex) remain as numbers

Additional Resources


Additional Resources

On this page