---
name: drupal-views
description: 'Build and theme Drupal Views. Use when: creating views configurations, writing Views Twig templates, theming views containers/rows/fields, implementing views preprocess functions, adding exposed filters, creating attachment displays, contextual filters, building Views-based listings or feeds, debugging views output.'
argument-hint: 'View machine name, display type (page/block/embed), and what it lists (content type, taxonomy, etc.)'
---

# Drupal Views — Square360 Conventions

> **Also load:** [drupal-theme](../drupal-theme/SKILL.md) for the full s360-base-theme file structure.
> **Also load:** [bem-scss](../bem-scss/SKILL.md) for class naming when theming view output.

## When to Use
- Creating or modifying Views configuration
- Writing Views Twig templates
- Adding preprocess functions for Views
- Theming exposed filters or pagers
- Making a view output match a component design from Storybook

---

## Views Template Suggestion Hierarchy

Views uses a layered template system. From most to least specific:

### View container (the outer wrapper)
```
views-view--{view-name}--{display-id}.html.twig
views-view--{view-name}.html.twig
views-view.html.twig
```

### Unformatted list rows
```
views-view-unformatted--{view-name}--{display-id}.html.twig
views-view-unformatted--{view-name}.html.twig
views-view-unformatted.html.twig
```

### Fields row
```
views-view-fields--{view-name}--{display-id}.html.twig
views-view-fields.html.twig
```

### Individual field
```
views-view-field--{view-name}--{display-id}--{field-name}.html.twig
views-view-field--{view-name}--{field-name}.html.twig
views-view-field--{field-name}.html.twig
views-view-field.html.twig
```

**Location in theme:** `web/themes/custom/{theme}/templates/views/`

---

## Discovering Template Suggestions

Enable Twig debug mode, then inspect the HTML source for `<!-- FILE NAME SUGGESTIONS -->` comments:

```html
<!-- FILE NAME SUGGESTIONS:
   * views-view--my-view--page-1.html.twig
   x views-view--my-view.html.twig
   * views-view.html.twig
-->
```

The `x` marks the template currently being used. Copy it then rename to the most specific suggestion.

---

## Theming the View Container

`views-view--{view-name}.html.twig` controls the overall view wrapper:

```twig
{#
  Variables:
  - rows: Rendered rows
  - header: View header blocks
  - footer: View footer blocks
  - exposed: Exposed filter form
  - pager: Pager links
  - empty: Empty text (no results)
#}
<div {{ attributes.addClass('view-' ~ view.id | replace({'_': '-'})) }}>
  {% if exposed %}
    <div class="view-{{ view.id | replace({'_': '-'}) }}__filters">
      {{ exposed }}
    </div>
  {% endif %}

  {% if header %}
    <div class="view-{{ view.id | replace({'_': '-'}) }}__header">
      {{ header }}
    </div>
  {% endif %}

  {% if rows %}
    <div class="view-{{ view.id | replace({'_': '-'}) }}__results">
      {{ rows }}
    </div>
  {% elseif empty %}
    <div class="view-{{ view.id | replace({'_': '-'}) }}__empty">
      {{ empty }}
    </div>
  {% endif %}

  {% if pager %}
    <div class="view-{{ view.id | replace({'_': '-'}) }}__pager">
      {{ pager }}
    </div>
  {% endif %}
</div>
```

---

## Theming Individual Rows

Use `views-view-unformatted--{view-name}.html.twig` to control row wrapper markup:

```twig
{#
  Variables:
  - rows: array of row objects with .content and .attributes
#}
<ul class="article-listing">
  {% for row in rows %}
    <li {{ row.attributes.addClass('article-listing__item') }}>
      {{ row.content }}
    </li>
  {% endfor %}
</ul>
```

---

## Preprocess for Views

```php
/**
 * Implements hook_preprocess_views_view().
 */
function mytheme_preprocess_views_view(array &$variables): void {
  $view = $variables['view'];

  // Add a modifier class based on display ID.
  if ($view->id() === 'articles' && $view->current_display === 'grid') {
    $variables['attributes']['class'][] = 'article-listing--grid';
  }
}

/**
 * Implements hook_preprocess_views_view_unformatted().
 */
function mytheme_preprocess_views_view_unformatted(array &$variables): void {
  // Add zebra striping or index to each row.
  foreach ($variables['rows'] as $index => &$row) {
    $row['attributes']->setAttribute('data-index', $index);
  }
}
```

---

## Exposed Filters

Exposed filter forms render via `{{ exposed }}` in the view template.

To theme the filter form itself, override `views-exposed-form.html.twig`:
```
templates/views/views-exposed-form--{view-name}--{display-id}.html.twig
```

```twig
<form {{ attributes.addClass('search-filters') }}>
  <div class="search-filters__fields">
    {% for filter in form %}
      {% if filter['#type'] is defined and filter['#type'] not in ['hidden', 'token'] %}
        <div class="search-filters__field">
          {{ filter }}
        </div>
      {% endif %}
    {% endfor %}
  </div>
  <div class="search-filters__actions">
    {{ form.actions }}
  </div>
</form>
```

---

## Common Views Patterns

**Passing view metadata to a template via preprocess:**
```php
function mytheme_preprocess_views_view(array &$variables): void {
  $variables['view_id'] = $variables['view']->id();
  $variables['display_id'] = $variables['view']->current_display;
}
```

**Empty result message — separate template:**
```
templates/views/views-view--{view-name}--{display-id}--empty.html.twig
```

---

## Square360 Conventions

> **TODO: Lead developer to complete this section.**

Document here:
- Views in use on this project and their machine names / displays
- Any shared pager style used across views
- Contextual filter patterns used (e.g. node ID in URL, taxonomy term)
- Any Views BEF (Better Exposed Filters) module usage
