Drupal: Views

This content was generated with the assistance of AI and should be verified against the official Drupal documentation before being relied on in production.

This sub-section’s Drupal bibliography lists the reference material consulted while preparing these pages.

Views is the part of Drupal that turns "show me a list of these things" into configuration instead of code. A view is a stored query plus the instructions for presenting its results: which table to read, which rows to keep, in what order, how many per page, which columns or rendered entities to output, and whether the output becomes a page at a URL, a block, an RSS feed, a JSON endpoint or an embedded fragment. Almost every list on a Drupal site is a view: the administrative content screen, the front page, the "recent comments" block, a search results page, a taxonomy term page and the JSON that a headless front end consumes.

This page was written against Drupal 11.4.x, with Drush 13 or 14 and PHP 8.3 or later, and the facts were checked against the official documentation on 2026-10-30. The running example is the Meridian Museum: the view upcoming_events (the next events, with an exposed filter), the view exhibitions_by_gallery (exhibitions in one room, driven by a contextual filter) and the view objects_on_display (items of the custom collection_object entity of the module meridian_collection, served as a page, a block and a JSON endpoint for the gallery kiosk). Drupal 12.0.0 is scheduled for the week of 2026-12-07 and the end of life of Drupal 10 for 2026-12-09, so both are described as upcoming.

What Views is and who uses it

The core Views module (views) is a general query and display engine: it builds a query for a data source, runs it, and formats the result. The core Views UI module (views_ui) is the administrative interface that creates and edits views at admin/structure/views. Four roles meet Views:

  • site builders create and tune views in the Views UI, which is where most of this page lives;

  • editors and visitors use the results, and the exposed filters and pagers a view offers;

  • administrators decide who may use the Views UI, who may see each view and how it is cached;

  • developers expose their own tables and entity types to Views, add plugins, alter queries and embed views in code.

The User Guide describes the same split: Views displays a view, and Views UI creates and edits it. The user guide’s chapter on listings walks through a content list, a duplicated view and a block display, and the developer overview on api.drupal.org lists the four ways a developer works with Views: providing plugins, providing data, implementing hooks and theming. Views UI is a development and site-building tool, so on a production site it is normally enabled only in local and staging environments, and views travel to production as exported configuration (see Exporting a view to configuration).

How a view is built, in order

A view is evaluated as a pipeline. Each stage narrows or reshapes the previous one, and the last stages decide what is cached.

flowchart LR B[(Base table<br/>node_field_data)] --> R[Relationships<br/>join other tables] R --> F[Filters and<br/>contextual filters] F --> S[Sorts] S --> P[Pager] P --> D[Display<br/>fields or rendered entity] D --> C{Cache plugin} C -- tag --> T[Cache tags<br/>node_list] C -- time --> M[Max age<br/>seconds]

Everything in the diagram is a handler or plugin configured on the view:

  • the base table is chosen once, when the view is created ("Show: Content" in the wizard) and cannot be changed afterwards;

  • relationships add joined tables, such as the author’s user account or the taxonomy term a field references;

  • filters and contextual filters become WHERE conditions, the first with fixed or visitor-chosen values and the second with values taken from the URL or another context;

  • sort criteria become ORDER BY, the pager becomes LIMIT and OFFSET;

  • the display and its format and row plugins decide the output, and the cache plugin decides how long the query results and the rendered output may be reused.

The parts of a view in the Views UI

The User Guide lists seven parts of a view in the administrative interface, and they map directly to the screen:

Part What it controls

Display

The kind of output: Page (content at a URL), Block, Feed, Attachment and others. A view can have several displays.

Format

How rows are arranged: unformatted list, HTML list, grid, table, and so on. Some formats output rendered entities or fields, while a table requires fields.

Fields

The columns to output when the row style is "Fields".

Filter criteria

Which rows are included (publication status, content type, field values). Filters can be exposed to visitors.

Sort criteria

The order of the results, by any available field.

Contextual filters

Filters whose values come from the context of the display, such as the URL, the current date or time, or another detectable value.

Relationships

Connections from the base content to other entities, such as the author’s user account, so their fields become available.

Beyond these, the Advanced column holds the header and footer, the "No results behavior", the pager, exposed form settings, query settings, caching and access. A view always has a hidden-by-default Default (also called Master) display that holds the settings shared by the others. The Views UI hides it unless "Always show the default display" is enabled at admin/structure/views/settings. Each setting in the UI carries a For selector: "All displays" writes to the Default display and is inherited, while "This page (override)" or "This block (override)" stores the value on that one display only. The User Guide’s block-display walk-through uses the override choice for every step so that the existing page display stays unchanged. Forgetting this selector is the most common reason that a block unexpectedly changes the page, or the reverse.

Creating a view with the wizard

