Blocks are one of the fundamental building blocks of Drupal. Whether you're a site builder dragging and dropping blocks in the UI or a developer writing custom block plugins, understanding blocks is essential. In this guide, we'll explore blocks from every perspective: configuration, code, views, field formats, and more.
What is a Block?
A block is a reusable piece of content or functionality that can be placed in different regions across your website. Think of blocks as modular containers that hold anything from text and images to forms and custom functionality.
In Drupal, blocks can be:
- Created through the UI (custom blocks)
- Defined in code (block plugins)
- Generated from views
- Created from field formatters
- Part of core or contributed modules
Part 1: Understanding Block Regions and Configuration
Block Regions
Before you can place a block, you need to understand regions. Regions are predefined areas in your theme where blocks can be placed. Common regions include header, sidebar, main content, footer, and more.
How to View and Manage Blocks:
- Navigate to
Structure > Block Layout - You'll see all available regions in your active theme
- Each region shows blocks currently placed there
- You can add, remove, and reorder blocks
Creating Custom Blocks via UI
Site builders can create custom blocks directly in Drupal without touching code:
- Go to
Structure > Block Layout > Custom block library - Click "Create custom block"
- Choose a custom block type (Basic, or custom types you've created)
- Add a description and content
- Save
- Place the block in your desired region
Key Advantages:
- Non-technical users can manage content
- Blocks are stored in the database
- Easy to edit without redeploying code
- Perfect for unique, one-off content
Part 2: Creating Custom Blocks in Code
The Modern Drupal Way: Block Plugins
In Drupal 8+, custom blocks are created as plugins using annotations. This is the recommended approach for reusable, developer-maintained blocks.
Basic Block Plugin Structure
Here's what a simple custom block looks like:
<?php
namespace Drupal\my_module\Plugin\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a 'Hello World' block.
*
* @Block(
* id = "hello_world_block",
* admin_label = @Translation("Hello World Block"),
* category = @Translation("Custom")
* )
*/
class HelloWorldBlock extends BlockBase implements ContainerFactoryPluginInterface {
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static($configuration, $plugin_id, $plugin_definition);
}
/**
* {@inheritdoc}
*/
public function build() {
return [
'#markup' => $this->t('Hello, World!'),
];
}
}
Breaking Down the Annotation
@Block(
id = "hello_world_block",
admin_label = @Translation("Hello World Block"),
category = @Translation("Custom")
)
id: Unique identifier for your block (machine name)admin_label: Human-readable name shown in the UIcategory: Groups your block in the Block Layout interface
The build() Method
The build() method returns a renderable array, Drupal's way of representing content before it's rendered as HTML.
public function build() {
return [
'#markup' => $this->t('Hello, World!'),
];
}
This is the core of your block. Whatever you return here becomes the block's output.
Part 3: Advanced Block Configuration
Adding Configuration Forms
Many blocks need settings. You can add a configuration form that site builders can use:
<?php
namespace Drupal\my_module\Plugin\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Provides a 'Welcome Message' block.
*
* @Block(
* id = "welcome_message_block",
* admin_label = @Translation("Welcome Message Block"),
* category = @Translation("Custom")
* )
*/
class WelcomeMessageBlock extends BlockBase {
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'message' => 'Welcome to our site!',
'show_greeting' => TRUE,
];
}
/**
* {@inheritdoc}
*/
public function blockForm($form, FormStateInterface $form_state) {
$form['message'] = [
'#type' => 'text_field',
'#title' => $this->t('Message'),
'#default_value' => $this->configuration['message'],
'#description' => $this->t('The message to display'),
];
$form['show_greeting'] = [
'#type' => 'checkbox',
'#title' => $this->t('Show greeting?'),
'#default_value' => $this->configuration['show_greeting'],
];
return $form;
}
/**
* {@inheritdoc}
*/
public function blockSubmit($form, FormStateInterface $form_state) {
$this->configuration['message'] = $form_state->getValue('message');
$this->configuration['show_greeting'] = $form_state->getValue('show_greeting');
}
/**
* {@inheritdoc}
*/
public function build() {
if ($this->configuration['show_greeting']) {
return [
'#markup' => $this->configuration['message'],
];
}
return [];
}
}
Now site builders can configure this block directly in the Block Layout UI without touching code.
Block Visibility and Access Control
Control who sees your block and when:
/**
* {@inheritdoc}
*/
public function blockAccess(\Drupal\Core\Session\AccountInterface $account) {
return \Drupal\Core\Access\AccessResult::allowedIfHasPermission($account, 'access content');
}
Part 4: Blocks from Views
Views are a powerful Drupal feature that generate content from your database. You can display a view as a block.
Creating a View Block
- Go to
Structure > Views > Add view - Create your view with filters, sorting, and fields
- Under "Display settings," add a "Block" display
- Configure the block display (number of items, paging, etc.)
- Save
- Go to
Structure > Block Layoutand place your new block
Why Use View Blocks?
- Display dynamic content without writing code
- Filter and sort data easily
- Reusable across multiple pages
- Site builders can modify without developer help
Example Use Cases:
- Recent blog posts
- Featured products
- Team members list
- Upcoming events
- Related content
Part 5: Field Formatters as Blocks
Field formatters control how fields display on content. While not technically blocks, they're often used within blocks to display field data in custom ways.
Creating a Custom Field Formatter
<?php
namespace Drupal\my_module\Plugin\Field\FieldFormatter;
use Drupal\Core\Field\FormatterBase;
use Drupal\Core\Field\FieldItemListInterface;
/**
* Plugin implementation of the 'custom_text_formatter' formatter.
*
* @FieldFormatter(
* id = "custom_text_formatter",
* label = @Translation("Custom Text Formatter"),
* field_types = {
* "string"
* }
* )
*/
class CustomTextFormatter extends FormatterBase {
/**
* {@inheritdoc}
*/
public function viewElements(FieldItemListInterface $items, $langcode) {
$elements = [];
foreach ($items as $delta => $item) {
$elements[$delta] = [
'#type' => 'markup',
'#markup' => '<strong>' . $item->value . '</strong>',
];
}
return $elements;
}
}
This formatter wraps field values in <strong> tags. Site builders can choose this formatter when configuring how a field displays.
Using Field Formatters in Blocks
You can fetch field data in a block and apply custom formatting:
public function build() {
$node = \Drupal::routeMatch()->getParameter('node');
if ($node) {
return [
'#markup' => '<strong>' . $node->getTitle() . '</strong>',
];
}
return [];
}
Part 6: Block Caching Strategy
Caching is critical for Drupal performance. Each block can have its own cache settings.
Cache Contexts
Cache contexts define when a block's cache should be invalidated:
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
return ['user', 'url'];
}
Common cache contexts:
user: Cache per userurl: Cache per URLrole: Cache per user rolerequest_format: Cache per content type (JSON vs HTML)
Cache Tags
Cache tags allow invalidation of specific blocks:
/**
* {@inheritdoc}
*/
public function getCacheTags() {
return ['node_list'];
}
When you update a node, Drupal invalidates all blocks tagged with node_list.
Cache Max-Age
Control how long a block is cached:
/**
* {@inheritdoc}
*/
public function getCacheMaxAge() {
return 3600; // 1 hour
}
Part 7: Practical Examples
Example 1: Recent Posts Block
<?php
namespace Drupal\my_module\Plugin\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Database\Connection;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a 'Recent Posts' block.
*
* @Block(
* id = "recent_posts_block",
* admin_label = @Translation("Recent Posts"),
* category = @Translation("Custom")
* )
*/
class RecentPostsBlock extends BlockBase implements ContainerFactoryPluginInterface {
protected $database;
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
$instance = new static($configuration, $plugin_id, $plugin_definition);
$instance->database = $container->get('database');
return $instance;
}
public function defaultConfiguration() {
return ['num_posts' => 5];
}
public function blockForm($form, \Drupal\Core\Form\FormStateInterface $form_state) {
$form['num_posts'] = [
'#type' => 'number',
'#title' => $this->t('Number of posts'),
'#default_value' => $this->configuration['num_posts'],
];
return $form;
}
public function blockSubmit($form, \Drupal\Core\Form\FormStateInterface $form_state) {
$this->configuration['num_posts'] = $form_state->getValue('num_posts');
}
public function build() {
$query = $this->database->select('node_field_data', 'n')
->fields('n', ['nid', 'title'])
->condition('n.type', 'article')
->condition('n.status', 1)
->orderBy('n.created', 'DESC')
->range(0, $this->configuration['num_posts']);
$results = $query->execute()->fetchAll();
$items = [];
foreach ($results as $post) {
$items[] = [
'#type' => 'link',
'#title' => $post->title,
'#url' => \Drupal\Core\Url::fromRoute('entity.node.canonical', ['node' => $post->nid]),
];
}
return [
'#theme' => 'item_list',
'#items' => $items,
'#title' => $this->t('Recent Posts'),
];
}
public function getCacheTags() {
return ['node_list'];
}
public function getCacheContexts() {
return [];
}
}
Example 2: User Info Block
<?php
namespace Drupal\my_module\Plugin\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a 'User Info' block.
*
* @Block(
* id = "user_info_block",
* admin_label = @Translation("User Info"),
* category = @Translation("Custom")
* )
*/
class UserInfoBlock extends BlockBase implements ContainerFactoryPluginInterface {
protected $currentUser;
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
$instance = new static($configuration, $plugin_id, $plugin_definition);
$instance->currentUser = $container->get('current_user');
return $instance;
}
public function build() {
$user = $this->currentUser;
if ($user->isAuthenticated()) {
return [
'#markup' => $this->t('Welcome, @name!', ['@name' => $user->getDisplayName()]),
];
}
return [
'#markup' => $this->t('Please log in'),
];
}
public function getCacheContexts() {
return ['user'];
}
}
Part 8: Block Discovery: How Drupal Finds Your Blocks
This is crucial to understand. When you create a block plugin with an annotation, how does Drupal know about it?
The Annotation System
Drupal scans your module for plugins using annotations:
- When a module is installed, Drupal reads all plugin files
- It looks for class files with
@Blockannotations - It stores these in the plugin manager
- They become available in the Block Layout UI
The annotation is the only way Drupal discovers code-based block plugins.
/**
* @Block(
* id = "my_custom_block",
* admin_label = @Translation("My Custom Block"),
* category = @Translation("Custom")
* )
*/
Without this annotation, Drupal won't recognize your block class.
Is This from Symfony?
While Drupal uses Symfony components, the plugin annotation system is Drupal-specific. Drupal built its own plugin manager on top of Symfony. The @Block, @FieldFormatter, and other Drupal annotations are unique to Drupal and handled by Drupal's plugin system.
Part 9: Block Visibility and Context Awareness
Drupal 8+ introduced a powerful system for block visibility based on context.
Built-in Visibility Conditions
When you place a block in the UI, you can set conditions:
- Content type: Show only on specific content types
- User role: Show only to specific roles
- Request path: Show on specific URLs
- Language: Show for specific languages
- Node field value: Show based on a node's field values
Programmatic Visibility
/**
* {@inheritdoc}
*/
public function blockAccess(\Drupal\Core\Session\AccountInterface $account) {
// Only show to authenticated users
if (!$account->isAuthenticated()) {
return \Drupal\Core\Access\AccessResult::forbidden();
}
return \Drupal\Core\Access\AccessResult::allowed();
}
Part 10: Best Practices and Tips
For Site Builders
- Use custom blocks for unique, one-off content
- Use views for dynamic, filtered content
- Take advantage of visibility conditions
- Organize blocks by category
- Document what each block does
For Developers
- Keep blocks focused and single-purpose
- Use dependency injection for services
- Set appropriate cache contexts and tags
- Write configuration forms for flexibility
- Document your blocks with descriptions
- Test blocks on different page types
For All Users
- Understand the difference between blocks and regions
- Use the right tool: custom blocks, views, or code-based plugins
- Plan your layout before building
- Cache strategically for performance
- Keep blocks lightweight and fast-loading
Common Questions Answered
Q: Should I create a custom block or use a view block? A: Use custom blocks for static content, view blocks for dynamic content that needs filtering and sorting.
Q: How do I make a block appear only on the homepage? A: Use visibility rules: add a "Request path" condition and enter <front>.
Q: Can I use the same block in multiple places? A: Yes, blocks are reusable. You can place the same block in different regions.
Q: What's the performance impact of blocks? A: Minimal if cached properly. Set cache contexts and max-age appropriately.
Q: Can non-technical users manage blocks? A: Yes, site builders can create and place custom blocks via the UI.
Conclusion
Blocks are the foundation of Drupal's flexibility. Whether through configuration, views, custom code, or field formatters, blocks allow you to build complex, modular layouts without rebuilding your site. Understanding blocks at every level—from site builders to developers—is essential for mastering Drupal.
Start simple with custom blocks and views, then advance to code-based plugins as your needs grow. Drupal's block system scales with your project.
Happy blocking!
This guide covers Drupal 8, 9, 10, and beyond. The concepts remain consistent across modern Drupal versions.