Skip to content

Single Directory Components (SDC)

I need to... Guide Summary
Understand SDC architecture and plugin system SDC Architecture Discovery recursively scans components/ directories in the active theme, base themes, and modules with no precedence between them — the component ID is provider:{basename of the .component.yml}. Precedence applies only to replacement: ComponentNegotiator picks a winner among candidates that declare replaces, active theme before base themes before module fallback.
Know component file structure and naming Component File Structure Every sibling file (.twig, .css, .js) must match the .component.yml basename, not the directory name — machineName is derived from the plugin ID, which is the YAML basename. Renaming the directory does not fix a 'component not found' error; check the basename match instead.
Define component YAML schema properly Component YAML Schema Core validates props against the schema but never strips undeclared ones and never applies YAML default: — the Twig's ?? / |default() is the only real default. There is no libraryDependencies key; use libraryOverrides: dependencies:. Read the .twig, not the YAML, to learn a component's real API.
Write Twig templates for SDCs Twig Templates in SDCs A slot arrives differently on each call path — {% embed %} gives a block override, include() gives a context variable, #type: component gives both. Default to rendering every slot through {% block %} with the fallback inside it; testing a slot-named variable around a {% block %} breaks the {% embed %} path with no error.
Add SCSS/CSS to components with proper scoping SCSS/CSS in SDCs Use BEM to scope component CSS and prevent collisions; prefer CSS custom properties for theming values that variants override. Never use @extend or !important — fix selector specificity or use mixins/utility classes instead.
Add JavaScript with Drupal.behaviors JavaScript in SDCs Attach behavior with Drupal.behaviors + once() scoped to context, never document.querySelectorAll, and implement detach for cleanup. Declare JS dependencies via libraryOverrides: dependencies: — there is no libraryDependencies key.
Compose components (include/embed/render) Component Composition Use include() for props-only inclusion (with with_context = false); use embed only when the child renders that slot through {% block name %} — if it prints a bare {{ name }} or wraps it in {% if %}, your block override is silently discarded. Render arrays are for preprocessing/controllers/hooks.
Decide between props and slots Props vs Slots Decision Framework Use props for structured, typed, validated data that drives logic; use slots for unstructured renderable content. Prop validation is a dev-time, assert()-gated lint that never mutates data, and slot required: is not a real key — pick the shape based on the data, not on an enforcement guarantee neither one gives you.
Create component variants (enum vs separate) Component Variants The Component Variants API landed in Drupal 11.2, not 11.1 — variants: is silently ignored on 11.1 and earlier. #variant only copies into $props['variant'] and adds a data-component-variant attribute; it never declares or restricts a variant prop, so declare variant as an enum prop too if you want it validated.
Replace Drupal templates with SDCs Replacing Templates with SDCs replaces is not themes-only — modules can replace components too, with a theme in the active hierarchy winning over a module fallback. The replacement schema must be compatible (required props match, shared props' type/enum lists are supersets), not identical; a narrowing replacement throws IncompatibleComponentSchema at cache rebuild, in production too.
Test SDCs (Storybook, visual regression) Testing SDCs The theme key is enforce_prop_schemas, not enforce_sdc_schemas — a misspelling turns nothing on and reports nothing. Production never validates props (the check is assert()-gated), so development with zend.assertions=1 is the only place a schema violation surfaces; test each slot call path (embed, include, render element) individually since the render element hides embed-only bugs.
Optimize performance (libraries, caching) Performance Libraries auto-generate per component and load only when the component renders; libraryOverrides is the only key core reads for dependency ordering (core/drupal is appended automatically). A js: or css: key in libraryOverrides replaces the auto-discovered entry rather than adding to it.
Prevent XSS and security issues Security Prop validation is assert()-gated, non-mutating, and skips undeclared props entirely — pattern/format/enum on a prop never stands between user input and markup. Sanitize at the boundary (UrlHelper::stripDangerousProtocols, Xss::filter) and rely on Twig auto-escaping plus the Attribute object, not on schema validation.
Avoid anti-patterns and code review issues Anti-Patterns The two highest-impact anti-patterns: wrapping {% block name %} in {% if name %} silently drops content on the embed path with no error, and narrowing a replacement's schema throws IncompatibleComponentSchema at cache rebuild in production. Both trace back to THE MECHANISM — the YAML declares, the Twig (and SchemaCompatibilityChecker) decide.
Integrate SDC with UI Patterns 2.x UI Patterns 2 Integration UI Patterns 2 reads component.yml directly — no separate pattern file needed — and exposes SDCs as blocks/layouts/views plugins via sub-modules. It is the one consumer that reads YAML default:, so keep default: in step with the Twig's ?? / |default(); if they disagree, UI-configured and Twig-called instances render differently.