Drupal Field Formatters: Custom Display Logic and Settingsfor Drupal 11 , and 10

Last updated :  

You've now seen how a color value is stored (the field type) and how it's edited (the widget). This lesson closes the loop with the third and final piece: the formatter, the plugin that decides how a stored value actually gets shown to a visitor who's just reading the page, not editing it. And this one does something more interesting than just printing the hex code — it uses the stored color as an actual background, and even picks black or white text automatically so it stays readable.

What you'll learn in this lesson

  • What a Field Formatter plugin does, and how field_types restricts which fields can use it — same pattern as the widget from the last lesson
  • How a formatter can offer its own configuration form to site builders, with defaults, a settings form, and a compact summary
  • How to build a render array using #type => 'html_tag' to output an arbitrary HTML element with custom attributes
  • Why business logic (like a color-lightness calculation) belongs in its own helper method, not crammed into the render method
The three-piece pattern, complete: field type stores the data, widget edits it, formatter displays it. Every field on every Drupal site works this way — even the built-in ones like Text or Image.

The source file

Path: modules/field_example/src/Plugin/Field/FieldFormatter/ColorBackgroundFormatter.php

<?php

namespace Drupal\field_example\Plugin\Field\FieldFormatter;

use Drupal\Core\Field\FormatterBase;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Form\FormStateInterface;

/**
 * Plugin implementation of the 'field_example_color_background' formatter.
 *
 * This example demonstrates how a field formatter plugin can provide
 * configuration options to the user and then alter the output based on their
 * choices. We'll add a toggle, that defaults to on, for a feature that
 * attempts to automatically adjust the foreground color of the text to either
 * black or white depending on the lightness of the background color.
 *
 * @FieldFormatter(
 *   id = "field_example_color_background",
 *   label = @Translation("Change the background of the output text"),
 *   field_types = {
 *     "field_example_rgb"
 *   }
 * )
 */
class ColorBackgroundFormatter extends FormatterBase {

  /**
   * {@inheritdoc}
   */
  public function viewElements(FieldItemListInterface $items, $langcode) {
    $elements = [];

    foreach ($items as $delta => $item) {
      // Set the value of the CSS color property depending on user provided
      // configuration. Individual configuration items can be accessed with
      // $this->getSetting('key') where 'key' is the same as the key in the
      // form array from settingsForm() and what's defined in the configuration
      // schema.
      $text_color = 'inherit';
      if ($this->getSetting('adjust_text_color')) {
        $text_color = $this->lightness($item->value) < 50 ? 'white' : 'black';
      }

      $elements[$delta] = [
        '#type' => 'html_tag',
        '#tag' => 'p',
        '#value' => $this->t('The content area color has been changed to @code', ['@code' => $item->value]),
        '#attributes' => [
          'style' => 'background-color: ' . $item->value . '; color: ' . $text_color,
        ],
      ];
    }
    return $elements;
  }

  /**
   * {@inheritdoc}
   *
   * Set the default values for the formatter's configuration.
   */
  public static function defaultSettings() {
    // The keys of this array should match the form element names in
    // settingsForm(), and the schema defined in
    // config/schema/field_example.schema.yml.
    return [
      'adjust_text_color' => TRUE,
    ] + parent::defaultSettings();
  }

  /**
   * {@inheritdoc}
   *
   * Define the Form API widgets a user should see when configuring the
   * formatter. These are displayed when a user clicks the gear icon in the row
   * for a formatter on the manage display page.
   *
   * The field_ui module takes care of handling submitted form values.
   */
  public function settingsForm(array $form, FormStateInterface $form_state) {
    // Create a new array with one or more form elements. $form is available for
    // context, but you should not add your elements to it directly.
    $elements = [];

    // The keys of the array, 'adjust_text_color' in this case, should match
    // what is defined in ::defaultSettings(), and the field_example.schema.yml
    // schema. The values collected by the form will be automatically stored
    // as part of the field instance configuration, so you do not need to
    // implement form submission processing.
    $elements['adjust_text_color'] = [
      '#type' => 'checkbox',
      // The current configuration for this setting for the field instance can
      // be accessed via $this->getSetting().
      '#default_value' => $this->getSetting('adjust_text_color'),
      '#title' => $this->t('Adjust foreground text color'),
      '#description' => $this->t('Switch the foreground color between black and white depending on lightness of the background color.'),
    ];
    return $elements;
  }

