Skip to content
Development
Skill

/drupal-sdc-twig

Best practices for building Drupal Single Directory Components (SDC) with Twig — including props vs slots, the `attributes` object, `include` vs `embed`, escaping rules, accessibility, schema and validation, and overriding components. Invoke when the user mentions "SDC", "Single

From plugin
cms-cultivator
1726 skills1 agent
Install
$ npx -y skills add kanopi/cms-cultivator --skill drupal-sdc-twig --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/drupal-sdc-twig

Context preview

The summary Claude sees to decide when to auto-load this skill.

Best practices for building Drupal Single Directory Components (SDC) with Twig — including props vs slots, the `attributes` object, `include` vs `embed`, escaping rules, accessibility, schema and validation, and overriding components. Invoke when the user mentions "SDC", "Single

SKILL.md

drupal-sdc-twig.SKILL.md
name: drupal-sdc-twig
description: Best practices for building Drupal Single Directory Components (SDC) with Twig — including props vs slots, the `attributes` object, `include` vs `embed`, escaping rules, accessibility, schema and validation, and overriding components. Invoke when the user mentions "SDC", "Single Directory Component", "component.yml", working with `components/` folders in a Drupal theme or module, writing or reviewing an SDC Twig template, or asks "how should I structure this Drupal component", "props or slots?", "embed vs include in SDC", or "best practices for Drupal components".

Drupal SDC + Twig Best Practices

Use this skill whenever you are creating, reviewing, or refactoring a Drupal Single Directory Component, or writing the Twig template that ships inside one. SDC has been part of Drupal core since 10.3, so assume it is available unless the user explicitly states an older version.

Anatomy of an SDC

Every component lives in its own folder inside a top-level `components/` directory of a theme or module:

components/
└── card/
    ├── card.component.yml   # Metadata + schema (props, slots, libraryOverrides)
    ├── card.twig            # Template — note: .twig, NOT .html.twig
    ├── card.css             # Auto-attached as a library asset
    ├── card.js              # Auto-attached as a library asset
    └── thumbnail.png        # Optional preview image

Subdirectories are allowed (`components/molecules/card/...`). Components are referenced by namespace `theme_or_module:component_name`, e.g. `my_theme:card`.

The Ten Rules I Apply Every Time

Apply these rules when generating or reviewing SDC Twig code. They are derived from the official Drupal SDC docs and the SDC FAQ on drupal.org.

1. Use `.twig`, not `.html.twig`

SDC templates use the bare `.twig` extension. This is the one place in Drupal where you do **not** use `.html.twig`. The Twig filename must match the component machine name (the folder name).

2. Decide props vs slots before you write any Twig

This is the most important design decision in an SDC and the source of most refactors. The rule:

  • **Props** = strictly typed primitive data (strings, numbers, booleans, enums, arrays of primitives). Use props when the template needs to make a UI decision based on the value (`{% if dismissible %}`, `<h{{ heading_level }}>`).
  • **Slots** = arbitrary renderables (render arrays, nested components, HTML markup, objects implementing `Stringable` / `RenderableInterface` / `MarkupInterface`). Use slots when you only need to know "is this empty or not?" and then print it.

If you would ever want to pass a nested component, another Twig render result, or a chunk of HTML, it must be a slot — not a prop. Props that are serialized HTML strings are an anti-pattern.

3. Always declare a schema, even if there are no props

A schema (`props.type: object` with `properties`) is required for:

  • Validation during development
  • Replacing/overriding the component
  • Integration with Storybook, UI Patterns, SDC Display
  • Variants (Drupal 11.2+)

For a propless component, use the empty-props pattern:

props:
  type: object
  additionalProperties: false
  properties: {}

To enforce schemas across a theme, add `enforce_prop_schemas: true` to `theme.info.yml`.

4. Always print `attributes` on the root element

Every SDC template automatically receives an `attributes` variable (a `\Drupal\Core\Template\Attribute` object). You must use it because Drupal core, SEO modules, accessibility modules, translation, and style utilities inject classes, `lang`, `data-*`, and ARIA attributes through it.

<div{{ attributes.addClass('card') }}>
  ...
</div>

Use `.addClass()`, `.setAttribute()`, `.removeClass()` — do not stringify and concatenate. Do not redeclare `attributes` in the schema; SDC adds it automatically. (You may declare `body_attributes` etc. as `type: 'Drupal\Core\Template\Attribute'` — that's a known escape hatch for passing additional Attribute objects.)

5. `embed` for slots, `include()` for props-only

Both work, but use them deliberately:

  • **`include()` function** — clean syntax for components with props and no markup-bearing slots. Always pass `with_context = false` to avoid leaking the parent context.
  • **`embed` tag** — required when consumers need to override slots with Twig `{% block %}` markup, because slots are implemented as Twig blocks under the hood.
{# Props only — use include() #}
{{ include('my_theme:button', {
  label: 'Sign up'|t,
  variant: 'primary',
}, with_context = false) }}

{# Slots — use embed #}
{% embed 'my_theme:card' with { variant: 'feature' } only %}
  {% block media %}
    {{ include('my_theme:image', { src: node.field_image|file_url }, with_context = false) }}
  {% endblock %}
  {% block body %}
    <p>{{ node.body.summary }}</p>
  {% endblock %}
{% endembed %}

Use `only` (or `with_context = false`) by default so the child component is isolated from the parent's variables. This makes components portable and prevents surprising regressions.

6. Never pass raw HTML through a prop

Twig will escape it. If you need to pass HTML, use a slot. Two patterns:

{# Pattern A: capture markup into a variable, pass to a slot via include() #}
{% set body %}
  <p><em>Any</em> HTML stays intact.</p>
{% endset %}
{{ include('my_theme:card', { body: body }, with_context = false) }}

{# Pattern B: use embed with a block (preferred for slots) #}
{% embed 'my_theme:card' only %}
  {% block body %}<p><em>Any</em> HTML stays intact.</p>{% endblock %}
{% endembed %}

7. Render slots simply — no business logic

Inside the component template, do not branch on the *contents* of a slot — only on whether it is empty:

{% if heading %}
  <h{{ heading_level|default(2) }} class="card__heading">{{ heading }}</h{{ heading_level }}>
{% endif %}
{{ body }}

All UI logic (variant switching, conditional classes, ARIA stat

Read more
Ships withcms-cultivator

Specialist agents and auto-invoked skills for Drupal/WordPress development. Works in Claude Code, Claude Desktop, and OpenAI Codex. Full documentation: What changed in 2.0? CMS Cultivator now focuses on CMS development workflows.

Get the whole plugin

Other skills on cms-cultivator.