<?php
namespace Drupalmy_modulePluginparagraphsBehavior;
use DrupalparagraphsEntityParagraph;
use DrupalparagraphsParagraphsBehaviorBase;
use DrupalCoreFormFormStateInterface;
/**
* @ParagraphsBehavior(
* id = "style_variant",
* label = @Translation("Style variant"),
* description = @Translation("Adds a color/style variant selector."),
* weight = 0,
* )
*/
class StyleVariantBehavior extends ParagraphsBehaviorBase {
public function buildBehaviorForm(Paragraph $paragraph, array &$form, FormStateInterface $form_state) {
$form['style'] = [
'#type' => 'select',
'#title' => $this->t('Style'),
'#options' => [
'light' => $this->t('Light'),
'dark' => $this->t('Dark'),
'brand' => $this->t('Brand'),
'neutral' => $this->t('Neutral'),
],
'#default_value' => $paragraph->getBehaviorSetting($this->getPluginId(), 'style', 'light'),
];
return $form;
}
public function preprocess(&$variables) {
$paragraph = $variables['paragraph'];
$style = $paragraph->getBehaviorSetting($this->getPluginId(), 'style', 'light');
$variables['attributes']['class'][] = 'paragraph--style-' . $style;
}
}
Component-based pages can become media-heavy, so build performance in rather than bolting it on later.<section{{ attributes.addClass('paragraph', 'paragraph--cta') }}>
<div class="paragraph--cta__inner">
{% if content.field_heading|render|trim %}
<h2>{{ content.field_heading }}</h2>
{% endif %}
{{ content.field_text }}
{{ content.field_cta }}
</div>
</section>
Now you make those components versatile.
What did you build in part 1?
Spacing is the problem every paragraph-based system hits eventually: two same-background sections stack into a huge gap, or a paragraph without a title leaves an awkward empty band. The fix is to give editors control over margin and padding per paragraph, because spacing is contextual and only the editor knows the context.This Drupal Paragraphs tutorial is based on patterns we use on production corporate rebuilds – extending a planned library with style variants, responsive layouts, editor-controlled spacing, and admin UX that content teams can run without training. Across two parts you’ve gone from a blank Drupal install to a production-grade, component-based corporate site: a library of universal paragraph types your editors trust and a codebase developers can maintain for years.
How do you implement color and style variants?
/* Mobile-first: stacked, image first. */
.paragraph--text-image {
display: grid;
gap: 1.5rem;
grid-template-columns: 1fr;
}
/* Tablet and up: two columns. */
@media (min-width: 48rem) {
.paragraph--text-image {
grid-template-columns: 1fr 1fr;
gap: 2rem;
align-items: center;
}
.paragraph--text-image--right .paragraph--text-image__media {
order: 2;
}
}
This continues to assume Drupal 10 or 11 and the setup from part 1. If your current Paragraphs setup frustrates editors, our post on Drupal Paragraphs from unusable to empowering explains what separates a broken implementation from one that works in production.For a library of ten-plus types, repeating field_style on each one is tedious and error-prone. The scalable alternative is a Paragraphs behavior plugin, which adds the style setting and its rendering logic to many types at once:
This
Drupal Paragraphs tutorial, part 2, turns the bare components from part 1 into a flexible, production-grade library. In part 1 you planned a reusable component library, set up the Paragraphs foundation, and built three core types: Hero, Text + Image, and Feature Grid. They work, but they’re rigid – one look, one layout. Here you add everything that makes a component library feel flexible and keeps it maintainable for years:
color and style variants, dedicated
responsive design, editor-controlled
spacing,
conditional fields, a polished
admin UX, and a plan for
performance and evolution. Then you apply the patterns to the rest of the library.Pair conditional fields with smart defaults. Pre-select the most common style and the medium spacing options so a paragraph looks right the moment it’s added. Most editors will accept the defaults most of the time, which is exactly what you want – the easy path should produce a good result.
Work mobile-first: write the single-column mobile layout as the default, then add complexity at larger breakpoints. Using the Text + Image component from part 1:A component library is a long-lived asset, so design it to grow. The mark of a healthy system is that adding a new type or variant never requires restructuring the existing ones.
How do you build responsive layouts for every paragraph?
Two quick examples.:root {
--space-s: 1.5rem;
--space-m: 3rem;
--space-l: 5rem;
}
.paragraph--margin-none { margin-block: 0; }
.paragraph--margin-small { margin-block: var(--space-s); }
.paragraph--margin-medium { margin-block: var(--space-m); }
.paragraph--margin-large { margin-block: var(--space-l); }
.paragraph--padding-none { padding-block: 0; }
.paragraph--padding-small { padding-block: var(--space-s); }
.paragraph--padding-medium { padding-block: var(--space-m); }
.paragraph--padding-large { padding-block: var(--space-l); }
none|None
small|Small
medium|Medium
large|Large
CTA Block – field_heading, field_text, field_cta, plus the style behavior:
How do you add editor-controlled spacing?
Add new types based on real demand rather than speculation. When editors start using components in ways you didn’t plan – repurposing a product section to promote an event, for instance – that’s the signal your architecture is working, and a guide to what to build next.The goal is an interface intuitive enough that editors need no training workshop to be productive. The best handover is a populated example page on staging, not a manual.This is the second and final part of a two-part guide to building a component-based corporate website with Drupal Paragraphs. By the end of the series you’ll have a library of 10-12 universal paragraph types with style variants, responsive layouts, and editor-friendly spacing controls.The mechanism is a select field mapped to a CSS class. Add a field_style list field to each paragraph type with these options:A few practices keep it maintainable:
Pay special attention to images. Use Drupal’s responsive image styles so each breakpoint loads an appropriately sized file, and consider art direction (a different crop on mobile) for hero imagery. This matters most when you are replacing image-based content with structured paragraphs: properly responsive components deliver an immediate win in mobile experience, page speed, and SEO. Test on real devices and at every breakpoint, not just by dragging the browser narrower.
How do you use conditional fields and smart defaults?
:root {
--color-light-bg: #ffffff;
--color-light-fg: #1a1a1a;
--color-dark-bg: #11151c;
--color-dark-fg: #ffffff;
--color-brand-bg: #0057ff;
--color-brand-fg: #ffffff;
--color-neutral-bg: #f4f5f7;
--color-neutral-fg: #1a1a1a;
}
.paragraph--style-light { background: var(--color-light-bg); color: var(--color-light-fg); }
.paragraph--style-dark { background: var(--color-dark-bg); color: var(--color-dark-fg); }
.paragraph--style-brand { background: var(--color-brand-bg); color: var(--color-brand-fg); }
.paragraph--style-neutral { background: var(--color-neutral-bg); color: var(--color-neutral-fg); }
Building a component-based Drupal site and want expert hands on the architecture? Our team designs and builds editor-friendly Paragraphs systems every day – from Twig templates and behavior plugins to spacing controls and long-term maintenance. Visit our Drupal development services to see how we can help.
light|Light
dark|Dark
brand|Brand
neutral|Neutral
Drupal core’s Form API supports conditional visibility through #states. For example, show a “Custom background color” field only when the style is set to a custom option:
How do you build the remaining paragraph types?
Replacing image-based content with structured, responsive paragraphs is itself a performance win – real HTML loads faster and reflows cleanly where a heavy graphic could not.Then install a modern admin theme – Gin transforms the editing experience in minutes and is one of the highest-ROI changes you can make on any Drupal site:The admin form is the CMS for content editors. If it’s confusing, the whole system feels confusing – so treat the editing experience as a deliverable, not an afterthought.{% set margin = content.field_margin['#items'].value|default('medium') %}
{% set padding = content.field_padding['#items'].value|default('medium') %}
{%
set spacing = [
'paragraph--margin-' ~ margin,
'paragraph--padding-' ~ padding,
]
%}
<section{{ attributes.addClass('paragraph', spacing) }}>
{# ... component markup ... #}
</section>
A style variant lets a single component render in multiple visual presentations – light, dark, brand, neutral – chosen by the editor from a dropdown. It is a low-cost feature that dramatically increases perceived flexibility, because the same paragraph can look completely different depending on context without anyone touching code.
How do you improve editor controls and admin UX?
With variants, responsiveness, and spacing established, the rest of the library is repetition with intent. Apply the same recipe – minimal fields, a style variant, spacing controls, a mobile-first layout – to each remaining type: CTA Block, Cards, Testimonial, FAQ, and Stats.In part 1 of this Drupal Paragraphs tutorial you established a planning approach (audit pages, find the reusable patterns, build a minimum viable set), installed Paragraphs, added a paragraphs reference field to a content type, and adopted a field-naming convention so every type stays consistent. You then built Hero, Text + Image, and Feature Grid with their Twig templates and base CSS.
- Logical field ordering. Put the most-used fields first: heading, then image, then body, then options like style and spacing. Configure this on the type’s “Manage form display”.
- Help text on every field. A one-line description removes guesswork. Tell editors what “Brand” style looks like or what image ratio works best.
- Smart defaults. Pre-select the most common style variant and column count so editors only change what they need to.
- Descriptive type names. “Hero section with image” reads better in the add menu than “hero_v2”.
Add two list fields – or, better, a spacing behavior – offering the same scale for each:
function my_module_form_alter(&$form, $form_state, $form_id) {
if (isset($form['field_custom_bg'])) {
$form['field_custom_bg']['#states'] = [
'visible' => [
':input[name="field_style"]' => ['value' => 'custom'],
],
];
}
}
composer require drupal/gin
drush theme:enable gin -y
drush config:set system.theme admin gin -y
Responsive does not mean “the same thing, but smaller.” Each paragraph type needs its own mobile layout where content is reorganized and reprioritized for a small screen – not merely scaled down.
- Lazy-load media. Defer offscreen images so the initial render stays fast. Modern Drupal adds
loading="lazy" to image fields, but verify it for media embedded in paragraphs.
- Cache effectively. Paragraph-based pages benefit from Drupal’s render cache; make sure your custom templates and behaviors declare correct cache tags and contexts so pages stay cached and still update when content changes.
- Optimize images per variant. A dark-background section may need a different image treatment than a light one; serve appropriately sized and compressed files via responsive image styles.
- Measure. Run Lighthouse before and after, watching Largest Contentful Paint and Cumulative Layout Shift, and treat regressions as bugs.
This section gives you a working implementation to drop into your tutorial build. For the full discussion of why component-based layouts develop spacing problems, the trade-offs between automatic and manual spacing, and the edge cases to watch for, see our dedicated deep dive on Drupal Paragraphs spacing and layout gaps.
How do you maintain and evolve the component library?
Start with the fundamentals on every paragraph type:Read also: flexible and easy content creation with the Drupal Paragraphs module and Paragraph View Mode review module for Drupal.
- Add types without breaking others. Because each paragraph type is self-contained, new ones slot in cleanly. Resist editing shared CSS in ways that ripple across components – lean on the custom-property scales instead.
- Version your templates. Keep template and behavior changes in code review and configuration management, so changes are deliberate and reversible.
- Document the library. Maintain a living reference of every type, its fields, and its variants. This is what stops two developers building near-duplicate components.
- Know when to refactor versus add. If a new requirement is a variant of an existing type, extend it. If it’s genuinely different, add a new type. Avoid both extremes: a graveyard of one-off types, and an overloaded mega-component trying to do everything.
.paragraph--stats__items {
display: grid;
gap: 1rem;
grid-template-columns: 1fr;
}
@media (min-width: 48rem) {
.paragraph--stats__items {
grid-template-columns: repeat(4, 1fr);
}
}