Coral UI

Component Composition

Learn how to embed components within components using slots and prop bindings.

Introduction

Coral supports powerful component composition—embedding components within other components, defining slots for content injection, and binding props and events across component boundaries.

Related: This guide covers composition concepts in depth. For API reference, see Component Composition API. For examples of composition in generated code, see Coral to React and Coral to HTML.

Component Instances

To embed one component inside another, use the COMPONENT_INSTANCE type:

{
  "name": "AlertDialog",
  "elementType": "div",
  "children": [
    {
      "name": "CloseButton",
      "type": "COMPONENT_INSTANCE",
      "$component": {
        "ref": "./button/button.coral.json",
        "version": "^1.0.0"
      },
      "propBindings": {
        "label": "Close",
        "intent": "secondary",
        "size": "sm"
      },
      "eventBindings": {
        "onClick": { "$event": "onClose" }
      }
    }
  ]
}

Component Reference

The $component field specifies which component to use:

{
  "$component": {
    "ref": "./path/to/component.coral.json",  // Relative path
    "version": "^1.0.0"                        // Optional semver constraint
  }
}

You can also reference components from other packages:

{
  "$component": {
    "ref": "@acme/design-system/Button",
    "version": "^2.0.0"
  }
}

Prop Bindings

Prop bindings pass values to component instances. They can be:

Static Values

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

Parent Prop References

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

Computed Values

{
  "propBindings": {
    "label": {
      "$computed": "ternary",
      "$inputs": [
        { "$prop": "loading" },
        "Loading...",
        { "$prop": "submitLabel" }
      ]
    }
  }
}

Prop Transforms

{
  "propBindings": {
    "aria-expanded": { "$prop": "isOpen", "$transform": "boolean" },
    "aria-disabled": { "$prop": "enabled", "$transform": "not" }
  }
}

Event Bindings

Forward events from child components to parent:

{
  "eventBindings": {
    "onClick": { "$event": "onButtonClick" },
    "onFocus": { "$event": "onButtonFocus" }
  }
}

Inline Handlers

For simple operations, use inline handlers:

{
  "eventBindings": {
    "onClick": {
      "$inline": "() => setIsOpen(false)"
    }
  }
}

Variant Overrides

Override variant values for component instances:

{
  "type": "COMPONENT_INSTANCE",
  "$component": { "ref": "./button/button.coral.json" },
  "propBindings": {
    "label": "Save"
  },
  "variantOverrides": {
    "intent": "primary",
    "size": "sm"
  }
}

Style Overrides

Apply style overrides to component instance root:

{
  "type": "COMPONENT_INSTANCE",
  "$component": { "ref": "./button/button.coral.json" },
  "styleOverrides": {
    "marginTop": "16px",
    "width": "100%"
  }
}

Slots

Slots define where content can be inserted into a component.

Defining Slots

{
  "name": "Card",
  "elementType": "div",

  "slots": [
    {
      "name": "default",
      "description": "Main content area",
      "required": true
    },
    {
      "name": "header",
      "description": "Card header content",
      "allowedElements": ["h1", "h2", "h3", "h4"]
    },
    {
      "name": "footer",
      "description": "Card footer content",
      "multiple": true
    },
    {
      "name": "actions",
      "description": "Action buttons",
      "allowedComponents": ["Button", "IconButton"],
      "multiple": true
    }
  ],

  "children": [
    {
      "name": "HeaderWrapper",
      "elementType": "div",
      "slotTarget": "header",
      "styles": { "padding": "16px", "borderBottom": "1px solid #eee" }
    },
    {
      "name": "ContentWrapper",
      "elementType": "div",
      "slotTarget": "default",
      "styles": { "padding": "16px" }
    },
    {
      "name": "FooterWrapper",
      "elementType": "div",
      "slotTarget": "footer",
      "styles": { "padding": "16px", "borderTop": "1px solid #eee" }
    }
  ]
}

Slot Properties

PropertyTypeDescription
namestringSlot identifier
descriptionstringDocumentation
requiredbooleanMust be provided
multiplebooleanAccepts multiple children
allowedElementsstring[]Allowed HTML elements
allowedComponentsstring[]Allowed component types
defaultContentobject[]Fallback content

Slot Targets

Mark where slot content renders with slotTarget:

{
  "name": "HeaderSlot",
  "elementType": "div",
  "slotTarget": "header",
  "slotFallback": [
    { "elementType": "h2", "textContent": "Default Title" }
  ]
}

Slot Bindings

When using a component with slots, bind content to them:

{
  "type": "COMPONENT_INSTANCE",
  "$component": { "ref": "./card/card.coral.json" },
  "slotBindings": {
    "header": { "elementType": "h2", "textContent": "Card Title" },
    "default": [
      { "elementType": "p", "textContent": "Card content goes here." }
    ],
    "actions": [
      {
        "type": "COMPONENT_INSTANCE",
        "$component": { "ref": "./button/button.coral.json" },
        "propBindings": { "label": "Save" }
      }
    ]
  }
}

