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.