The Decision That
Defines Everything
Properties vs. Slots: Where Does Control Live?
Every web component in this system must answer one fundamental question: does the component control the rendering, or does Drupal? This architectural decision shapes every integration pattern, every TWIG template, and every content editor's experience.
Three Strategies, One Goal
Every component sits somewhere on this spectrum. Understanding where helps you choose the right approach for each use case.
See the Difference in Code
The same card component, built two ways. See how each approach impacts both your TWIG templates and your Storybook stories.
{# Component controls ALL rendering #} <wc-card title="{{ content.field_title }}" description="{{ content.field_body }}" image-src="{{ file_url(content.field_image) }}" image-alt="{{ content.field_image.alt }}" href="{{ url }}" variant="elevated" ></wc-card>
// Perfect in Storybook - all props visible export const Default = { args: { title: 'Article Title', description: 'Summary text...', imageSrc: '/placeholder.jpg', variant: 'elevated', }, };
{# Drupal controls ALL content #} <wc-card variant="elevated"> <img slot="media" src="{{ file_url(content.field_image) }}" alt="{{ content.field_image.alt }}" /> <h3 slot="heading"> {{ content.field_title }} </h3> <div slot="body"> {{ content.field_body }} </div> <a slot="actions" href="{{ url }}"> Read More </a> </wc-card>
// Slots need HTML strings in Storybook export const Default = { args: { variant: 'elevated', }, render: (args) => ` <wc-card variant="${args.variant}"> <img slot="media" ... /> <h3 slot="heading">...</h3> <div slot="body">...</div> </wc-card>`, };
Drupal Module Compatibility
Not every Drupal module works equally well with every strategy. Here is how each approach maps to the modules you use daily.
Right Tool for Each Level
The component's complexity determines the right strategy. Simple atoms use properties. Complex organisms use slots. Forms use both.
Small, self-contained components with a fixed API. All rendering logic lives in the component.
- Button
- Badge
- Toggle
- Tooltip
- Spinner
- Avatar
Complex, content-rich components. Drupal editors need full control over what appears inside.
- Card
- Hero
- Modal
- Navigation
- Data Table
- Accordion
Properties control validation, state, and behavior. Slots allow custom labels, help text, and error messages.
- Text Input
- Select
- Checkbox
- Radio
- Date Picker
- File Upload
Our Architecture Decision
Slot-First, Property-Enhanced
Default to slots for content flexibility. Use properties for behavior, configuration, and state management. This approach maximizes Drupal's strengths while keeping components powerful and testable.
Content editors already know how to use Drupal's field system, media library, and WYSIWYG. Slots let them keep using these tools without learning component APIs.
Use properties for things content editors should not control: variant styles, validation rules, accessibility states, animation settings, and component configuration.
Healthcare organizations need robust content governance. Slot-driven components give editorial teams control while maintaining WCAG AAA accessibility standards.
How Components
Reach the Browser
Bundle Strategy: One File, Many Files, or Smart Groups?
The second critical architecture decision: how do web components get delivered to each page? A single monolithic bundle, individual per-component files, or intelligent context-based groups? The right strategy means fast first paint and zero wasted bytes.
How Components Reach the Browser
The architecture decision does not stop at props vs. slots. How you physically load web component JavaScript into Drupal pages has a direct impact on performance, cacheability, and maintainability.
# Single bundle - every component in one file wc_2026: js: dist/wc-2026.bundle.js: { minified: true } dependencies: - core/drupal - core/once
# Per-component - surgical loading wc_2026/card: js: dist/components/wc-card.js: { minified: true } dependencies: - wc_2026/lit-runtime wc_2026/button: js: dist/components/wc-button.js: { minified: true } dependencies: - wc_2026/lit-runtime wc_2026/accordion: js: dist/components/wc-accordion.js: { minified: true } dependencies: - wc_2026/lit-runtime
# Smart bundles - grouped by usage context wc_2026/core: js: dist/groups/core.js: { minified: true } # button, badge, spinner, avatar (~32KB) dependencies: - wc_2026/lit-runtime wc_2026/navigation: js: dist/groups/navigation.js: { minified: true } # nav, breadcrumb, tabs, sidebar (~28KB) dependencies: - wc_2026/core wc_2026/content: js: dist/groups/content.js: { minified: true } # card, hero, accordion, modal (~45KB) dependencies: - wc_2026/core wc_2026/forms: js: dist/groups/forms.js: { minified: true } # text-input, select, checkbox, radio (~38KB) dependencies: - wc_2026/core
{# In paragraph--card.html.twig #} {{ attach_library('wc_2026/card') }} <wc-card variant="elevated"> <img slot="media" src="{{ image_url }}" /> <h3 slot="heading">{{ title }}</h3> <div slot="body">{{ body }}</div> </wc-card> {# Drupal only loads wc-card.js + lit-runtime.js #} {# Total: ~18KB for this page (not 220KB!) #}
# Shared Lit runtime - loaded once, cached forever wc_2026/lit-runtime: js: dist/vendor/lit-core.js: { minified: true } dependencies: - core/drupal - core/once # ~15KB gzipped - Lit 3 runtime # Cached across ALL pages after first load
Real-World Patterns
Three components, three strategies. Here is how each approach looks in production TWIG templates.
Drupal renders content into named slots. The component provides layout, elevation, and interaction patterns.
<wc-card variant="elevated" interactive> <img slot="media" src="{{ file_url(image.uri) }}" alt="{{ image.alt }}" loading="lazy" /> <h3 slot="heading"> {{ title }} </h3> <p slot="body"> {{ body|striptags|truncate(120) }} </p> <a slot="actions" href="{{ url }}"> Read More → </a> </wc-card>
All configuration via properties. The component handles all rendering, states, and accessibility.
<wc-button variant="primary" size="large" icon="arrow-right" icon-position="end" {% if is_disabled %} disabled {% endif %} > {{ button_label }} </wc-button> {# Loading state example #} <wc-button variant="primary" loading aria-busy="true" > Submitting... </wc-button>
Properties handle validation and state. Slots allow custom labels, descriptions, and error messages.
<wc-text-input name="{{ field_name }}" type="{{ field_type }}" required pattern="{{ validation_pattern }}" maxlength="{{ max_length }}" > <!-- Slots for custom content --> <span slot="label"> {{ field_label }} {% if required %} <abbr title="required">*</abbr> {% endif %} </span> <span slot="help"> {{ field_description }} </span> <span slot="error"> {{ error_message }} </span> </wc-text-input>