Open Structure > Views > Add view (admin/structure/views/add). The wizard asks for:

  • View name (the label, from which the machine name such as upcoming_events is derived);

  • Show: the base table, for example Content, Taxonomy terms, Users, Comments, Files, or a custom entity type;

  • of type and sorted by for content;

  • whether to create a page (title, path, display format, items to display, pager, menu link) and a block.

The User Guide’s example creates a Vendors page with the display format Table, ten items and a menu link in the main navigation, then uses "Save and edit" to add the fields in a second step. The wizard is only a shortcut for the settings that the full editor exposes, so everything below applies equally to a view created by the wizard and to one built from scratch.

The base table is the one decision the wizard cannot reverse. The Views documentation states that the base table is chosen when the view is created, from options such as Comments, Content and Taxonomy terms, and cannot be changed later. When the wrong one was chosen, duplicate the view only to copy its display settings and rebuild it on the correct base.

Base tables and what "Show" really means

A base table is a table that Views can start a query from. Core describes them through hook_views_data() (or, for entity types, through the entity’s views data handler), and every base table lists a primary key (base_field), a human title and a description. Typical base tables:

"Show" in the wizard Table in the query Notes

Content

node_field_data

One row per node translation. A multilingual site can show each translation as its own row, so add a "Content: Translation language" filter or the rendering-language setting where that matters.

Taxonomy terms

taxonomy_term_field_data

Used for glossary pages and term listings. See Taxonomy.

Users

users_field_data

Directories of members or staff.

Media

media_field_data

Galleries of images and documents. See Media and the Media Library.

Files

file_managed

File listings, with "File usage" as a relationship.

Custom entity (collection_object)

collection_object_field_data

Available only when the entity type declares a views data handler.

A view therefore always reads one kind of thing. To show data from a second kind, such as the gallery name next to an exhibition, a view uses a relationship or a rendered-entity row, not a second base table.

Exposing a custom entity type to Views

Content entity types get their Views integration from a views data handler. The api.drupal.org overview recommends implementing \Drupal\views\EntityViewsDataInterface and referencing it in the entity type’s views_data handler, and notes that extending \Drupal\views\EntityViewsData generates most of the integration automatically. For the Meridian Museum’s collection_object entity, the handler is a single line in the entity type attribute:

<?php

declare(strict_types=1);

namespace Drupal\meridian_collection\Entity;

use Drupal\Core\Entity\Attribute\ContentEntityType;
use Drupal\Core\Entity\ContentEntityBase;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\views\EntityViewsData;

#[ContentEntityType(
  id: 'collection_object',
  label: new TranslatableMarkup('Collection object'),
  label_collection: new TranslatableMarkup('Collection objects'),
  entity_keys: [
    'id' => 'id',
    'label' => 'title',
    'uuid' => 'uuid',
    'published' => 'status',
  ],
  handlers: [
    // Generates the Views data for the base table and every base field.
    'views_data' => EntityViewsData::class,
  ],
  base_table: 'collection_object',
  data_table: 'collection_object_field_data',
)]
class CollectionObject extends ContentEntityBase {
  // Base field definitions (accession_number, period, material, dimensions,
  // on_display) are declared in baseFieldDefinitions().
}

Base fields declared with setDisplayConfigurable() and a proper field type become fields, filters and sorts automatically, with the right handler for the type (a boolean field gets a boolean filter, a string a string filter). The full entity type is built in Custom entity types. Configurable (bundle) fields added through the field UI are exposed the same way, each as its own <entity>__<field> table.

For data that is not an entity, hook_views_data() returns an array whose outer keys are Views table names, usually matching the hook_schema() table names, each describing the table, its joins and its fields. The api.drupal.org documentation lists hook_views_data() and hook_views_data_alter() as the two hooks for providing and changing data. Core’s own implementations are now OOP methods in classes under src/Hook/ (for example the dblog and history modules), so a module can use the same style as described in Hooks and OOP hooks.

Displays

A display is a way of exposing the view’s configuration somewhere. One view holds one Default display plus any number of additional displays that override parts of it. The core display plugins are listed in the Views API documentation; all are plugins with a ViewsDisplay attribute in current 11.x core.

Display Plugin id Use

Page

page

A route at a path, with an optional menu link or tab and a title. Path arguments (events/%) feed contextual filters.

Block

block

A block that appears in Structure > Block layout (see Blocks and block layout), with its own block name and category. A block display can take contextual filters from the page it is placed on.

Feed

feed

RSS or Atom output at a path; uses the Feed style and a row plugin such as Content or Fields. It can be attached to other displays as a feed icon.

REST export

rest_export

A data endpoint from the rest module. The default style is serializer, the default row type data_entity, the default format JSON. Routes are restricted to GET. See REST and serialization.

Embed

embed

A display that can be embedded using the Views API (for example from a render array). It allows attachment displays to be attached to it and marks its output as embedded.

Attachment

attachment

Extra output placed before or after another display: a summary table above a list, a "featured" row before the results. Settings include the attachment position, whether to inherit contextual filters, exposed filters and the pager.

