logo

Palette - Make it Colorful🎨

Palette - Visual Page Builder, no design degree required.

Live Demo Download Palette

Scroll

Creating new Palette component

A practical guide to adding a Single Directory Component (SDC) to Palette: what every component must contain, and how the Style (variant) setting works. See CLAUDE.md for the architecture overview, styles.md for extending a component from another module/theme.


1. The anatomy of a component

Every component lives in its own folder under components/:

components/<id>/
  <id>.component.yml     # required — metadata + prop/slot schema
  <id>.twig              # required — the markup
  <id>.css               # optional — co-located styles
  <id>.js                # optional — co-located behaviour
  • <id> is the machine name (lowercase, underscores). Palette exposes the component as canvas_palette:<id>.
  • Core SDC auto-discovers the folder and auto-generates a library core/components.canvas_palette--<id> from the co-located .css/.js. You do not register the library yourself.

1a. <id>.component.yml — the required keys

$schema: https://git.drupalcode.org/project/drupal/-/raw/HEAD/core/assets/schemas/v1/metadata.schema.json

name: My Thing              # human label shown in the editor
group: Palette               # category in the component list — always "Palette"
status: experimental         # experimental | stable | deprecated
description: 'One line describing what it does.'

props:
  type: object
  properties:
    # … see below …

slots:
  # … optional, see 1c …

1b. Props

Each prop is a JSON-schema property. Common shapes used in this kit:

props:
  type: object
  properties:
    title:
      type: string
      title: Title
      examples: ['Default title']        # the editor placeholder / seed value

    text:
      type: string
      title: Text
      contentMediaType: text/html         # rich text …
      x-formatting-context: block         # … edited with the block CKEditor
      examples: ['<p>Some copy.</p>']

    count:
      type: integer
      title: Count
      default: 3

    image:
      $ref: json-schema-definitions://canvas.module/image
      type: object
      title: Image

Rules that matter (all enforced by Palette — get them wrong and the component silently disables itself and vanishes from the editor):

  • A required: prop MUST have an examples value. If you don't want a default, make the prop optional (leave it out of required:) rather than giving it an empty example.
  • examples[0] is the value seeded onto a newly placed instance — treat it as the editor default/placeholder.
  • default: is the runtime fallback the Twig sees when the prop is unset.
  • Enum props list a meta:enum map for human labels (see §2).
  • Optional integer props error when cleared in the editor; prefer a default: 0 that the Twig treats as "unset" instead of making it clearable.

1c. Slots (container components)

Container components (Carousel, Tiles, Stats…) hold child components in static slots, and usually pair with a dedicated *_item component:

slots:
  items:
    title: Items
    description: 'Place one My Item per row.'
    expected: ['canvas_palette:my_item']
    minItems: 1

Slots are rendered in Twig with {% block <slot> %}{% endblock %}. Palette blocks runtime-dynamic slots, so the number of slots is fixed at definition time.

1d. <id>.twig

Plain SDC Twig. Props and slots are top-level variables/blocks:

<div class="cp-my-thing cp-my-thing--{{ style|default('default') }}">
  {% if title %}<h2 class="cp-my-thing__title">{{ title }}</h2>{% endif %}
  {% if text %}<div class="cp-my-thing__body">{{ text }}</div>{% endif %}
  {% block items %}{% endblock %}
</div>

Conventions: BEM class names cp-<id>, cp-<id>__element, cp-<id>--modifier.

1e. JS that uses Drupal.behaviors / once / a third-party library

The auto-generated component library gets core/drupal injected only when the component declares libraryOverrides. So any component whose .js uses Drupal.behaviors, once(), or a shared library must declare its dependencies:

libraryOverrides:
  dependencies:
    - core/drupal
    - core/once
    - canvas_palette/glightbox   # third-party libs are attached the same way

Without this, Drupal/once are undefined at runtime. (See tabs, accordion, carousel for real examples.)

1f. Editor-only chrome

To show something only inside the Palette editor preview (grab padding, empty outlines…), gate it on the canvas_is_preview flag Palette injects into the Twig context and emit a cp-<id>--preview modifier — it never reaches published pages:

