Claude
Skills
Sign in
Back

drupal-canvas-sdc

Included with Lifetime
$97 forever

Drupal Canvas SDC (Single Directory Components) with Twig templates. Use when creating, modifying, or troubleshooting Twig-based Canvas components in themes/modules, component.yml schemas, Canvas preview issues, or page builder functionality. For React/JSX Code Components, see drupal-canvas-code-components skill. (project)

Design

What this skill does


# Drupal Canvas SDC Components (Twig)

## Overview

**Drupal Canvas 1.0** (released December 2025) is a React-based visual page builder powered by Single Directory Components (SDC). Canvas consolidates three component types:
- **SDC components** from themes/modules
- **Code Components** written in-browser with React/JSX
- **Traditional Drupal Blocks**

Canvas becomes the default page builder in Drupal CMS 2.0 (January 2026).

## Installation

```bash
composer require 'drupal/canvas:^1.0'
drush en canvas -y
```

**Requirements**: Drupal ^11.2

## Component Discovery

SDC components live in a `components/` directory within any theme or module. Drupal scans enabled themes/modules for directories containing a `.component.yml` file.

**Component ID pattern**: `{provider}:{component-name}` (e.g., `mytheme:hero-banner`)

**Management UI**: Appearance > Components (`/admin/appearance/component`)

## SDC File Structure

Every SDC requires exactly two files. CSS/JS with matching names auto-attach.

```
theme_name/
└── components/
    ├── atoms/
    │   └── button/
    │       ├── button.component.yml   # REQUIRED
    │       ├── button.twig            # REQUIRED
    │       ├── button.css             # Auto-attached
    │       └── button.js              # Auto-attached
    └── molecules/
        └── card/
            ├── card.component.yml
            ├── card.twig
            ├── card.css
            └── thumbnail.png          # Optional preview
```

**Naming Rules**:
- Use **kebab-case** for directories and files
- All files share the same base name as the directory
- Templates use `.twig` extension (NOT `.html.twig`)
- Organizational folders (`atoms/`, `molecules/`) don't affect component IDs

## component.yml Schema

```yaml
$schema: https://git.drupalcode.org/project/drupal/-/raw/HEAD/core/assets/schemas/v1/metadata.schema.json
name: Hero Banner
description: Full-width hero with image background and CTA
status: stable  # experimental, stable, deprecated, obsolete
group: Content

props:
  type: object
  required:
    - heading
  properties:
    heading:
      type: string
      title: Heading
      examples: ['Welcome to our site']  # REQUIRED for Canvas preview
    background_image:
      type: object
      $ref: json-schema-definitions://canvas.module/image
    cta_text:
      type: string
      title: Button Text
      default: Learn More
    size:
      type: string
      title: Size
      enum: ['small', 'medium', 'large']
      default: medium
      meta:enum:
        small: "Compact"
        medium: "Standard"
        large: "Full Screen"

slots:
  content:
    title: Body Content
    description: Main content area below the heading

libraryOverrides:
  dependencies:
    - core/drupal
    - core/once
```

**CRITICAL**: The `examples` key is **required for Canvas** to display helpful placeholders.

## Prop Types and Canvas Widgets

| Schema Definition | Canvas Widget | Use Case |
|-------------------|---------------|----------|
| `type: string` | Text input | Simple text |
| `type: string` + `contentMediaType: text/html` | CKEditor 5 | Rich text |
| `type: boolean` | Checkbox | Toggle options |
| `type: integer` with `minimum`/`maximum` | Number input | Numeric values |
| `type: string` + `enum: [...]` | Dropdown | Preset options |
| `$ref: json-schema-definitions://canvas.module/image` | Media library | Image picker |
| `type: array` with `items:` | List input | Multiple values |

### Rich Text Props

```yaml
content:
  type: string
  title: Rich Text Content
  contentMediaType: text/html
  x-formatting-context: block  # or 'inline'
  examples: ['<p>Formatted <strong>content</strong></p>']
```

### Enum Labels (Human-Readable Dropdowns)

```yaml
color:
  type: string
  enum: ['primary', 'secondary', 'danger']
  meta:enum:
    primary: "Primary Brand Color"
    secondary: "Secondary Accent"
    danger: "Warning/Error State"
```

## Validation Patterns

### Required Properties