  /**
   * {@inheritdoc}
   */
  public function settingsSummary() {
    // This optional summary text is displayed on the manage displayed in place
    // of the formatter configuration form when the form is closed. You'll
    // usually see it in the list of fields on the manage display page where
    // this formatter is used.
    $state = $this->getSetting('adjust_text_color') ? $this->t('yes') : $this->t('no');
    $summary[] = $this->t('Adjust text color: @state', ['@state' => $state]);
    return $summary;
  }

  /**
   * Determine lightness of a color.
   *
   * This might not be the best way to determine if the contrast between the
   * foreground and background colors is legible. But it'll work well enough for
   * this demonstration.
   *
   * Logic from https://stackoverflow.com/a/12228730/8616016.
   *
   * @param string $color
   *   A color in hex format, leading '#' is optional.
   *
   * @return float
   *   Percentage of lightness of the provided color.
   */
  protected function lightness(string $color) {
    $hex = ltrim($color, '#');
    // Convert the hex string to RGB.
    $r = hexdec($hex[0] . $hex[1]);
    $g = hexdec($hex[2] . $hex[3]);
    $b = hexdec($hex[4] . $hex[5]);

    // Calculate the HSL lightness value and return that as a percent.
    return ((max($r, $g, $b) + min($r, $g, $b)) / 510.0) * 100;
  }

}

How it works

The @FieldFormatter annotation

@FieldFormatter(
  id = "field_example_color_background",
  label = @Translation("Change the background of the output text"),
  field_types = {
    "field_example_rgb"
  }
)

Same shape as the widget annotation from the last lesson: an id stored in field display configuration, a label shown in the "Manage display" dropdown, and a field_types allowlist — this formatter will only ever be offered as an option for field_example_rgb fields, nothing else.

Extending FormatterBase

Just like WidgetBase for widgets, FormatterBase gives every formatter default no-op implementations of the optional methods (defaultSettings(), settingsForm(), settingsSummary()) plus helpers like $this->getSetting() and $this->t(). This particular formatter actually uses all three optional methods, which is what makes it a good teaching example — most formatters in the wild only bother implementing one or two.

viewElements() — the one method every formatter must have

public function viewElements(FieldItemListInterface $items, $langcode) {
  $elements = [];

  foreach ($items as $delta => $item) {
    $text_color = 'inherit';
    if ($this->getSetting('adjust_text_color')) {
      $text_color = $this->lightness($item->value) < 50 ? 'white' : 'black';
    }

    $elements[$delta] = [
      '#type' => 'html_tag',
      '#tag' => 'p',
      '#value' => $this->t('The content area color has been changed to @code', ['@code' => $item->value]),
      '#attributes' => [
        'style' => 'background-color: ' . $item->value . '; color: ' . $text_color,
      ],
    ];
  }
  return $elements;
}

This is called once per entity display, and must return a render array keyed by delta (so multi-value fields render every value, not just the first). Two things worth slowing down on:

  • #type => 'html_tag' is a general-purpose render element for "wrap this in an arbitrary HTML tag." Here it's a <p>, with #value as the inner text and #attributes mapping directly to HTML attributes — in this case, an inline style attribute that sets the background to the actual stored color.
  • The @code placeholder inside $this->t('...@code', ['@code' => $item->value]) isn't just string interpolation — Drupal's translation system automatically HTML-escapes anything substituted through a @placeholder, which is what keeps this safe from cross-site scripting even though the value is being echoed straight into a rendered page.

Giving site builders a configuration option

public static function defaultSettings() {
  return [
    'adjust_text_color' => TRUE,
  ] + parent::defaultSettings();
}

