Drupal Paragraphs: Varianten, Abstände & Admin-UX | Droptica

Drupal Paragraphs tutorial, part 2: variants, responsive design, spacing, and admin UX

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.

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.

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.

In this article:

What did you build in part 1?

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.

Now you make those components versatile.

How do you implement color and style variants?

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.

The mechanism is a select field mapped to a CSS class. Add a field_style list field to each paragraph type with these options:

light|Light
dark|Dark
brand|Brand
neutral|Neutral

Read the value in the template and apply it as a modifier class on the wrapper:

{% set style = content.field_style['#items'].value|default('light') %}
<section{{ attributes.addClass('paragraph', 'paragraph--hero', 'paragraph--style-' ~ style) }}>
  {# ... component markup ... #}
</section>

Then structure the CSS as base styles plus variant overrides, with the palette defined once as custom properties:

: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); }

Because every variant reads from the same custom properties, a rebrand becomes a handful of value changes rather than a sweep through every component.

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:

<?php

namespace Drupal\my_module\Plugin\paragraphs\Behavior;

use Drupal\paragraphs\Entity\Paragraph;
use Drupal\paragraphs\ParagraphsBehaviorBase;
use Drupal\Core\Form\FormStateInterface;

/**
 * @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;
  }

}

Enable the behavior on whichever paragraph types should offer variants, and the selector appears consistently across all of them. This is the approach we reach for on real component libraries because it keeps cross-cutting settings in one place.

Read also: flexible and easy content creation with the Drupal Paragraphs module and Paragraph View Mode review module for Drupal.

How do you build responsive layouts for every paragraph?

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.

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:

/* 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;
  }
}

Adopt a consistent breakpoint strategy across the whole library - for example a mobile base, a tablet breakpoint around 48rem, and a desktop breakpoint around 64rem - so components behave predictably together.

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 add editor-controlled spacing?

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 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.

Add two list fields - or, better, a spacing behavior - offering the same scale for each:

none|None
small|Small
medium|Medium
large|Large

Expose two separate controls. Margin pushes a paragraph away from its neighbors; padding adjusts the space inside it. Both are needed, and keeping them separate is what makes the system genuinely flexible. Read them in the template as modifier classes:

{% 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>

Define the scale once with custom properties so it stays consistent everywhere:

: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); }

Test the controls against the scenarios that always come up: two white paragraphs stacked (editor sets the second’s top margin to none), a paragraph without a title (drop the padding a size), and a full-bleed image (zero padding). Recommend editor controls over automatic CSS rules - automatic spacing changes things editors can’t see or predict, and predictability is what keeps them trusting the system.

How do you use conditional fields and smart defaults?

A long form with every possible field visible at once overwhelms editors. Conditional fields show options only when they’re relevant, cutting cognitive load through progressive disclosure.

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:

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'],
      ],
    ];
  }
}

For non-developers configuring this through the UI, the Conditional Fields contrib module provides the same behavior without code. Either way, the principle is the same: hide what isn’t needed yet.

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.

How do you build the remaining paragraph types?

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.

Two quick examples.

CTA Block - field_heading, field_text, field_cta, plus the style behavior:

<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>

Stats / Numbers - a stats type referencing repeating stat_item paragraphs (field_value, field_label), rendered as a responsive row:

.paragraph--stats__items {
  display: grid;
  gap: 1rem;
  grid-template-columns: 1fr;
}
@media (min-width: 48rem) {
  .paragraph--stats__items {
    grid-template-columns: repeat(4, 1fr);
  }
}

As you build each one, document it - the type’s purpose, its fields, and its variants. A short living document of the component library prevents two developers from creating slightly different versions of the same thing and is the reference editors and designers will keep coming back to. The same component mindset that guided planning in part 1 applies here: each type should be universal enough to combine on many pages, not bespoke to one layout.

How do you improve editor controls and admin UX?

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.

Start with the fundamentals on every paragraph type:

  • 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”.

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:

composer require drupal/gin
drush theme:enable gin -y
drush config:set system.theme admin gin -y

Gin improves the Paragraphs editing experience itself - clearer add buttons, better drag-and-drop, and a tidier form - which directly supports the editor-friendly approach the whole library is built around. Push the details further with a few advanced touches:

  • Paragraph type icons. Add an icon to each type so the add menu is scannable at a glance.
  • Limit available types per content type. On the paragraphs field’s form display, restrict which types editors can add on a given content type, so a landing page and a news article each offer only relevant components.
  • A clear add experience. Use the modal or dropdown add mode so editors know exactly what they’re inserting. For power users, modules like Geysir enable faster in-place paragraph editing.
  • Custom admin styling. A little CSS targeting the paragraph forms - grouping related fields, tightening spacing - makes long forms easier to scan.

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.

What performance considerations matter for paragraph-based pages?

Component-based pages can become media-heavy, so build performance in rather than bolting it on later.

  • 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.

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.

How do you maintain and evolve the component library?

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.

A few practices keep it maintainable:

  • 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.

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.

Want to build a component-based Drupal site with Paragraphs?

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.

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.