```yaml
props:
  type: object
  required:
    - title
    - url
  properties:
    title:
      type: string
```

### Numeric Constraints

```yaml
spacing:
  type: integer
  minimum: 0
  maximum: 100

heading_level:
  type: integer
  enum: [2, 3, 4, 5, 6]
```

### Nullable Values

```yaml
subtitle:
  type: ['string', 'null']
  title: Optional Subtitle
```

### Array Constraints

```yaml
tags:
  type: array
  items:
    type: string
  minItems: 1
  maxItems: 5
```

### Default Values in Twig

**IMPORTANT**: Schema `default` values are documentation only. Always use `|default()` in Twig:

```twig
{% set heading_level = heading_level|default(2) %}
{% set color = color|default('primary') %}
```

## Slots for Composition

Slots accept arbitrary markup or nested components. Props handle typed data; slots handle content.

### Define Slots

```yaml
slots:
  header:
    title: Header Content
    description: Optional header area
  body:
    title: Body Content
  footer: {}  # Minimal definition
```

### Render Slots in Twig

```twig
<article class="card">
  {% if header %}
    <header class="card__header">{{ header }}</header>
  {% endif %}
  <div class="card__body">{{ body }}</div>
  {% if footer %}
    <footer class="card__footer">{{ footer }}</footer>
  {% endif %}
</article>
```

### Populate Slots with Embed

```twig
{% embed 'my_theme:card' with { heading: 'Featured Article' } only %}
  {% block body %}
    {{ content.field_image }}
    <p>{{ content.field_summary }}</p>
    {{ include('my_theme:button', { text: 'Read More', url: url }) }}
  {% endblock %}
{% endembed %}
```

### Populate Slots from PHP

```php
$build = [
  '#type' => 'component',
  '#component' => 'my_theme:card',
  '#props' => ['heading' => $title],
  '#slots' => [
    'body' => ['#markup' => $content],
    'footer' => $footer_render_array,
  ],
];
```

## Atomic Design Principles

- **Atoms**: Buttons, icons, labels, form inputs
- **Molecules**: Cards, media objects, search forms
- **Organisms**: Headers, footers, article teasers
- **Templates**: Page layouts with slots for organisms

### Single Responsibility Pattern

```yaml
# ✅ Good: Single component with variants
name: Button
props:
  type: object
  properties:
    variant:
      type: string
      enum: ['primary', 'secondary', 'outline', 'ghost']
    size:
      type: string
      enum: ['small', 'medium', 'large']
```

```yaml
# ❌ Avoid: Separate components per variant
# button-primary.component.yml
# button-secondary.component.yml
```

## CSS Styling with BEM

**Component-Scoped CSS (CRITICAL)**: NEVER scope styles to route/path body classes (e.g., `.alias--masterclasses`, `.path-some-page`). Instead, scope styles to the component's own classes (e.g., `.view-masterclasses`, `.block-views-blockmasterclasses-block-2`). The Canvas editor renders page previews in an iframe without the page's body classes, so route-scoped styles won't appear there. All styles must be componentized and portable -- they should look correct regardless of what route or context they render in.

SDC auto-attaches CSS when file matches component name. Use BEM for scoped styles:

```css
/* card.css */
.card {
  --card-padding: 1.5rem;
  --card-radius: 0.5rem;
  --card-shadow: 0 2px 8px rgba(0,0,0,0.1);

  display: flex;
  flex-direction: column;
  padding: var(--card-padding);
  border-radius: var(--card-radius);
  box-shadow: var(--card-shadow);
}

.card__header {
  margin-bottom: 1rem;
  font-weight: 700;
}

.card__body {
  flex: 1;
}

.card--featured {
  --card-shadow: 0 4px 16px rgba(0,0,0,0.15);
  border: 2px solid var(--color-primary);
}

.card--compact {
  --card-padding: 1rem;
}
```

### External Dependencies

```yaml
libraryOverrides:
  css:
    component:
      card.css: {}
      additional-styles.css: {}
  dependencies:
    - core/normalize
```

## JavaScript Behaviors

Use Drupal's behavior pattern with `once()`:

```javascript
// accordion.js
((Drupal, once) => {
  Drupal.behaviors.accordion = {
    attach(context) {
      once('accordion', '.accordion__trigger', con

Related in Design