Skip to content

Props vs Slots Decision Framework

When to Use

  • You're designing a component API
  • You need to decide if something should be a prop or slot
  • You're debugging schema validation errors

Decision

Use Props When: - Data is structured and typed (string, boolean, number, enum, object, array). - Data needs validation against JSON Schema. - Data drives component logic (variants, states, configuration). - Data is scalar or simple objects. - Example: variant: 'primary', disabled: true, size: 'large'.

Use Slots When: - Content is unstructured renderables (HTML, Drupal render arrays, nested components). - Content type cannot be known in advance. - Content implements RenderableInterface, MarkupInterface, or Stringable. - No validation needed, only an existence check. - Example: main content area, header region, action buttons area.

What "validated" actually buys you. Props are checked against the JSON Schema in development only — the check is assert()-gated (ComponentsTwigExtension.php:106) and off on a production zend.assertions=-1, and it never modifies the data. So "props are validated" means you get a loud error in dev when you pass the wrong shape, not bad data cannot reach the template. Slots get even less: ComponentNodeVisitor::validateSlots() reports slots you supplied but never declared, and never reports a declared slot you failed to supply. Pick props vs slots on the shape of the data, not on an enforcement guarantee neither one gives you.

Pattern

Pattern: Props for Configuration

Reference: /themes/contrib/radix/components/button/button.component.yml

props:
  type: object
  properties:
    variant:
      type: string
      enum: [primary, secondary, danger]
      default: primary
    size:
      type: string
      enum: [small, medium, large]
      default: medium
    disabled:
      type: boolean
      default: false

These control button appearance and behavior — perfect for props. The default: lines document intent; button.twig is what makes them happen ({% set variant = variant|default('primary') %}). Check the template before you rely on any of them.

Pattern: Slots for Content

Reference: /core/themes/olivero/components/teaser/teaser.component.yml

slots:
  content:
    title: 'Content'
    description: 'Required in practice  teaser.twig has no fallback.'
  image:
    title: 'Image'
  meta:
    title: 'Metadata'

These accept arbitrary renderable content — perfect for slots. There is no required: key for slots: the schema does not define one and no code reads one, so "required" can only be a note to the next developer plus a sensible fallback inside the {% block %}.

Pattern: Mixed Props and Slots

Most components use both props (configuration) and slots (content).

# Alert component example
props:
  type: object
  properties:
    variant:
      type: string
      enum: [success, warning, danger, info]
    dismissible:
      type: boolean
      default: false

slots:
  heading:
    title: 'Alert Heading'
  message:
    title: 'Alert Message'
    description: 'Required  alert.twig renders nothing without it.'

Common Mistakes

Common Mistake: Writing required: true on a slot. WHY: Nothing in core reads it. ComponentNodeVisitor::validateSlots() only reports undeclared slots. The component ships and renders an empty region in production with no warning. Handle the omission in the template.

Common Mistake: Using props for HTML/renderable content. WHY: Props must validate against JSON Schema. HTML/render arrays don't have predictable schemas. Use slots instead.

Common Mistake: Using slots for simple text/boolean/enum values. WHY: Slots bypass validation. Simple values should be validated props for better error messages and type safety.

Common Mistake: Applying logic to slot content in templates beyond existence checks. WHY: Slots contain arbitrary renderables. Can't reliably check their properties. Capture the block and test the rendered output ({% set x %}{% block x %}{% endblock %}{% endset %}{% if x|trim is not empty %}) — see Twig Templates in SDCs. Never test a slot-named variable around a {% block %}; it does not exist on the {% embed %} path.

See Also