Forwarding Slots

Forward parent slots to child components:

{
  "slotBindings": {
    "header": { "$slot": "cardHeader" },
    "default": { "$slot": "cardContent" }
  }
}

Component Sets

Group related components that share context:

{
  "type": "COMPONENT_SET",
  "name": "Tabs",
  "members": [
    {
      "name": "TabsList",
      "role": "container",
      "path": "./tabs-list/tabs-list.coral.json"
    },
    {
      "name": "Tab",
      "role": "item",
      "path": "./tab/tab.coral.json"
    },
    {
      "name": "TabsContent",
      "role": "content",
      "path": "./tabs-content/tabs-content.coral.json"
    }
  ],
  "sharedContext": {
    "activeTab": {
      "type": "string",
      "description": "Currently active tab ID"
    }
  }
}

Using Component Sets

{
  "type": "COMPONENT_INSTANCE",
  "$component": { "ref": "./tabs/tabs.coral.json" },
  "slotBindings": {
    "tabs": [
      { "type": "COMPONENT_INSTANCE", "$component": { "ref": "./tabs/tab.coral.json" }, "propBindings": { "id": "tab1", "label": "First" } },
      { "type": "COMPONENT_INSTANCE", "$component": { "ref": "./tabs/tab.coral.json" }, "propBindings": { "id": "tab2", "label": "Second" } }
    ],
    "panels": [
      { "type": "COMPONENT_INSTANCE", "$component": { "ref": "./tabs/tabs-content.coral.json" }, "propBindings": { "tabId": "tab1" }, "slotBindings": { "default": [{ "elementType": "p", "textContent": "First panel" }] } },
      { "type": "COMPONENT_INSTANCE", "$component": { "ref": "./tabs/tabs-content.coral.json" }, "propBindings": { "tabId": "tab2" }, "slotBindings": { "default": [{ "elementType": "p", "textContent": "Second panel" }] } }
    ]
  }
}

Utility Functions

Find Component Instances

import { findComponentInstances, getInstanceDependencies } from '@reallygoodwork/coral-core'

// Find all component instances in a tree
const instances = findComponentInstances(rootNode)
// [{ node, path: ['children', 0, 'children', 1] }, ...]

// Get dependencies for a component
const deps = getInstanceDependencies(component)
// ['./button/button.coral.json', './icon/icon.coral.json']

Resolve Component Instance

import { resolveComponentInstance } from '@reallygoodwork/coral-core'

const resolved = resolveComponentInstance(instanceNode, {
  getComponent: (ref) => pkg.components.get(ref),
  parentProps: { title: 'Hello' }
})
// Returns resolved component with merged props and bound events

Validate Composition

import { validateComposition, findCircularDependencies } from '@reallygoodwork/coral-core'

// Check for circular dependencies
const circles = findCircularDependencies(pkg)
if (circles.length > 0) {
  console.error('Circular dependencies found:', circles)
}

// Full composition validation
const result = validateComposition(pkg)
for (const error of result.errors) {
  console.error(`[${error.type}] ${error.message}`)
}

Flatten Component Tree

import { flattenComponentTree } from '@reallygoodwork/coral-core'

// Get flat list of all nodes (resolving instances)
const nodes = flattenComponentTree(rootNode, {
  getComponent: (ref) => pkg.components.get(ref)
})

Best Practices

1. Define Clear Slot Contracts

Document what each slot expects:

{
  "slots": [
    {
      "name": "trigger",
      "description": "Button or element that triggers the dropdown",
      "required": true,
      "allowedComponents": ["Button", "IconButton"]
    }
  ]
}

2. Use Default Slot Content

Provide sensible defaults:

{
  "slotTarget": "icon",
  "slotFallback": [
    {
      "type": "COMPONENT_INSTANCE",
      "$component": { "ref": "./icon/icon.coral.json" },
      "propBindings": { "name": "chevron-down" }
    }
  ]
}

3. Keep Component Instances Shallow

Avoid deeply nested composition—prefer flatter structures:

// Good: Flat structure with slots
{
  "type": "COMPONENT_INSTANCE",
  "$component": { "ref": "./dialog/dialog.coral.json" },
  "slotBindings": {
    "content": { "$slot": "dialogContent" }
  }
}

// Avoid: Deep nesting
{
  "children": [{
    "children": [{
      "children": [{
        "type": "COMPONENT_INSTANCE"
      }]
    }]
  }]
}

4. Validate Early

Use validation tools during development:

coral validate --strict

5. Document Prop Requirements

Clear documentation prevents integration errors:

{
  "propBindings": {
    "onClose": { "$event": "onClose" }  // Required: Parent must handle onClose
  }
}

Next Steps

API Documentation

On this page