Skip to content

Component Composition

When to Use

  • You're including one component in another
  • You need to decide between include(), embed, or render arrays
  • You're nesting components

Decision

If you need... Use... Why
Simple prop-only inclusion include() with with_context = false Isolated, predictable, lowest overhead
To fill a slot via a Twig block embed Only tag that can override a {% block %} — but only if the child renders that slot as a block
Programmatic composition (preprocess, controller, form, hook) Render array (#type: component) The only path that sets both a block override and a context variable for every slot

include() can also fill a slot, but only if the component template prints it as a variable ({{ header }}) rather than as a {% block %}. It cannot override blocks. That is the whole reason embed exists.

Precondition: the component you are embedding must render that slot through {% block name %}. If its template prints a bare {{ name }} instead, or wraps the block in {% if name %}, your block override is silently discarded — see Twig Templates in SDCs. Open the component's .twig before you write the embed.

Pattern

Pattern: include() Function (Most Common)

Use for simple component inclusion with props only.

{# Simple inclusion #}
{{ include('my_theme:button', {
  text: 'Click Me',
  variant: 'primary',
  disabled: false
}) }}

{# With context control (recommended) #}
{{ include('my_theme:button', {
  text: 'Save',
  variant: 'primary'
}, with_context = false) }}

WHY with_context = false: Prevents automatic variable leakage into the component, keeping components isolated and predictable.

Pattern: embed Tag (Only for Slots)

Use when populating slots via Twig blocks.

Reference (component side): /core/themes/olivero/components/teaser/teaser.twig

{% embed 'my_theme:card' with {
  title: node.label,
  variant: 'featured'
} only %}

  {% block content %}
    {{ content.body }}
    {{ include('my_theme:button', {
      text: 'Read More',
      url: node.url
    }) }}
  {% endblock %}

  {% block footer %}
    {{ content.field_tags }}
  {% endblock %}

{% endembed %}

Pattern: Render Arrays (Programmatic)

Use in preprocessing, controllers, forms, hooks.

// In .theme file or controller
$build = [
  '#type' => 'component',
  '#component' => 'my_theme:card',
  '#props' => [
    'title' => $node->label(),
    'variant' => 'featured',
  ],
  '#slots' => [
    'content' => $node->body->view('teaser'),
    'footer' => $node->field_tags->view('compact'),
  ],
];

Pattern: Nested Components

Components can include other components via slots or direct inclusion.

{# Parent component with child components in slots #}
{% embed 'my_theme:hero-banner' with { variant: 'primary' } only %}

  {% block content %}
    <h1>{{ title }}</h1>
    {{ include('my_theme:button', {
      text: 'Get Started',
      variant: 'primary',
      size: 'large'
    }) }}
  {% endblock %}

{% endembed %}

Common Mistakes

Common Mistake: Using embed when include() is sufficient. WHY: embed has overhead and complexity. Only use when you need to populate slots with Twig blocks. For props-only components, use include().

Common Mistake: Hardcoding child components instead of using slots. WHY: Reduces flexibility. Slots allow different child components in different contexts. Hardcoding couples parent to specific children.

See Also