Entity Reference

entity_reference

Supplies the options of an entity reference field that uses the "Views: Filter by an entity reference view" selection method.

Two practical consequences. First, REST export, Feed and Embed displays return data or fragments, not full pages, so they are tested with curl or a render array rather than with the browser’s rendered page. Second, JSON:API, not a REST export, is the zero-configuration way to expose standard content (see JSON:API); a REST export earns its place when the response must be shaped by a view (a filtered list with exposed filters, or a computed selection such as "objects currently on display").

REST export authentication is a setting of the display. The auth option is empty by default, in which case the default (cookie based) authentication applies. The settings form warns that the authentication system falls back to the anonymous user when authentication fails, so always set the view’s access restrictions separately and never rely on the authentication provider alone to hide data. Authentication for the Next.js kiosk is covered in Authentication and OAuth.

Format and row: fields versus rendered entities

The format (style plugin) decides the overall layout, and the row plugin decides what one item looks like. The two common row choices are very different:

Show: Fields Show: Content (rendered entity)

Output

Exactly the fields you add, in the order and with the formatters you choose in the view.

The entity rendered in a chosen view mode, such as teaser, using that view mode’s field formatters and its template.

Where you configure it

In the view (field settings, rewrite results, labels).

In Manage display of the entity type (see Display modes and image styles).

Required for

Table format, aggregation, sortable columns, rewritten output, data export fields.

Reusing the same teaser on a page, a block and a search result so that it looks the same everywhere.

Cost

One query for the base rows. Each field loads the entity unless Views can read the value straight from the joined table.

Loads each entity and renders it. Entity render caches are used, so repeated lists are cheap once warm.

For the Exhibitions by gallery view the best row is the rendered entity in the teaser view mode, which already contains the exhibition-teaser component of the meridian theme. For the Objects on display table, fields are the better choice because the page is a sortable table of accession numbers, periods and materials. Mixing the two in one view is possible per display: a block can show rendered teasers while the page shows a table.

Field settings worth knowing:

  • Rewrite results lets a field be output through a small Twig-like template using replacement tokens (for example the title field’s token) of fields placed earlier in the view; the token appears only for fields that come before the one being rewritten, so rearranging the field order changes what is available.

  • Multiple field settings controls how a multi-value field is output. With "Display all values in the same row" enabled (the default), the row stays single; without it, each value produces a duplicate row for the same entity, which is the usual cause of "my events appear three times".

  • Hide if empty, No results text and Link to content (link the field to the entity) are set per field.

  • Each field can carry a label (turn it off with "Create a label" unchecked, as the User Guide does for a title and image list).

The Views documentation page "Rewrite the output of a view field" and "Customize the output style of a view field or list" cover the details of the UI; theming of the resulting HTML follows the normal template suggestion rules (see Template suggestions and preprocess).

Filters, including exposed filters

A filter restricts the rows. Filters always apply: a view with no published-status filter shows unpublished content to anyone who may view the view and who passes access checks on the individual entities, so the "Published (= Yes)" filter that the wizard adds is part of the design, not decoration. Common filters for the Meridian Museum:

  • Content: Published (= Yes), or "Published status or admin user" so that editors can still preview;

  • Content: Content type (= Event);

  • a date filter, for example field_event_dates end value greater than or equal to the offset now;

  • a taxonomy term filter on field_topic with the "Dropdown" selection type or an autocomplete;

  • a boolean filter, such as on_display equal to true for objects on display.

Filter groups combine conditions with AND or OR. Grouping several conditions into an OR group is how "exhibitions that are either current or have the 'permanent' flag" is expressed.

Exposing a filter

Any filter can be exposed to visitors: in the Views UI open the filter, choose More, and enable "Expose this filter to visitors". The documentation distinguishes two modes. A single filter shows one control with a label and description. Grouped filters show a pre-built choice list in which each row defines a label, an operator and a value, with options to remember the visitor’s selection, allow multiple selections and render the widget as radio buttons or a select list.

An exposed filter has an identifier (the query-string key, topic in ?topic=12), a required checkbox, and for text filters an operator choice. Several exposed filters plus the pager and exposed sorts make up the exposed form. Its settings are on the Advanced column under "Exposed form style":

  • Basic renders the form with a submit button;

  • Input required shows nothing until the visitor submits at least one value, which is the right choice for a very large collection_object search page;

  • the submit and reset button labels, the "Expose sort order" and "Expose items per page" options, and whether the form appears above or below the results are settings of the form style.

The form submits with GET, so exposed filters are part of the URL and are bookmarkable. That also means the query string is part of the page cache key: Views adds the url.query_args cache context for exposed input, and the dynamic page cache stores one variation per distinct combination. A site that exposes a free-text filter and gets thousands of distinct searches fills its page cache with variations, so exposed free-text forms usually belong on pages with the "Input required" style and the Search API (see Search and Search API) rather than on a plain SQL view.

