Skip to content

Replacing Templates with SDCs

When to Use

  • You're migrating existing Twig templates to SDC
  • You need to override contrib module/theme components
  • You're implementing field formatters with components

Decision

Themes and modules can both use replaces. The rule is precedence, not prohibition: ComponentNegotiator::doNegotiate() collects every definition whose replaces equals the requested ID, then maybeNegotiateByTheme() picks the winner from the active theme hierarchy (active theme first, then base themes by order); if no theme in that hierarchy claims it, maybeNegotiateByModule() returns a module candidate. Core ships a fixture module that does exactly this: core/modules/system/tests/modules/sdc_test_replacements/ (type: module) replaces sdc_test:my-button.

The replacement must be schema-compatible, not identical. SchemaCompatibilityChecker::isCompatible() runs two checks and no others: 1. the two components' required prop sets must match; 2. for props present in both schemas, the replacement's type list and enum list must be supersets of the original's.

Adding a new optional prop, accepting an extra type, or accepting an extra enum value all pass. Dropping a prop the original declared is ignored entirely — only shared props are compared.

One hard precondition: both components must declare a props schema. ComponentPluginManager::alterDefinitions() (:282-291) errors with "component replacement requires both components to have schema definitions" if either side has none.

WHY compatibility rather than equality: calling code written against the original must keep working. Anything that only adds to the accepted input satisfies that; anything that narrows it does not.

Unlike prop validation, this check is not assert()-gatedalterDefinitions() throws IncompatibleComponentSchema outright (:307-309), in production too, during the cache rebuild that discovers the definitions. An incompatible replacement takes the site down at drush cr, not at render time.

One quiet exception: if the component named in replaces does not exist at all, the candidate is filtered out (:276-279) and no error is raised. A typo'd replaces target is silently ignored.

Pattern

Pattern: Override with replaces

# themes/my_theme/components/enhanced-button/enhanced-button.component.yml
$schema: https://git.drupalcode.org/project/drupal/-/raw/HEAD/core/assets/schemas/v1/metadata.schema.json
name: 'Enhanced Button'
replaces: 'radix:button'

props:
  type: object
  required:
    # must be the same set as radix:button's required list
    - text
  properties:
    text:
      type: string
    variant:
      # widening is allowed: original enum ⊆ this enum
      type: string
      enum: [primary, secondary, danger, ghost]
    icon:
      # a brand-new optional prop is allowed
      type: string

Pattern: Custom Field Formatter

Reference: Field formatter integration patterns

// Custom field formatter using components
class ComponentFieldFormatter extends FormatterBase {

  public function viewElements(FieldItemListInterface $items, $langcode) {
    $elements = [];

    foreach ($items as $delta => $item) {
      $elements[$delta] = [
        '#type' => 'component',
        '#component' => 'my_theme:field-card',
        '#props' => [
          'title' => $item->title,
          'variant' => $this->getSetting('variant'),
        ],
        '#slots' => [
          'content' => [
            '#markup' => $item->value,
          ],
        ],
      ];
    }

    return $elements;
  }
}

Pattern: Migration Path from Traditional Templates

  1. Create SDC with equivalent structure
  2. Update calling templates to use include('provider:component')
  3. Test in development with schema validation enabled
  4. For complete replacement, use the replaces directive (from a theme or a module)

Common Mistakes

Common Mistake: Expecting a same-named component in your theme to take over a module's. WHY: Components are namespaced by provider. my_theme:card and my_module:card are separate plugins and both stay live. Replacement happens only through an explicit replaces key.

Common Mistake: Assuming a module cannot replace a component. WHY: It can. A theme candidate in the active theme hierarchy wins when there is one; the module candidate is the fallback, not a forbidden case.

Common Mistake: Narrowing the schema when using replaces. WHY: Removing a type, dropping an enum value, or changing the required-prop set breaks callers written against the original and throws at cache rebuild. Widening is fine — extra optional props, extra accepted types, extra enum values all pass.

See Also

  • Reference: /core/lib/Drupal/Core/Theme/Component/SchemaCompatibilityChecker.php:35-51 — the entire compatibility contract
  • Reference: /core/lib/Drupal/Core/Theme/ComponentNegotiator.php:72-140 — theme-then-module precedence
  • Reference: /core/modules/system/tests/modules/sdc_test_replacements/ — a module that replaces a component
  • SDC Architecture
  • Component YAML Schema
  • UI Patterns Module — Component integration