Sdc component best practices
3.5 SDC Component Best Practices
When to Use This Section
- You're deciding component granularity (too small vs too large)
- You need guidance on props vs slots architecture
- You're questioning whether a Bootstrap class is sufficient vs creating an SDC
- You want senior themer guidance on component design decisions
Component Granularity Decision Framework
Pattern: When Component is Too Small
DON'T CREATE SDC FOR:
# BAD: Wrapper component
name: Flex Container
props:
gap:
type: string
enum: ['1', '2', '3']
WHY: Bootstrap already provides .d-flex .gap-2. Creating an SDC adds indirection without value.
INSTEAD USE: Bootstrap utility classes directly in templates:
<div class="d-flex gap-2 align-items-center">
{# Content #}
</div>
DON'T CREATE SDC FOR:
# BAD: Spacing component
name: Spacer
props:
size:
type: string
WHY: This is literally what Bootstrap spacing utilities (.mb-3, .py-4) are for.
RULE OF THUMB: If the component is just applying 1-3 Bootstrap classes without custom logic or markup structure, use Bootstrap classes directly.
Pattern: When Component is Too Large
SPLIT WHEN: - Component has 200+ lines of Twig - Component needs different props for different "sections" - Component is reused but you only need parts of it sometimes
EXAMPLE: Hero section too large
# BAD: Monolithic hero
name: Hero Section
props:
# 30+ props for different hero variants
# Background image, video background, carousel, parallax...
WHY: Different page types need different hero patterns. Monolithic component has unused props/slots and complex conditional logic.
INSTEAD: Split into focused components:
- hero-simple/ — Image background + heading + CTA
- hero-video/ — Video background variant
- hero-carousel/ — Carousel-based hero
Pattern: Component Granularity Sweet Spot
CREATE SDC WHEN: 1. Reused across multiple templates — Used in 3+ places 2. Has configurable variants — Not just one fixed layout 3. Contains custom markup structure — Not achievable with utility classes alone 4. Represents a design system atom/molecule — Part of your design language
EXAMPLE: Button with icon (justified)
# GOOD: Icon button component
name: Icon Button
props:
icon_position:
type: string
enum: ['before', 'after']
default: 'before'
icon_class:
type: string
color:
type: string
enum: ['primary', 'secondary']
slots:
content:
title: Button text
WHY: This pattern appears across the site, has consistent structure but configurable variants, and represents a design system element.
Props vs Slots Architecture Decisions
Decision Table: Props vs Slots
| Scenario | Use Props | Use Slots | WHY |
|---|---|---|---|
| Simple text/string | ✅ | ❌ | Props are validated, typed, documented in schema |
| HTML content | ❌ | ✅ | Props can't contain markup safely |
| Number/boolean config | ✅ | ❌ | Props provide type validation |
| Nested components | ❌ | ✅ | Slots enable composition |
| User-generated content | ❌ | ✅ | Props shouldn't trust user HTML |
| Design system token | ✅ | ❌ | Props enforce valid values via enum |
Pattern: Props for Configuration
GOOD PRACTICE:
props:
type: object
properties:
size:
type: string
enum: ['sm', 'md', 'lg'] # Enforce valid values
default: 'md'
color:
type: string
enum: ['primary', 'secondary', 'success']
default: 'primary'
WHY: - Schema validation — Invalid values are caught during rendering - Documentation — Developers see valid options without reading code - IDE support — Schema enables autocomplete in modern IDEs - Design system enforcement — Can't use arbitrary colors outside the system
Pattern: Slots for Content
GOOD PRACTICE:
slots:
header:
title: Card Header
description: Optional header content with custom markup
body:
title: Card Body
description: Main card content
footer:
title: Card Footer
description: Optional footer with buttons or links
WHY:
- Flexibility — Content editors can use rich text, other components, or Drupal fields
- Composition — Slots enable nesting {% include 'theme:button' %} inside card
- No escaping issues — Slots handle markup safely without prop escaping concerns
SCSS Scoping Best Practices
Pattern: BEM Inside Components
CRITICAL RULE: Use BEM methodology for component-specific classes.
// components/card-teaser/card-teaser.scss
@import '../../src/scss/init';
.card-teaser {
// Block-level styles
background: $white;
border-radius: $border-radius;
// Elements (use &__ syntax)
&__image {
width: 100%;
height: auto;
}
&__title {
font-size: $h3-font-size;
color: $headings-color;
}
&__excerpt {
color: $text-muted;
}
// Modifiers (use &-- syntax)
&--featured {
border: 2px solid $primary;
box-shadow: $box-shadow-lg;
}
}
WHY BEM IN SDCs:
- Prevents global CSS conflicts — .card-teaser__title won't accidentally affect other .title classes
- Self-documenting — Class names describe the component structure
- Maintainable — Easy to understand which styles belong to which component
- Scoped styling — Changes to .card-teaser don't leak to other components
Pattern: Import _init.scss for Bootstrap Access
ALWAYS DO THIS:
// At the top of every SDC SCSS file
@import '../../src/scss/init';
WHY:
- Access to variables — $primary, $spacer, $border-radius available
- Access to mixins — media-breakpoint-up(), button-variant() available
- Access to functions — color-contrast(), shade-color(), tint-color() available
- Consistency — Component uses same design tokens as rest of theme
COMMON MISTAKE — Hardcoding values:
// BAD: Hardcoded
.my-component {
color: #194582; // What token is this?
padding: 16px; // Is this consistent with spacing scale?
}
// GOOD: Using Bootstrap variables
.my-component {
color: $primary;
padding: $spacer; // Uses spacing scale
}
Pattern: Component-Specific Styles Only
KEEP SCOPED:
// GOOD: Styles scoped to .card-teaser
.card-teaser {
&__title {
// Only affects titles inside card-teaser
}
}
DON'T DO THIS:
// BAD: Global styles in component SCSS
h3 {
// This affects ALL h3 elements, not just in this component
font-size: 1.5rem;
}
.container {
// Accidentally overriding Bootstrap's .container
}
WHY: Component SCSS should style ONLY that component. Global styles belong in src/scss/base/_elements.scss or _typography.scss.
Schema Definition Best Practices
Pattern: Require Only What's Necessary
props:
type: object
required:
- title # Actually required
properties:
title:
type: string
title: Title
description:
type: string
title: Description
default: '' # Optional, has default
WHY: - Flexibility — Components work with minimal data - Reusability — Can use component in more contexts - Better error messages — Clear what's actually required
COMMON MISTAKE — Over-requiring:
required:
- title
- description
- icon
- color
- size
WHY BAD: Forces developers to pass props they don't need, reducing reusability.
Pattern: Use Enums for Fixed Options
props:
properties:
size:
type: string
enum: ['sm', 'md', 'lg']
default: 'md'
WHY: - Validation — Drupal rejects invalid values at render time - Documentation — Developers see valid options immediately - Prevents typos — Can't accidentally pass 'medium' instead of 'md'
Pattern: Default Values for Everything Optional
props:
properties:
show_icon:
type: boolean
default: false # Explicit default
button_style:
type: string
enum: ['primary', 'secondary']
default: 'primary' # Explicit default
WHY: - Graceful fallback — Component works even if prop omitted - Consistent behavior — All devs get same default behavior - Documentation — Defaults show intended usage
Common Mistakes Senior Themers Catch in Code Review
1. Not checking slot existence
{# BAD: Will error if slot not provided #}
<div class="header">
{{ slots.header }}
</div>
{# GOOD: Conditional rendering #}
{% if slots.header %}
<div class="header">
{{ slots.header }}
</div>
{% endif %}
WHY: Empty slots render empty <div>s or cause errors.
2. Using props for HTML content
# BAD: Props can't safely contain markup
props:
properties:
description:
type: string # What if this contains <script> tags?
{# BAD: Unescaped HTML in prop #}
<div>{{ description|raw }}</div>
WHY: Security risk (XSS) and props aren't designed for markup. Use slots instead.
3. Not using Drupal attributes object
{# BAD: Hardcoded classes #}
<div class="my-component my-component--primary">
{# GOOD: Merge with attributes #}
{%
set classes = [
'my-component',
variant ? 'my-component--' ~ variant : '',
]
%}
<div {{ attributes.addClass(classes) }}>
WHY: attributes object allows Drupal to add contextual classes, data attributes, and IDs. Hardcoding prevents this.
4. Duplicating Radix components
# BAD: Creating custom button when Radix has one
components/button/button.component.yml
WHY: Radix already has 57 components. Check radix/components/ before creating.
PROCESS:
1. Check Radix component catalog (Section 8.1)
2. Can you use Radix component as-is? → Use {% include 'radix:button' %}
3. Need minor changes? → Extend via wrapper or custom SCSS
4. Need major structural changes? → Override (copy to sub-theme)
5. Doesn't exist? → Create new component
5. Not namespacing component CSS
// BAD: Generic class names
.title {
// This will conflict with other .title classes
}
// GOOD: Namespaced with BEM
.hero-section__title {
// Clear scope and ownership
}
WHY: CSS specificity battles and accidental overrides across components.