public function settingsForm(array $form, FormStateInterface $form_state) {
  $elements = [];
  $elements['adjust_text_color'] = [
    '#type' => 'checkbox',
    '#default_value' => $this->getSetting('adjust_text_color'),
    '#title' => $this->t('Adjust foreground text color'),
    '#description' => $this->t('Switch the foreground color between black and white depending on lightness of the background color.'),
  ];
  return $elements;
}

These two methods work as a pair. defaultSettings() declares that this formatter has one configurable setting, adjust_text_color, defaulting to on. settingsForm() exposes that same setting as a checkbox in the UI — click the gear icon next to this formatter on the "Manage display" screen and you'll see exactly this checkbox. Notice you don't write any code to save the submitted value: the field_ui module handles persisting whatever the site builder submits, as long as the keys in your form match the keys in defaultSettings() (and the field's config schema).

settingsSummary() — a one-line status, no clicking required

public function settingsSummary() {
  $state = $this->getSetting('adjust_text_color') ? $this->t('yes') : $this->t('no');
  $summary[] = $this->t('Adjust text color: @state', ['@state' => $state]);
  return $summary;
}

This is a small but genuinely useful UX touch: instead of making a site builder open the settings form just to check whether "adjust text color" is on or off, this text appears directly in the "Manage display" table whenever the settings panel is collapsed. Small optional methods like this are what separates a formatter that's pleasant to configure from one that isn't.

A private helper method: lightness()

protected function lightness(string $color) {
  $hex = ltrim($color, '#');
  $r = hexdec($hex[0] . $hex[1]);
  $g = hexdec($hex[2] . $hex[3]);
  $b = hexdec($hex[4] . $hex[5]);
  return ((max($r, $g, $b) + min($r, $g, $b)) / 510.0) * 100;
}

This method isn't part of any Drupal interface — it's just a plain PHP helper the class defines for its own use, marked protected so it's available to this class (and any subclass) but not called from outside. It splits the hex string into its R/G/B decimal components with hexdec(), then applies a standard HSL-lightness formula: (max + min) / 510 * 100. The divisor 510 normalizes the result to a 0–100 percentage, since the largest possible max + min is 255 + 255. Below 50 is treated as "dark enough that white text reads better"; 50 and above flips to black text. This is exactly the kind of logic that shouldn't live inline inside viewElements() — pulling it into its own method keeps the render method readable and makes the math independently testable.

Quick check: if a site builder unchecks "Adjust foreground text color" in the settings form, what happens to $text_color in viewElements()? (Answer: it stays at its initial value, 'inherit' — meaning the text just uses whatever color it would normally have, and the lightness calculation never even runs.)

See it for yourself

Visit any article with a "Favorite Color" value on your DDEV site (e.g. /node/82).

An article page showing the Favorite Color field rendered as an orange background bar with the text The content area color has been changed to #FF5733

That colored bar is the formatter's output, not a screenshot trick — the background color is the literal stored hex value (#FF5733, a warm orange), and because that color is light enough, lightness() decided black text would read better than white. Same underlying field value from the widget lesson, completely different presentation, because a formatter is doing the rendering now instead of an edit form.

Key takeaways

  • A Field Formatter is declared with @FieldFormatter, using the same id / label / field_types pattern as widgets — field_types is what filters the "Manage display" dropdown.
  • The only required method is viewElements(), which must return a render array keyed by delta so multi-value fields render every value.
  • #type => 'html_tag' lets you output an arbitrary HTML element with custom attributes directly from a render array — no template file required for something this simple.
  • Formatter settings follow the same three-method pattern every time: defaultSettings() declares them, settingsForm() exposes them to site builders, and $this->getSetting() reads them at render time. The field_ui module persists them for you automatically.
  • settingsSummary() is optional but worth implementing — it saves site builders a click by showing the current configuration state directly in the "Manage display" table.
  • Keep calculation logic (like the color-lightness math here) in its own protected helper method rather than inline in viewElements() — it stays readable and can be tested on its own.

Coming up next

You now have the full field type / widget / formatter trio. The last piece of the Field API puzzle is access control: what happens when you want different roles to see or edit a field's value differently? That's exactly what the next lesson covers.