The Upcoming events view

The upcoming_events view lists events whose end date has not passed, soonest first, with an exposed filter on the topic vocabulary. The event content type has a field_event_dates date range field (see Content types and fields). Configuration, written step by step in the Views UI, is:

  • Show: Content of type Event, sorted by Title, create a page at events and a block, both with a mini pager;

  • Filter criteria: Published (= Yes); Content type (= Event); field_event_dates (field_event_dates:end_value) greater than or equal to the type "An offset from the current time" with value now;

  • Exposed filter: Content: Topic (field_topic), identifier topic, "Dropdown";

  • Sort criteria: field_event_dates (field_event_dates:value) ascending;

  • Pager: Paged, mini, 10 items;

  • Row: Content, view mode teaser;

  • Caching: Time-based, one hour (see Caching);

  • Access: Permission, "View published content".

The relative filter "now" is evaluated when the query runs, so a cache that never expires would keep showing a finished event for as long as no node is saved. The cache section explains why this view uses time-based caching.

Contextual filters

A contextual filter has the same effect as a filter, but its value is not configured: it is read from the display’s context. The User Guide describes contextual filters as taking their values from the URL, the date or time, or another detectable value. In the page display the path exhibitions/gallery/% passes the third URL segment to the first contextual filter as argument 1. In a block display the arguments come from the page it is placed on, normally the route.

When adding a contextual filter, the "When the filter value is not in the URL" section is the part that matters. The documentation lists the choices:

  • Display all results for the specified field;

  • Provide default value;

  • Hide view;

  • Display a summary;

  • Display contents of "No Results Found";

  • Display "Access Denied".

"Provide default value" offers several sources: Content ID from URL, a fixed value, a query parameter, a raw value from the URL, Taxonomy term ID from URL, User ID from logged in user and User ID from route context. The "When the filter value is in the URL or a default is provided" section lets the view override the title (using a placeholder such as %1) and specify validation criteria. A relationship may be needed when the context value belongs to another entity than the base table.

Defaults that bite:

  • "Display all results for the specified field" is the default if the argument is missing, which is fine on a page but surprising on a block that is placed on every page: the block then lists everything. Prefer "Hide view" for contextual blocks.

  • Validation should be turned on. Without validation, an argument such as exhibitions/gallery/999999 simply returns an empty list; with "Taxonomy term" validation restricted to the gallery vocabulary, an unknown or foreign term is rejected, and the "action to take if the filter does not validate" can be a 404 page.

  • "Allow multiple values" lets + and a comma combine values, so 1+2 means "gallery 1 or gallery 2" and 1,2 means "and". Do not enable it when the argument comes from a user-controlled path unless a bounded number of values makes sense.

The view exhibitions_by_gallery lists exhibitions in one gallery. Its settings:

  • Show: Content of type Exhibition, sorted by start date;

  • Relationship: Content: Gallery (the term referenced by field_gallery), so the gallery name can be displayed;

  • Contextual filter: Content: Gallery (field_gallery), default "Taxonomy term ID from URL" with the gallery vocabulary, validation "Taxonomy term" restricted to gallery, and "Hide view" for an unmatched value;

  • Title override: Exhibitions in %1;

  • Page display at exhibitions/gallery/%, Block display placed on the taxonomy term page (the term page route supplies the term ID, so the same view works as the content of the term page without a hand-made path).

The block display lets editors see the list in the sidebar of any gallery term page, while the page display allows a link from the main menu. The same contextual filter feeds both through the shared Default display.

Relationships

A relationship joins a second table to the base table so that its fields, filters and sorts become available. The documentation example adds "Content: Content author" to a content view; the user name field then appears as "User: Name". The "Require this relationship" option turns the join from a left join to an inner join, so that rows without the related entity disappear from the view. The documentation warns that content without the relationship is hidden when this option is enabled.

Typical relationships on the Meridian Museum:

  • Content: Content author (to the user table);

  • Content: Gallery (field_gallery, to the taxonomy_term_field_data table);

  • Content: Hero image (a media entity reference, then Media: Image to get the file);

  • Content: Related objects (a reference to collection_object, which allows filtering exhibitions by an object’s period).

Relationships are the main source of unexpected row counts. A relationship to a multi-value reference returns one row per referenced entity, so a node with three related objects appears three times. Two remedies exist: enable Query settings > Distinct (adds DISTINCT, which can break sorting and aggregation) or use aggregation with a group column. When every row is accidentally duplicated, check that no relationship is set to return more rows than the display needs.

Sort criteria

Sorts are applied in the order listed, so a sort list reads from the most to the least significant key. Rearrange the sorts with the drop-down next to the Sort criteria heading. Some sorts have special behaviour:

  • Content: Sticky or Promoted are simple flag sorts that put pinned items first;

  • Global: Random reorders the results on each request and defeats result caching (each request has a different order), so a random display must use a cache maximum age short enough to make the randomness visible, or none;

  • Date granularity on date sorts ("Second", "Minute", "Hour", "Day") controls whether two events on the same day tie.

