Single-directory components
A single-directory component (SDC) packages one piece of UI, its template, styles, behavior, and a typed interface, into a single folder that any module or theme can ship. This page covers the full component format and the ways to render, extend, and replace components.
Define components with validated props and slots, use them from Twig templates and from PHP render arrays, and swap a module’s component for your theme’s own version. If you have never built a component, the card component tutorial part is the gentler entry point; this page is the complete reference for the moving parts.
Components are core functionality, implemented by \Drupal\Core\Theme\ComponentPluginManager. There is nothing to install: the sdc module that existed as an experimental container for this feature is obsolete and hidden in Drupal 11, so ignore any instructions that tell you to enable it.
Prerequisites
Section titled “Prerequisites”- A custom theme or module to hold the component, for example one built in the first theme tutorial.
- Comfort with Twig in Drupal, since component templates are Twig.
Lay out the component directory
Section titled “Lay out the component directory”Drupal discovers components in any components/ directory inside an installed module or theme, one subdirectory per component. The directory name, the *.component.yml file, and the Twig file must share the component’s machine name:
Directoryfirst_theme/
Directorycomponents/
Directorycard/
- card.component.yml the definition and schema
- card.twig the template
- card.css attached automatically when the component renders
- card.js attached automatically when the component renders
- README.md optional documentation
The component’s ID is provider:machine_name, here first_theme:card. CSS and JS files named after the component become an auto-generated asset library that Drupal attaches whenever the component is rendered; you never write a libraries.yml entry for them.
Define props and slots in the component file
Section titled “Define props and slots in the component file”# This $schema line gives IDEs autocomplete for this file's syntax.$schema: https://git.drupalcode.org/project/drupal/-/raw/HEAD/core/assets/schemas/v1/metadata.schema.jsonname: Cardstatus: stabledescription: 'A content card with an optional collapsible body.'
# Props are typed values, validated against JSON Schema.props: type: object properties: attributes: type: Drupal\Core\Template\Attribute title: Attributes description: Wrapper attributes. heading_level: type: integer title: 'Heading level' description: 'Which <h*> element to use for the card heading.' enum: [2, 3, 4] default: 2 collapsible: type: boolean title: Collapsible description: 'Let visitors collapse the card body (uses the card behavior).' default: false
# Slots hold arbitrary markup. In card.twig each slot is a {% block %}.slots: heading: title: Heading required: true description: 'The card heading text or link.' content: title: Content required: true description: 'The card body.' footer: title: Footer description: 'Optional footer, for metadata or links.'The two kinds of inputs serve different jobs:
- Props are typed values: strings, numbers, booleans, enums, also objects such as
Drupal\Core\Template\Attribute. They are declared as a JSON Schema object underpropsand become plain Twig variables in the template. - Slots are regions for arbitrary markup, declared under
slots. They carry no schema beyond a title, an optional description, and whether they are required.
Use a prop when the component needs to make a decision with the value (a heading level, a variant name, a flag). Use a slot when the caller should control the markup (body content, a footer).
Schema enforcement depends on who provides the component, as implemented in \Drupal\Core\Theme\Component\ComponentMetadata:
- Components in modules must define a
propsschema. A module component without one fails discovery with an exception. - Components in themes may omit schemas, unless the theme opts in by setting
enforce_prop_schemas: truein its*.info.yml. Define schemas anyway: they enable validation, replacement, and IDE autocompletion through the$schemaline.
\Drupal\Core\Theme\Component\ComponentValidator checks every render against the schema through a PHP assertion. With assertions enabled (zend.assertions = 1, the default in development environments), a wrong prop type throws an exception listing each violation; in production, where assertions are compiled out, validation costs nothing.
Write the component template
Section titled “Write the component template”{#/** * @file * The card single-directory component. * * card.css (and any card.js) next to this file is attached automatically * whenever the component is rendered, with no libraries.yml entry needed. * * Render it from any template: * {% include 'first_theme:card' with { heading: label, content: body } %} * or fill the slots with {% embed %} blocks (see node--teaser.html.twig). */#}{% set heading_level = heading_level ?? 2 %}<article{{ attributes.addClass('card', collapsible ? 'card--collapsible') }}> <h{{ heading_level }} class="card__heading"> {% block heading %}{{ heading }}{% endblock %} </h{{ heading_level }}> <div class="card__content"> {% block content %}{{ content }}{% endblock %} </div> {% block footer %}{{ footer }}{% endblock %}</article>Props arrive as regular variables (heading_level, collapsible, attributes). Each slot is a {% block %} that prints the variable of the same name by default, so callers can pass a slot value as a plain variable or fill the block with {% embed %}.
Embed the component from Twig
Section titled “Embed the component from Twig”For a component whose slots only need simple values, pass everything through include, slots included:
{% include 'first_theme:card' with { heading_level: 3, heading: label, content: content,} only %}When a slot needs real markup, switch to embed and fill the blocks. This teaser template renders each node as a card:
{#/** * @file * Teaser display of a node, rendered with the card component. * * This file demonstrates template suggestions: node--teaser.html.twig is * more specific than node.html.twig, so Drupal uses it whenever a node is * rendered in the "teaser" view mode. Enable Twig debug mode to see every * suggestion as an HTML comment in the page source. * * It fills the slots of the card single-directory component defined in * components/card/. */#}{# With "only", the blocks below see just the variables listed here. #}{% embed 'first_theme:card' with { attributes, heading_level: 3, label, url, content, title_prefix, title_suffix,} only %} {% block heading %} <a href="{{ url }}" rel="bookmark">{{ label }}</a> {% endblock %} {% block content %} {# Modules print contextual links and similar output here. #} {{ title_prefix }} {{ title_suffix }} {{ content }} {% endblock %}{% endembed %}Use only to keep the surrounding template’s context from leaking into the component; components should receive everything they use explicitly. With embed, that includes the variables your blocks print (label, url, content above), because the blocks render inside the component’s context.
Render the component from PHP
Section titled “Render the component from PHP”Modules render components with the component render element provided by \Drupal\Core\Render\Element\ComponentElement. Props take scalar values; slots take render arrays:
/** * Implements hook_ENTITY_TYPE_view() for nodes. * * Renders a single-directory component from PHP with the "component" * render element. Props are scalar values validated against the * component's schema; slots take render arrays. */#[Hook('node_view')]public function nodeView(array &$build, NodeInterface $node, EntityViewDisplayInterface $display, string $view_mode): void { if ($view_mode !== 'full') { return; } $build['feedback_card'] = [ '#type' => 'component', '#component' => 'first_theme:card', '#props' => [ 'heading_level' => 3, 'collapsible' => TRUE, ], '#slots' => [ 'heading' => ['#plain_text' => $this->t('Was this page helpful?')], 'content' => [ '#markup' => '<p>' . $this->t('Open an issue if something is wrong or missing.') . '</p>', ], ], '#weight' => 100, ];}This is the bridge between Drupal’s render pipeline and components: blocks, controllers, and hooks build the same #type => component structure, and caching, lazy building, and asset attachment behave like any other render array.
Replace another extension’s component
Section titled “Replace another extension’s component”Components are deliberately closed to alteration: there is no alter hook for a component’s definition, and you cannot override only its CSS file from a theme. The supported tool is replacement. To make your theme’s version of some_module:button win whenever your theme is active:
- Copy the component directory into your theme’s
components/folder. Keep or change the directory name; the replacement is linked by metadata, not by name. - Add
replaces: 'some_module:button'at the top level of your component’s*.component.yml. - Keep the prop schema compatible. Both components must define schemas, and
\Drupal\Core\Theme\Component\SchemaCompatibilityCheckerrejects a replacement whose schema could break existing callers. - Rebuild caches with
ddev drush cache:rebuild(ddev drush cr).
At render time \Drupal\Core\Theme\ComponentNegotiator picks the replacement provided by the active theme (or its base themes), so different themes can ship different replacements of the same component, and code keeps requesting the original ID.
Add dependencies and extra assets
Section titled “Add dependencies and extra assets”The auto-generated library covers one CSS and one JS file named after the component. For anything more, declare a libraryOverrides key in the component file. Paths are relative to the component directory, and the normal library keys apply:
libraryOverrides: dependencies: - core/drupal - core/onceDeclare the dependencies your JS needs: a component’s auto-library does not depend on core/drupal or core/once by itself, so a card.js that registers a JavaScript behavior works reliably only when the component declares those dependencies through libraryOverrides. (When libraryOverrides is present, Drupal adds core/drupal for you, but being explicit costs nothing and documents intent.)
Verify
Section titled “Verify”- Run
ddev drush cache:rebuild, then render a page that uses the component (for the card: any page listing article teasers). - The component’s markup appears with a
data-component-id="first_theme:card"attribute, and with Twig debug mode on,<!-- … Component start: first_theme:card -->comments mark each render. - The component’s CSS and JS load with the page; check the network panel or page source for the file (or for the aggregate containing it).
- With PHP assertions enabled, feed a prop the wrong type, for example
heading_level: 'huge', and confirm the render fails with a schema validation error naming the prop. Remove the bad value afterward.
Variations
Section titled “Variations”Component variants
Section titled “Component variants”A component can declare named variants in its definition file, each with a label. The caller picks one with variant in include/embed context or #variant in a render array; the value reaches the template as the variant prop. Use variants when one component has a small set of designed appearances, and separate components when the markup diverges.
Hide a component from UIs
Section titled “Hide a component from UIs”Set noUi: true in the component file for internal building blocks that visual tools (such as page builders that list components) should not offer to users. Code can still render them.
Related
Section titled “Related”- Build your first theme 4: a card component builds the card used on this page step by step.
- Asset libraries explains the library system behind component assets.
- Twig in Drupal covers the template syntax components rely on.
The Component plugin classis the runtime object behind every component definition.
Was this page helpful?
Drupal is a registered trademark of Dries Buytaert.