Building a custom block using the Block Plugin APIfor Drupal 8 , 9 , 10 , and 11

Last updated :  

The Block plugin type is the most visible plugin system in all of Drupal — every widget you've ever dragged into a region on the Block Layout page is a Block plugin. Now that you understand plugins in the abstract, this lesson builds a real one: a configurable block whose content a site builder can change entirely from the admin UI, with no code changes required.

What you'll learn in this lesson

  • The minimum shape a Block plugin needs: an annotation and a build() method
  • How defaultConfiguration(), blockForm(), and blockSubmit() work together to make a block configurable
  • Why block configuration lives on the block instance, not in module code

The @Block annotation

Every Block plugin extends BlockBase and carries a @Block annotation with just two required properties:

/**
 * Provides a 'Example: configurable text string' block.
 *
 * @Block(
 *   id = "example_configurable_text",
 *   admin_label = @Translation("Example: configurable text")
 * )
 */
class ExampleConfigurableTextBlock extends BlockBase {

id is the machine name Drupal uses internally; admin_label is what a site builder sees in the "Place block" list. That annotation alone — combined with living in a module's src/Plugin/Block/ directory — is enough for Drupal to discover this class as a placeable block. No YAML registration, no hook.

Four methods that make a block configurable

A block that always shows the same fixed output only needs build(). Making it configurable adds three more methods that work together as a loop:

public function defaultConfiguration() {
  return [
    'block_example_string' => $this->t('A default value. This block was created at %time', ['%time' => date('c')]),
  ];
}

public function blockForm($form, FormStateInterface $form_state) {
  $form['block_example_string_text'] = [
    '#type' => 'textarea',
    '#title' => $this->t('Block contents'),
    '#default_value' => $this->configuration['block_example_string'],
  ];
  return $form;
}

public function blockSubmit($form, FormStateInterface $form_state) {
  $this->configuration['block_example_string']
    = $form_state->getValue('block_example_string_text');
}

public function build() {
  return [
    '#markup' => $this->configuration['block_example_string'],
  ];
}

The loop reads: defaultConfiguration() supplies a starting value the very first time the block is placed. blockForm() renders that value into an editable textarea whenever a site builder opens the block's configuration form. blockSubmit() reads the submitted textarea value back into $this->configuration when the form is saved. And build() reads from that same $this->configuration array every time the block actually renders on a page. Change the text once in the config form, and every subsequent page load reflects it — because build() and blockForm() are reading from the same source of truth.

The configuration lives on the block placement, not the plugin class. If you place the same "Example: configurable text" block in two different regions, each placement gets its own independent $this->configuration — editing one doesn't touch the other. The plugin class defines the behavior; each placement instance carries its own data.

Quick check: if you deleted blockSubmit() entirely but left blockForm() in place, what would happen when a site builder changed the textarea and saved? (The form would still render with the current value, and saving would succeed with no error — but the new text would never actually be stored, because nothing reads the submitted form value back into $this->configuration. The block would keep showing its old content forever.)

Key takeaways

  • A Block plugin needs at minimum a @Block annotation (with id and admin_label) and a build() method returning a render array.
  • defaultConfiguration(), blockForm(), and blockSubmit() form a loop: default → editable form → saved back into $this->configuration → read again by build().
  • Configuration is per-placement, not per-class — placing the same block twice gives each instance independent settings.
  • Forgetting blockSubmit() is a silent bug: the form appears to work, but nothing ever persists.

Coming up next

Blocks are one Drupal core plugin type; Fields are built on three separate, cooperating plugin types. Next you'll build the first of them — a custom Field Type plugin that defines what a field actually stores.