Sorts can also be exposed: the visitor then chooses the order from the exposed form. Table formats can additionally make column headings clickable to sort.

Pagers

The pager is part of the Advanced column. The core pager plugins are:

  • Display all items (no limit; dangerous on a large table);

  • Display a specified number of items (a plain limit, with an optional offset; no pager links);

  • Paged output, mini (previous and next only; it does not run the count query that the full pager needs);

  • Paged output, full (page numbers; needs a COUNT query for the total).

The mini pager is cheaper on big tables. The full pager shows total counts but must count all rows matching the filters, which for the collection_object table with exposed filters can cost more than the page itself. A view can expose "Items per page" and the offset. The pager uses a numbered page query parameter, so every page is a separate cache variation by url.query_args. The User Guide’s block display uses "Display a specified number of items" with a limit of 5 to show the newest recipes in a sidebar.

Aggregation

By default each row of a view is one entity (or one row of a table). Enabling Advanced > Use aggregation changes the query into a GROUP BY query, and each field, filter, sort and contextual filter gets an "Aggregation settings" link that selects how its values combine: Group results together, Count, Count DISTINCT, Sum, Average, Minimum, Maximum and similar. For the Meridian Museum, a statistics page "Objects by period" uses:

  • Show: Collection object;

  • Field: Period, aggregation type "Group results together";

  • Field: ID, aggregation type "Count";

  • Sort: Period, ascending.

The result is one row per period with the number of objects. Aggregation requires the Fields row style (rendered entities cannot be grouped), is incompatible with several features of the full pager, and interacts with DISTINCT and with relationships, because every field that is not aggregated must be part of the GROUP BY. Two pitfalls: a field formatter that needs the entity (an entity reference label, for example) loads one entity for each result row, and an aggregated view still needs the correct cache tags, because the aggregate changes whenever any object changes.

Caching

Views has two cache layers, both configured under Advanced > Caching: the query results (the raw rows) and the rendered output (the HTML of the display). Core ships three cache plugins, all attribute-based plugins in the Drupal\views\Plugin\views\cache namespace and extending CachePluginBase:

Plugin Id Behaviour

None

none

No Views-specific caching. The render and page caches still apply to the surrounding page.

Tag based

tag

"Tag based caching of data. Caches will persist until any related cache tags are invalidated." The default maximum age is permanent, so the cache entry is valid until a tag is invalidated.

Time-based

time

"Simple time-based caching of data." Two options, results_lifespan and output_lifespan, both default to 3600 seconds; the select list offers Never cache, 1 minute, 5 minutes, 30 minutes, 1 hour, 6 hours, 6 days and Custom.

Tag-based caching

Tag-based caching relies on the cache tags that the view declares. A view that lists nodes carries the tag node_list, plus the per-entity tags of each node it renders, and a list of entities of a type is invalidated whenever one is added, changed or deleted. That is exactly right for "Exhibitions by gallery": a new exhibition or an edited one clears the affected views at once, and nothing else does. Contextual filter values and the pager add the cache contexts url and url.query_args, so each combination is cached separately. The cache metadata of a display is stored in the view configuration under cache_metadata and recalculated when the view is saved. For the underlying model see Render API and cache metadata and Performance and caching.

Tag-based caching is wrong for data that changes without a tag being invalidated, which is exactly the case for relative dates. In upcoming_events the filter "end date >= now" changes meaning as time passes, but nobody saves a node when an event finishes, so a tag-only cache would keep showing the event until another event is edited. Use time-based caching for such a view, with a lifespan short enough for the editorial requirement (an hour for events that end on the hour).

Time-based caching

Time-based caching sets a maximum age. It is simpler than tags but means editors wait for a change to appear, unless a tag invalidates it earlier: the rendered output cache is also tagged, so a node edit still invalidates it through the dynamic page cache. The practical recommendation is to combine them: time-based for views with a "now" component, tag-based for everything else. A custom cache plugin can combine both; the contributed Views Custom Cache Tag module adds per-view tags and optional time-based invalidation for relative date filters.

The cacheExpire() method in the core tag and time plugins is deprecated in Drupal 11.4.0 and scheduled for removal in 13.0.0 with no replacement, so a custom cache plugin that extends one of them and calls it should be updated.

What not to cache

Views that depend on the current user (for example "My unread news" with the contextual default "User ID from logged in user") need the user cache context. Views declares it when the user default is used. A custom plugin that varies by something else must say so in its cache metadata, or one user’s list will be served to another.

Access

The Access setting (Advanced > Access) decides who may see a display:

  • None (unrestricted; use only for views that cannot expose private data);

  • Permission (a single permission such as "View published content");

  • Role (one or more roles).

