Skip to content

How-to guideFor module developersDrupal 11

Custom field types

A custom field type lets site builders attach your data type to any entity in the UI, the same way they add a text or number field. It comes in three plugins: the field type (storage and shape), a widget (how it is edited), and a formatter (how it is displayed). The worked example is a hex color field.

Ship a reusable field type rather than a one-off database column. Reach for this when several entities need the same structured value and editors should manage it; for a single entity’s internal data, a base field on a custom entity is simpler.

The field type declares its typed-data properties and its database schema. Put it in src/Plugin/Field/FieldType/, mark it with the FieldType attribute, and name the default widget and formatter you build below.

web/modules/custom/howto_field_type/src/Plugin/Field/FieldType/ColorHexItem.php
<?php
declare(strict_types=1);
namespace Drupal\howto_field_type\Plugin\Field\FieldType;
use Drupal\Core\Field\Attribute\FieldType;
use Drupal\Core\Field\FieldItemBase;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\TypedData\DataDefinition;
/**
* Defines the 'color_hex' field type: a single hex color string.
*/
#[FieldType(
id: 'color_hex',
label: new TranslatableMarkup('Color (hex)'),
description: new TranslatableMarkup('Stores a hex color like #0678be.'),
default_widget: 'color_hex_default',
default_formatter: 'color_hex_swatch',
)]
final class ColorHexItem extends FieldItemBase {
/**
* The properties (the typed-data shape) of one field value.
*/
public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition): array {
$properties['value'] = DataDefinition::create('string')
->setLabel(new TranslatableMarkup('Hex color'))
->setRequired(TRUE);
return $properties;
}
/**
* The database columns backing those properties.
*/
public static function schema(FieldStorageDefinitionInterface $field_definition): array {
return [
'columns' => [
'value' => [
'type' => 'varchar',
'length' => 7,
],
],
];
}
/**
* An item with no value is treated as empty (and not saved).
*/
public function isEmpty(): bool {
$value = $this->get('value')->getValue();
return $value === NULL || $value === '';
}
}

propertyDefinitions() describes the value to the typed-data system; schema() describes the columns to the database. isEmpty() decides when an item is skipped on save; get it wrong and you store blank rows.

The widget renders the edit element for one field item. Put it in src/Plugin/Field/FieldWidget/, and list the field types it serves.

web/modules/custom/howto_field_type/src/Plugin/Field/FieldWidget/ColorHexWidget.php
<?php
declare(strict_types=1);
namespace Drupal\howto_field_type\Plugin\Field\FieldWidget;
use Drupal\Core\Field\Attribute\FieldWidget;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\WidgetBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Edits a color_hex value with the HTML5 color picker.
*/
#[FieldWidget(
id: 'color_hex_default',
label: new TranslatableMarkup('Color picker'),
field_types: ['color_hex'],
)]
final class ColorHexWidget extends WidgetBase {
/**
* Returns the form element for one field item (delta).
*/
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state): array {
$element['value'] = $element + [
'#type' => 'color',
'#default_value' => $items[$delta]->value ?? '#000000',
];
return $element;
}
}

formElement() returns a render element keyed by the property it edits (value), pre-filled from the current item.

The formatter turns stored values into a render array. Put it in src/Plugin/Field/FieldFormatter/.

web/modules/custom/howto_field_type/src/Plugin/Field/FieldFormatter/ColorHexFormatter.php
<?php
declare(strict_types=1);
namespace Drupal\howto_field_type\Plugin\Field\FieldFormatter;
use Drupal\Core\Field\Attribute\FieldFormatter;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\FormatterBase;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Renders each color_hex value as a labelled swatch.
*/
#[FieldFormatter(
id: 'color_hex_swatch',
label: new TranslatableMarkup('Swatch'),
field_types: ['color_hex'],
)]
final class ColorHexFormatter extends FormatterBase {
/**
* Builds a render array for the whole field item list.
*/
public function viewElements(FieldItemListInterface $items, $langcode): array {
$elements = [];
foreach ($items as $delta => $item) {
$value = (string) $item->get('value')->getValue();
$elements[$delta] = [
'#type' => 'html_tag',
'#tag' => 'span',
'#value' => $value,
'#attributes' => [
'class' => ['color-hex-swatch'],
'style' => 'background-color:' . $value,
],
];
}
return $elements;
}
}

Install the module, then add the field and check it round-trips:

Terminal window
ddev drush pm:install howto_field_type
ddev drush php:eval "
\Drupal\field\Entity\FieldStorageConfig::create(['field_name' => 'field_color', 'entity_type' => 'node', 'type' => 'color_hex'])->save();
print 'created';
"

Add a “Color (hex)” field to a content type at Structure → Content types → Manage fields; the color picker appears on the edit form and the swatch on display. The kernel test in the example module proves the value round-trips through storage.

Add defaultStorageSettings() / storageSettingsForm() to the field type, and defaultSettings() / settingsForm() to the widget or formatter, to expose per-field options.

Override getConstraints() on the field type to enforce rules (a valid hex pattern, here). Core’s EmailItem is a concise model.

Was this page helpful?

Drupal is a registered trademark of Dries Buytaert.