If the widget from the last lesson is how a value gets in, a formatter is how it gets shown. This lesson closes out the field-plugin trio by building a formatter that turns a stored hex color into an actual colored block of text on the page — and, as a bonus, shows how a formatter can offer its own configuration options, exactly like the configurable block from earlier in this tutorial.
What you'll learn in this lesson
- The
@FieldFormatterannotation andviewElements(), the one method every formatter must implement - How a formatter can expose its own settings form via
settingsForm()anddefaultSettings() - Why formatter output is a render array, not raw HTML
The @FieldFormatter annotation and viewElements()
/**
* @FieldFormatter(
* id = "field_example_color_background",
* label = @Translation("Change the background of the output text"),
* field_types = {
* "field_example_rgb"
* }
* )
*/
class ColorBackgroundFormatter extends FormatterBase {
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;
}
}
viewElements() receives every value stored in the field (an entity can have multiple values on a multi-value field) and must return one render array per delta. This formatter builds an html_tag element styled with an inline background-color matching the stored hex value — and optionally flips the text color between black and white based on a lightness calculation, so the label stays readable against any background color.
Giving the formatter its own settings
public static function defaultSettings() {
return [
'adjust_text_color' => TRUE,
] + parent::defaultSettings();
}
public function settingsForm(array $form, FormStateInterface $form_state) {
$elements['adjust_text_color'] = [
'#type' => 'checkbox',
'#default_value' => $this->getSetting('adjust_text_color'),
'#title' => $this->t('Adjust foreground text color'),
];
return $elements;
}
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 the exact same configuration pattern you saw on the configurable Block plugin — defaultSettings() supplies a starting value, settingsForm() renders it as an editable checkbox (reached by clicking the gear icon next to the formatter on the "Manage display" page), and Drupal's Field UI module automatically saves whatever's submitted with no submit handler needed on your part. settingsSummary() is a small but useful extra: the one-line text shown in the field list when the settings form is collapsed, so a site builder can see the current setting without opening it.
html_tag element (rather than returning a literal <p style="...">...</p> string from #markup) keeps the value's attributes as structured, alterable data — another module's hook_field_formatter_view_alter() can still adjust the color or text afterward, which wouldn't be possible if the HTML were already flattened into a string.Quick check: why does
settingsForm()not need its own submit handler, unlike the Block plugin'sblockForm()/blockSubmit()pair? (Because the Field UI module — not your formatter class — owns the "Manage display" form as a whole, and it already knows how to save whatever settings a formatter'ssettingsForm()declares. A Block's configuration form is a different, standalone form with no equivalent built-in save handler, which is why blocks need their ownblockSubmit().)
Key takeaways
- A Field Formatter's one required method is
viewElements(), returning one render array per stored value (delta). - Formatters can be configurable via
defaultSettings()/settingsForm()— no submit handler needed, since the Field UI module handles saving. settingsSummary()shows a one-line preview of the current configuration on the "Manage display" page.- Keep formatter output as structured render array properties, not a flattened HTML string, so other modules can still alter it.
Coming up next
You've now built all three field plugins — type, widget, formatter — completing the same pattern used by every field on every content type in Drupal. Next, the tutorial moves to a plugin type with no direct Field API equivalent: building a custom Views plugin.