Access to the display does not replace access to the rows. For a node base table Views adds the node_access query tag when the query runs, so queries respect the content access system. The query setting Disable SQL rewriting turns this off and is an administrative bypass that exposes unpublished and restricted content; leave it off except for views that only administrators can reach. Other points:

  • The "Published (= Yes)" filter is the only thing that hides unpublished content in the view’s own query. The query access system also hides it from users without permission; do not depend on only one of the two.

  • An entity of a custom type (collection_object) needs its own query access rules in its entity type, because there is no automatic access rewrite for arbitrary tables. See Access control and permissions in code.

  • A REST export view needs a permission or role plus an authentication provider. The default authentication is cookie based; for tokens use the OAuth setup in Authentication and OAuth.

  • The permission "Administer views" is a high-privilege permission: a person who can edit views can read anything the site stores by writing a view over it. Grant it only to administrators.

Review the permissions of the Views modules in Users, roles and permissions.

The header and footer areas hold text, a result summary ("Displaying 1 - 10 of 124"), links, an embedded view or the exposed form. The No results behavior area shows a message when the query has no rows, which for an events listing is a friendly "No events are scheduled" instead of a blank region. Text in these areas uses a text format, so it is subject to the format’s HTML restrictions (see Text formats and CKEditor 5).

Attachments

An attachment display adds extra content to another display. Typical use: an attachment "Featured exhibition" with one item before the list. Its important settings are the attachment position (before or after), the displays to attach to, and the three inheritance checkboxes (contextual filters, exposed filters and the pager). If it inherits exposed filters, the attachment re-runs with the visitor’s choices; if not, it is independent of the form. An attachment never has its own path or block, so it is tested through the display it is attached to.

Block displays in detail

A block display publishes the view as a block plugin. Settings include the block name (what the Block layout UI shows), the block category, "Allow settings" (lets the placer override items per page in the block configuration) and a caching option inherited from the view. After saving the view, place it from Structure > Block layout (see Blocks and block layout). Block displays are the cheapest way to produce "On this day" or "More from this gallery" modules, but they have two traps: a block placed on many pages runs once per page unless the cache is warm, and contextual filters of a block use the route of the page, so a block placed on a page without the expected route parameter falls to its "value not available" action.

The Objects on display view

The view objects_on_display has three displays on the collection_object base table:

Display Path or placement Purpose

Page

/collection/on-display

A table of accession number, title, period, material, with exposed filters on period and material and a full pager.

Block

Region Sidebar second on gallery pages

Five objects, rendered as the teaser view mode, with a link to the page.

REST export

/api/kiosk/objects

JSON of accession number, title and gallery for the Next.js kiosk, authenticated with OAuth.

All three share the Default display’s filter "On display (= True)" and Published status. The REST export is the only display that sets its own style (serializer) and authentication, as in the exported configuration in the next section.

The headless kiosk is described in Decoupled Drupal with Next.js. When the output of a list should be generic content, JSON:API is better; when it should be a specific, filtered projection, a view is the cleaner tool.

Exporting a view to configuration

A view is a configuration entity named views.view.<machine_name>, so it is exported like any other configuration (see Configuration management and Configuration API and schema). Export with Drush and commit the file:

# Export all configuration to the sync directory (config/sync)
ddev drush config:export -y

# Look at one view as YAML
ddev drush config:get views.view.upcoming_events

# Import on the target environment as part of a deploy
ddev drush deploy

Drush also ships view-specific commands: views:list (list views), views:enable and views:disable (change a view’s status), views:execute (run a view and print the results) and views:dev (set several Views settings to developer-friendly values). On a staging site ddev drush views:execute upcoming_events page_1 is a quick check that the query returns rows without a browser.

The exported views.view.upcoming_events.yml has the structure below. Only the part relevant to the page display is shown. Note the dependencies block (Views records the modules, content types and fields it needs, so the view imports only where these exist) and the cache plugin settings.

langcode: en
status: true
dependencies:
  config:
    - field.storage.node.field_event_dates
    - field.storage.node.field_topic
    - node.type.event
  module:
    - datetime_range
    - node
    - taxonomy
    - user