{% set classes = ['cp-my-thing', canvas_is_preview ? 'cp-my-thing--preview' : ''] %}

The JS-side equivalent is window.frameElement?.dataset?.canvasPreview === 'true'.


2. How the Style setting works

The "Style" setting is Palette's single convention for component variants. There is no per-style Twig or JS branching — it is one enum prop turned into a CSS class.

2a. The style prop

Most components declare a single enum string prop named style (labelled "Style", or sometimes "Layout" / "Columns"):

    style:
      type: string
      title: Style
      default: default
      enum:
        - default
        - colorful
      meta:enum:
        default: Default
        colorful: Colorful
      examples: [default]
  • enum — the machine values.
  • meta:enum — the human labels shown in the select.
  • default / examples — the starting value.

2b. Twig interpolates it into a BEM modifier

The Twig writes the value verbatim onto the root element as a modifier class:

<div class="cp-my-thing cp-my-thing--{{ style|default('default') }}">

So style: colorful renders class="cp-my-thing cp-my-thing--colorful".

2c. CSS keys on the modifier

Each style is just a CSS rule set keyed on .cp-<id>--<value>:

.cp-my-thing--colorful {
  background: linear-gradient(135deg, #0973f2, #7333e5);
  color: #fff;
}

That is the entire mechanism: enum value → cp-<id>--<value> class → CSS.

2d. It floats to the top of the editor form

canvas_palette_move_style_prop_first() gives any prop literally named style a very low #weight, so the Style selector always appears first in the component-inputs sidebar. This runs from canvas_palette_form_component_instance_form_alter() for every sdc.canvas_palette.* component — you get it for free just by naming the prop style. (Components with a custom inputs-form handler that re-weights props must still leave style at the top.)

2e. Adding a new Style value to an existing component

Adding an enum value is an additive, safe change:

  1. Add the value to enum: and a label to meta:enum:.
  2. Add the .cp-<id>--<value> CSS.
  3. Rebuild (see §3). The new value appears on newly placed instances and after a hard editor reload; existing instances keep their pinned version.

Shared style conventions in this kit (see CLAUDE.md for the exact token values): a colorful value and an optional eyebrow kicker prop, styled to the landing-page design system. Reuse the hard-coded token values there rather than inventing new colours.


3. Build & the versioning gotchas

After adding or editing a component, rebuild from the site root:

php vendor/drush/drush/drush.php cr

On this OSPanel stack a CLI drush cr often does not reach the browser — flush through the web (admin Configuration → Development → Performance → Clear all caches, or a throwaway web/flush.php). See CLAUDE.md → "Local environment quirk".

Canvas freezes each component's schema as a versioned config entity (sdc.canvas_palette.<id>). The consequences:

  • Editing *.component.yml does not live-update placed instances. New props / enum values show up on newly placed instances and after a hard editor reload.
  • Additive changes are safe (new enum value, new optional prop). Changing an existing prop's field type is not — it throws and disables the whole component. Revert the type to restore it.
  • If a component ends up disabled after a change, regenerate and re-enable:

    \Drupal::service('Drupal\canvas\ComponentSource\ComponentSourceManager')
      ->generateComponents();
    $c = \Drupal::entityTypeManager()->getStorage('component')
      ->loadUnchanged('sdc.canvas_palette.<id>');
    if ($c && !$c->status()) { $c->enable()->save(); }

    Verify it stays enabled across a second generate — a fix that only survives one manual re-enable is not a real fix.


Checklist for a new component

components/<id>/<id>.component.yml with $schema, name, group: Palette, status, description.

Props defined; every required prop has examples.

<id>.twig with cp-<id> BEM classes.

 A style enum prop (+ meta:enum) if the component has variants, wired to cp-<id>--{{ style }} and matching CSS.

libraryOverrides.dependencies if the JS uses Drupal.behaviors / once / a third-party library.

Slots + a paired *_item component if it's a container.

Rebuilt (through the web); component shows ENABLED across two generates.