SDC Component Format
When to Use
You are creating a Canvas-compatible component using Drupal's Single Directory Component (SDC) system. This produces a server-side-rendered Twig component with Drupal field widget integration in the Canvas editor — content editors get proper input widgets (text fields, image pickers, link pickers, rich text editors) rather than raw JSON inputs.
File Structure
An SDC component for Canvas follows standard Drupal SDC conventions, organized in a single directory:
my_theme/
components/
hero/
hero.component.yml ← required: metadata, props, slots
hero.twig ← required: Twig template (NOT .html.twig)
hero.css ← optional: component styles
hero.js ← optional: component JS behaviors
The directory can live in any Drupal module or theme under a components/ folder.
Important: Canvas automatically discovers all SDC components from all enabled modules and themes — there is no registration step. Discovery is not eligibility. Every discovered component is then run through a requirements check, and any component that fails is silently excluded: it never appears in the Canvas component panel, and nothing is logged to the page. One class of mistake is the exception to that rule and is not silent at all — a $ref naming a definition that does not exist throws out of SDC discovery and takes the whole site down. See Component Eligibility for the gate list, for both failure modes, and for where Canvas records the reason.
YAML Schema
The *.component.yml file defines the component for both Drupal's SDC system and Canvas's editor:
# hero.component.yml
$schema: 'https://git.drupalcode.org/project/drupal/-/raw/HEAD/core/assets/schemas/v1/metadata.schema.json'
name: Hero
status: stable
group: Marketing # Groups components in Canvas panel
description: 'Full-width hero with headline, subtext, CTA, and background image.'
props:
type: object
properties:
headline:
type: string
title: Headline
description: 'Main hero headline text.'
examples:
- 'Welcome to our platform'
image:
type: object
title: Background Image
$ref: 'json-schema-definitions://canvas.module/image'
cta_text:
type: string
title: 'Button Label'
examples:
- 'Get started'
cta_url:
# A link prop is a plain STRING with a `format` — NOT an object with a $ref.
# `uri-reference` accepts internal (/node/1) and external URLs;
# `uri` accepts absolute URLs only.
type: string
format: uri-reference
title: 'Button URL'
examples:
- '/contact'
body:
type: string
title: Description
contentMediaType: text/html
x-formatting-context: block
slots:
badge:
title: Badge
description: 'Optional badge component above the headline.'
Key prop types:
| Prop type | YAML definition | Canvas editor widget |
|---|---|---|
| Plain text | type: string |
Single-line text input |
| Rich text (block) | type: string + contentMediaType: text/html + x-formatting-context: block |
CKEditor block editor |
| Rich text (inline) | type: string + contentMediaType: text/html + x-formatting-context: inline |
CKEditor inline editor |
| Image | type: object + $ref: 'json-schema-definitions://canvas.module/image' |
Media Library / image picker |
| Video | type: object + $ref: 'json-schema-definitions://canvas.module/video' |
File upload (mp4) |
| Entity reference | type: object + $ref: 'json-schema-definitions://canvas.module/content-entity-reference' + x-allowed-entity-type-id |
Entity autocomplete |
| Link (any URL) | type: string + format: uri-reference |
Link field, URL only — no link-text input |
| Link (external only) | type: string + format: uri |
Link field, URL only — no link-text input |
| Number | type: integer or type: number |
Numeric input |
| Boolean | type: boolean |
Toggle |
| Enum/select | type: string + enum: [value1, value2] (+ meta:enum for labels) |
Select dropdown |
About $ref: the three URIs in the table above are a closed enum of the only type: object refs Canvas knows how to store. They are not the only $refs that exist. Canvas's schema.json ships fourteen $defs, and the string- and integer-typed ones resolve through the ordinary scalar path and produce real storable shapes — .../heading-element (string enum), .../column-width (integer enum), .../image-uri, .../stream-wrapper-uri and .../stream-wrapper-image-uri all work. So does a $ref into another extension's own schema.json, addressed as json-schema-definitions://{extension}.{module|theme}/{definition}.
What does not exist is json-schema-definitions://canvas.module/link. Writing a $ref to a definition that is not in the target extension's schema.json is fatal, not silent — see Component Eligibility.
Twig Template
Canvas-compatible Twig templates follow standard SDC conventions:
{# hero.twig #}
{% set image_attrs = create_attribute() %}
<section class="hero">
{% if badge is not empty %}
<div class="hero__badge">{{ badge }}</div>
{% endif %}
<div class="hero__content">
{% if headline %}
<h1 class="hero__headline">{{ headline }}</h1>
{% endif %}
{% if body %}
<div class="hero__body">{{ body }}</div>
{% endif %}
{% if cta_text and cta_url %}
<a href="{{ cta_url }}" class="btn btn-primary">
{{ cta_text }}
</a>
{% endif %}
</div>
{# canvas:image reads `src` at top level — SPREAD the image object into the
include context with |merge. Passing `{image: image}` throws a TypeError. #}
{% if image %}
{% include 'canvas:image' with image|merge({
loading: 'eager',
class: 'hero__image',
}|filter((value) => value is not null)) only %}
{% endif %}
</section>
Link prop structure: a link prop is a plain string — the URL. Render it directly with {{ cta_url }}. There is no .url and no .title: Canvas maps URI-format string props onto Drupal's link field with the instance setting title: 0, which switches the link widget's text field off. If you want editor-supplied link text, declare a second type: string prop for it (the cta_text above).
Image rendering: Use the canvas:image built-in SDC component for images (see SDC Image Handling).
Slot rendering: Slots arrive as renderable Twig variables — render directly with {{ slot_name }}. No {% block %} needed.
Common Mistakes
- Using
.html.twigextension instead of.twig— Canvas (and SDC) only recognize.twig - Writing
$ref: 'json-schema-definitions://canvas.module/link'— that definition has never existed, and a$refto a missing definition is fatal: the next cache rebuild throws out of SDC plugin discovery and the site returns 500. It does not quietly disable one component - Referencing image props directly with
<img src="{{ image }}">— image props are objects, not URLs; usecanvas:image - Passing an image prop as
{% include 'canvas:image' with {image: image} %}—canvas:imagetakessrc, notimage; spread withimage|merge({...}) - Assuming
$refis limited to image, video and content-entity-reference — those three are only the storabletype: objectrefs. String- and integer-typed refs such asheading-element,column-widthandstream-wrapper-uriresolve too - Forgetting
x-formatting-contexton rich text props — without it, Canvas may not show the CKEditor widget - Putting components in
templates/instead ofcomponents/— SDC components must be incomponents/ - Using
{% embed %}with{% block %}for Canvas:image — always use{% include ... only %} - Relying on YAML
default:— Canvas strips it. The stored default comes fromexamples[0]
See Also
- SDC Props Reference for all prop types in detail
- SDC Slots for slot behavior
- SDC Image Handling for image patterns
- Official SDC docs: https://www.drupal.org/docs/develop/theming-drupal/using-single-directory-components
- Canvas SDC docs: https://project.pages.drupalcode.org/canvas/sdc-components/