id: upcoming_events
label: 'Upcoming events'
module: views
description: ''
tag: ''
base_table: node_field_data
base_field: nid
display:
  default:
    id: default
    display_title: Default
    display_plugin: default
    position: 0
    display_options:
      title: 'Upcoming events'
      fields: {  }
      pager:
        type: mini
        options:
          offset: 0
          items_per_page: 10
      exposed_form:
        type: basic
        options:
          submit_button: Filter
          reset_button: true
      access:
        type: perm
        options:
          perm: 'access content'
      cache:
        type: time
        options:
          results_lifespan: 3600
          results_lifespan_custom: 0
          output_lifespan: 3600
          output_lifespan_custom: 0
      query:
        type: views_query
        options:
          disable_sql_rewrite: false
          distinct: false
      style:
        type: default
      row:
        type: 'entity:node'
        options:
          view_mode: teaser
      sorts:
        field_event_dates_value:
          id: field_event_dates_value
          table: node__field_event_dates
          field: field_event_dates_value
          plugin_id: datetime
          order: ASC
      filters:
        status:
          id: status
          table: node_field_data
          field: status
          plugin_id: boolean
          value: '1'
          group: 1
        type:
          id: type
          table: node_field_data
          field: type
          plugin_id: bundle
          value:
            event: event
          group: 1
        field_event_dates_end_value:
          id: field_event_dates_end_value
          table: node__field_event_dates
          field: field_event_dates_end_value
          plugin_id: datetime
          operator: '>='
          value:
            type: offset
            value: now
          group: 1
        field_topic_target_id:
          id: field_topic_target_id
          table: node__field_topic
          field: field_topic_target_id
          plugin_id: taxonomy_index_tid
          exposed: true
          expose:
            identifier: topic
            label: Topic
          group: 1
    cache_metadata:
      max-age: 3600
      contexts:
        - 'languages:language_content'
        - url.query_args
        - user.permissions
      tags: {  }
  page_1:
    id: page_1
    display_title: Page
    display_plugin: page
    position: 1
    display_options:
      path: events
      menu:
        type: normal
        title: Events
        menu_name: main

The meridian_events recipe applies this view to a fresh site. It lists the modules to install and imports the exported configuration shipped inside the recipe directory (see Recipes):

name: 'Meridian events'
description: 'Event content type with an Upcoming events view.'
type: 'Content type'
install:
  - node
  - datetime_range
  - taxonomy
  - views
config:
  import:
    node:
      - node.type.event
    views:
      - views.view.upcoming_events

Views UI is itself a module whose configuration should not reach production unless site builders must edit there. A common pattern is to enable views_ui locally, export, commit, and keep views_ui out of the production module list through a configuration split (see Deployment and CI/CD).

Rendering a view from code

A view can be embedded in a render array with the view render element. Its properties are #name (the machine name), #display_id (default default), #arguments (default empty array), #embed (default TRUE, which calls preview(); FALSE runs executeDisplay() and adds contextual links) and, optionally, #view to pass a loaded view object.

<?php

declare(strict_types=1);

namespace Drupal\meridian_collection\Controller;

use Drupal\Core\Controller\ControllerBase;
use Drupal\views\Views;

/**
 * Shows the Objects on display view inside a custom page.
 */
final class OnDisplayController extends ControllerBase {

  /**
   * Builds a page that embeds the block display with one argument.
   */
  public function page(string $gallery): array {
    return [
      '#type' => 'view',
      '#name' => 'objects_on_display',
      '#display_id' => 'block_1',
      '#arguments' => [$gallery],
      '#embed' => TRUE,
    ];
  }

  /**
   * Loads the view object to read or change it before execution.
   */
  public function count(): int {
    $view = Views::getView('objects_on_display');
    if ($view === NULL) {
      return 0;
    }
    $view->setDisplay('page_1');
    $view->execute();
    return count($view->result);
  }

}

The old helper views_embed_view() embeds a view from a PHP snippet, but it is deprecated as of Drupal 11.4.0 and will be removed in Drupal 13.0.0; its documentation recommends '#type' => 'view' render elements instead. Older books and tutorials that call views_embed_view('name', 'block_1') or views_get_view_result() in a preprocess function should be updated.

Altering a view

Two hooks cover most needs. hook_views_query_alter(ViewExecutable $view, QueryPluginBase $query) runs just before a query is executed; core’s own implementations include the workspaces module and the content moderation test module. hook_views_data_alter() changes the data that other modules provide. In an OOP module the implementation sits in a class under src/Hook with the #[Hook] attribute, as described in Hooks and OOP hooks:

<?php

declare(strict_types=1);

namespace Drupal\meridian_collection\Hook;

use Drupal\Core\Hook\Attribute\Hook;
use Drupal\views\Plugin\views\query\QueryPluginBase;
use Drupal\views\ViewExecutable;

/**
 * Views hooks for the Meridian collection.
 */
final class MeridianViewsHooks {

  /**
   * Hides retired objects from the kiosk endpoint.
   */
  #[Hook('views_query_alter')]
  public function queryAlter(ViewExecutable $view, QueryPluginBase $query): void {
    if ($view->id() !== 'objects_on_display' || $view->current_display !== 'rest_export_1') {
      return;
    }
    // The query is a Sql query; add a condition on the base table.
    $query->addWhere(0, 'collection_object_field_data.retired', 0, '=');
  }

}

Most customisations should be done with the UI instead. Custom query code hides logic from site builders, bypasses the exported configuration and has to be kept in step with schema changes. When the same filter is useful everywhere, a custom filter plugin (a class with the ViewsFilter attribute in src/Plugin/views/filter) is the maintainable form.

