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
1728 skills1 agent

Claude Code

$ npx -y skills add kanopi/cms-cultivator --skill drupal-sdc-twig --agent claude-code

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
@kanopi
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.

1728 skills1 agent
Get the whole plugin

Stars on GitHub

Momentum

stars in total
17
Star history of kanopi/cms-cultivator on GitHub, a chart by star-history.com

Other skills on cms-cultivator.

browser-validator

browser-validator

Automatically validate implementations in real browsers after code is written or when user says "test this", "test what you built…

coverage-analyzer

coverage-analyzer

Automatically analyze test coverage when user asks which code is tested, mentions coverage gaps, or shows code asking about…

ddev-docker-cleanup

ddev-docker-cleanup

Deterministic cleanup of DDEV and Docker disk usage on OrbStack, Docker Desktop, or any Docker provider. Safely reclaims space by…

More skills from other plugins.

brainstorming
Auto-invoked

brainstorming

You MUST use this before any creative work - creating features, building components, adding functionality, modifying behavior, or…

@obra@obraInstall Skill
ask-matt

ask-matt

Ask which skill or flow fits your situation. A router over the skills in this repo.

skills10d309k
@mattpocock@mattpocockInstall Skill
accessibility
Auto-invoked

accessibility

Design, implement, and audit accessible UI to WCAG 2.2 Level AA across Web, iOS, and Android — semantic ARIA roles and labels,…

ecc11d293k
@affaan-m@affaan-mInstall Skill
compiler-commit

compiler-commit

Use when you want to verify compiler changes and commit with the correct convention. Runs tests, lint, and format, then commits…

react1d252k
@facebook@facebookInstall Skill

Questions about drupal-sdc-twig.

Which coding agents can use drupal-sdc-twig?
drupal-sdc-twig installs in Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, OpenCode and Windsurf with the skills CLI, using each agent's flag shown on this page. Flowy's auto-invocation, which fires a skill when your prompt matches, runs in Claude Code only.