Debugging and troubleshooting

  • Show the SQL query. At admin/structure/views/settings, "Live Preview Settings" has "Show the SQL query", which prints the generated queries in the Preview. At admin/structure/views/settings/advanced, "Add Views signature to all SQL queries" appends the view and display names to each SELECT so that they are findable in database server logs; the documentation says to use it only when troubleshooting.

  • Preview and autopreview. The preview under the editor runs the display with its exposed filters and arguments (a field for arguments, separated by /).

  • Duplicate rows: check relationships to multi-value references, the field setting "Display all values in the same row", and "Distinct".

  • Missing rows: a "Require this relationship" setting, an inner join with a missing value, translation filters, or a published filter.

  • Stale output: the cache plugin. The documentation notes that Views caches data for tables, modules and available views, and that disabling caching can seriously harm performance, so disable it for debugging only (the "Never cache" choice of the time-based plugin, or drush views:dev).

  • Overridden settings: open every display and read the "For" selector of the setting that looks wrong; the value on the Default display and the display’s override can differ.

  • Broken views after a module is removed: the view’s dependencies block lists the module; uninstalling a module that provides a field used by a view is blocked by the dependency until the view is fixed or deleted.

Version notes and findings

  • The Views and Views UI modules are in core in Drupal 11.4. The Standard install profile no longer creates the Article and Page content types from 11.4, so a tutorial that starts from "the Article content type" or "Frontpage view of Articles" needs you to create the content types first; a view that filters by a type that does not exist imports with a missing dependency.

  • Plugins used by Views (cache, display and others) use PHP attributes in current 11.x core (for example #[ViewsCache] and #[ViewsDisplay]); annotations are on a deprecation path (see Plugin API and attributes).

  • views_embed_view() is deprecated as of 11.4.0, for removal in 13.0.0 (use the view render element).

  • cacheExpire() in the core tag and time cache plugins is deprecated as of 11.4.0, for removal in 13.0.0.

  • Views that rely on modules removed from core lose their data. Statistics ("Content: Total views", "Popular content" blocks) was removed in 11.0, as were Book and Forum, so views built on their fields need the contrib versions installed before the upgrade, not uninstalled. Drupal 12 moves History ("Has new content", "Last viewed" fields) and Search to contrib, so views using their handlers need the contrib module. See Removed core modules and replacements and Preparing for Drupal 12.

  • Drupal 12 removes Claro, Olivero and Stable 9 from core. The Views UI is themed through the admin theme, so check how the Views edit screen looks under Default Admin (based on Gin) when it becomes the admin theme.

  • Entity queries must call accessCheck() explicitly. A view does the equivalent through its access plugin and query tags, so replacing a view with a hand-written entity query in a custom controller means adding the access check by hand.

Corrections to older books and tutorials

Older books such as Drupal 10 Masterclass (2023) and Drupal 10 Development Cookbook (2023) teach Views from the Drupal 10.0 and 10.1 era. The Views UI concepts (base table, display, fields, filters, contextual filters, relationships, caching) still apply, and the User Guide and the Views documentation pages remain the reference. What changed:

  • Examples that embed a view with views_embed_view() should use the view render element; the function is deprecated in 11.4.

  • Custom Views cache, display, filter or field plugins shown with @ViewsCache, @ViewsDisplay or @ViewsFilter annotations now use the matching attribute; annotations are deprecated.

  • Views hooks shown only as .module or .views.inc functions can be written as OOP hook methods with #[Hook] on autowired classes in src/Hook since 11.1; core already does this for its own hook_views_data() implementations.

  • Views built on Statistics, Book, Forum, Tour and Actions UI data refer to modules removed from core in 11.0; pages about "Popular content" blocks need the contrib replacement.

  • Screens that show Classy or Seven markup for the Views UI, or CKEditor 4 in header and footer text, belong to older releases: Views text areas use CKEditor 5, and the administration theme is Claro (Default Admin in Drupal 12).

  • Tutorials that begin with the Article content type and the Tags vocabulary assume a Standard install from before 11.4.

Common errors and pitfalls

  • Choosing the wrong base table; it cannot be changed. Rebuild the view.

  • Editing the wrong scope: using "All displays" when "This page (override)" was intended.

  • A relationship to a multi-value field producing duplicate rows.

  • Forgetting the Published filter; or enabling "Disable SQL rewriting" and exposing unpublished content.

  • A "Hide view" or "Display all results" choice on a contextual block that renders on every page.

  • A tag-based cache on a filter that depends on now; use time-based caching.

  • An exposed free-text filter with the full pager on a large table, which runs a costly count query for each distinct search.

  • Exposed filters that require GET parameters ignoring the clean-URL path; test with a real query string.

  • A rendered-entity row with aggregation (not possible; use fields).

  • A REST export with no permission or authentication restriction; the anonymous fallback reveals what the view returns.

  • Editing a view on production through Views UI; the next configuration import overwrites it.