Files
gh-omedia-drupal-skill-drup…/references/theming.md
2025-11-30 08:45:16 +08:00

13 KiB

Drupal Theming Reference

Comprehensive guide to Drupal theming for Drupal 8-11+.

Theme Structure

mytheme/
├── mytheme.info.yml          # Theme metadata (required)
├── mytheme.libraries.yml     # CSS/JS libraries
├── mytheme.theme             # Theme functions and preprocess
├── mytheme.breakpoints.yml   # Responsive breakpoints
├── logo.svg                  # Theme logo
├── screenshot.png            # Admin screenshot
├── composer.json             # Composer dependencies
├── package.json              # NPM dependencies (if using build tools)
├── config/
│   ├── install/             # Default configuration
│   └── schema/              # Configuration schema
├── css/
│   ├── base/
│   ├── components/
│   ├── layout/
│   └── theme/
├── js/
│   └── custom.js
├── images/
├── templates/
│   ├── block/
│   ├── content/
│   ├── field/
│   ├── layout/
│   ├── navigation/
│   ├── views/
│   ├── page.html.twig
│   ├── node.html.twig
│   └── block.html.twig
└── src/                     # Optional PHP classes
    └── Plugin/
        └── Preprocess/

Theme Info File (mytheme.info.yml)

name: My Theme
type: theme
description: 'A custom Drupal theme'
core_version_requirement: ^9 || ^10 || ^11
package: Custom

# Base theme
base theme: stable9
# Or for no base theme:
# base theme: false

# Regions
regions:
  header: Header
  primary_menu: 'Primary menu'
  secondary_menu: 'Secondary menu'
  page_top: 'Page top'
  page_bottom: 'Page bottom'
  highlighted: Highlighted
  breadcrumb: Breadcrumb
  content: Content
  sidebar_first: 'Sidebar first'
  sidebar_second: 'Sidebar second'
  footer: Footer

# Libraries to load on all pages
libraries:
  - mytheme/global-styling
  - mytheme/global-scripts

# Libraries to load only when certain conditions are met
libraries-override:
  # Replace core library
  core/drupal.dialog:
    mytheme/custom-dialog: {}

libraries-extend:
  # Add to existing library
  core/drupal.dialog:
    - mytheme/dialog-extend

# Remove libraries
libraries-override:
  core/normalize:
    css:
      base:
        assets/vendor/normalize-css/normalize.css: false

# Component libraries (for single-directory components)
component-libraries:
  atoms:
    paths:
      - components/atoms
  molecules:
    paths:
      - components/molecules

# Logo and favicon
logo: images/logo.svg
favicon: images/favicon.ico

# Stylesheets to remove
stylesheets-remove:
  - core/assets/vendor/normalize-css/normalize.css
  - '@classy/css/components/tabs.css'

# CKEditor stylesheet
ckeditor_stylesheets:
  - css/ckeditor.css

# Hidden theme (for base themes)
hidden: false

Libraries (mytheme.libraries.yml)

global-styling:
  version: 1.0
  css:
    base:
      css/base/reset.css: {}
    layout:
      css/layout/layout.css: {}
    component:
      css/components/button.css: {}
      css/components/card.css: {}
    theme:
      css/theme/colors.css: {}
      css/theme/typography.css: {}

global-scripts:
  version: 1.0
  js:
    js/custom.js: {}
  dependencies:
    - core/drupal
    - core/jquery

# Conditional library (loaded via preprocess or template)
modal:
  version: 1.0
  css:
    component:
      css/components/modal.css: {}
  js:
    js/modal.js: {}
  dependencies:
    - core/drupal
    - core/drupalSettings

# External library
fontawesome:
  version: 6.0
  css:
    theme:
      https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css: { type: external, minified: true }

# SCSS/SASS (requires compilation)
compiled-styles:
  version: 1.0
  css:
    theme:
      dist/css/styles.css: {}
  dependencies:
    - core/normalize

Theme Functions (mytheme.theme)

<?php

/**
 * @file
 * Functions to support theming in the My Theme theme.
 */

use Drupal\Core\Form\FormStateInterface;
use Drupal\node\NodeInterface;

/**
 * Implements hook_preprocess_HOOK() for page templates.
 */
function mytheme_preprocess_page(&$variables) {
  // Add custom variable to all pages
  $variables['site_slogan'] = \Drupal::config('system.site')->get('slogan');

  // Add body classes
  $variables['attributes']['class'][] = 'custom-page-class';
}

/**
 * Implements hook_preprocess_HOOK() for node templates.
 */
function mytheme_preprocess_node(&$variables) {
  /** @var \Drupal\node\NodeInterface $node */
  $node = $variables['node'];

  // Add custom variables
  $variables['created_date'] = \Drupal::service('date.formatter')->format(
    $node->getCreatedTime(),
    'custom',
    'F j, Y'
  );

  // Add template suggestions
  $variables['theme_hook_suggestions'][] = 'node__' . $node->bundle() . '__' . $variables['view_mode'];
}

/**
 * Implements hook_preprocess_HOOK() for block templates.
 */
function mytheme_preprocess_block(&$variables) {
  // Add block ID as class
  if (isset($variables['elements']['#id'])) {
    $variables['attributes']['class'][] = 'block-' . $variables['elements']['#id'];
  }
}

/**
 * Implements hook_preprocess_HOOK() for field templates.
 */
function mytheme_preprocess_field(&$variables) {
  $element = $variables['element'];

  // Custom preprocessing for specific fields
  if ($element['#field_name'] == 'field_custom') {
    $variables['custom_class'] = 'field-custom-class';
  }
}

/**
 * Implements hook_theme_suggestions_HOOK_alter() for page templates.
 */
function mytheme_theme_suggestions_page_alter(array &$suggestions, array $variables) {
  // Add template suggestions based on current route
  if ($node = \Drupal::routeMatch()->getParameter('node')) {
    if ($node instanceof NodeInterface) {
      $suggestions[] = 'page__node__' . $node->bundle();
      $suggestions[] = 'page__node__' . $node->id();
    }
  }
}

/**
 * Implements hook_theme_suggestions_HOOK_alter() for node templates.
 */
function mytheme_theme_suggestions_node_alter(array &$suggestions, array $variables) {
  /** @var \Drupal\node\NodeInterface $node */
  $node = $variables['elements']['#node'];
  $view_mode = $variables['elements']['#view_mode'];

  // Add suggestion for bundle and view mode combination
  $suggestions[] = 'node__' . $node->bundle() . '__' . $view_mode;
}

/**
 * Implements hook_form_alter().
 */
function mytheme_form_alter(&$form, FormStateInterface $form_state, $form_id) {
  // Add custom classes to forms
  if ($form_id == 'search_block_form') {
    $form['actions']['submit']['#attributes']['class'][] = 'custom-search-submit';
  }
}

/**
 * Implements hook_page_attachments_alter().
 */
function mytheme_page_attachments_alter(array &$attachments) {
  // Add custom meta tags
  $meta_charset = [
    '#tag' => 'meta',
    '#attributes' => [
      'charset' => 'utf-8',
    ],
  ];
  $attachments['#attached']['html_head'][] = [$meta_charset, 'meta_charset'];
}

/**
 * Implements hook_library_info_alter().
 */
function mytheme_library_info_alter(&$libraries, $extension) {
  // Modify libraries from other modules
  if ($extension == 'core' && isset($libraries['drupal.dialog'])) {
    $libraries['drupal.dialog']['dependencies'][] = 'mytheme/custom-dialog';
  }
}

Twig Templates

page.html.twig

<div class="layout-container">
  <header role="banner">
    {{ page.header }}
    {{ page.primary_menu }}
  </header>

  {{ page.breadcrumb }}
  {{ page.highlighted }}

  <main role="main" class="main-content">
    <a id="main-content" tabindex="-1"></a>

    <div class="layout-content">
      {{ page.content }}
    </div>

    {% if page.sidebar_first %}
      <aside class="sidebar sidebar--first" role="complementary">
        {{ page.sidebar_first }}
      </aside>
    {% endif %}

    {% if page.sidebar_second %}
      <aside class="sidebar sidebar--second" role="complementary">
        {{ page.sidebar_second }}
      </aside>
    {% endif %}
  </main>

  {% if page.footer %}
    <footer role="contentinfo">
      {{ page.footer }}
    </footer>
  {% endif %}
</div>

node.html.twig

<article{{ attributes.addClass('node', 'node--type-' ~ node.bundle|clean_class, node.isPromoted() ? 'node--promoted', node.isSticky() ? 'node--sticky', not node.isPublished() ? 'node--unpublished', view_mode ? 'node--view-mode-' ~ view_mode|clean_class) }}>

  {{ title_prefix }}
  {% if not page %}
    <h2{{ title_attributes.addClass('node__title') }}>
      <a href="{{ url }}" rel="bookmark">{{ label }}</a>
    </h2>
  {% endif %}
  {{ title_suffix }}

  {% if display_submitted %}
    <div class="node__meta">
      {{ author_picture }}
      <span{{ author_attributes }}>
        {% trans %}Submitted by {{ author_name }} on {{ date }}{% endtrans %}
      </span>
      {{ metadata }}
    </div>
  {% endif %}

  <div{{ content_attributes.addClass('node__content') }}>
    {{ content }}
  </div>

</article>

block.html.twig

<div{{ attributes.addClass('block', 'block-' ~ configuration.provider|clean_class, 'block-' ~ plugin_id|clean_class) }}>
  {{ title_prefix }}
  {% if label %}
    <h2{{ title_attributes }}>{{ label }}</h2>
  {% endif %}
  {{ title_suffix }}

  {% block content %}
    {{ content }}
  {% endblock %}
</div>

field.html.twig

{% if multiple %}
  <div{{ attributes.addClass('field', 'field--name-' ~ field_name|clean_class, 'field--type-' ~ field_type|clean_class) }}>
    {% if label %}
      <div{{ title_attributes.addClass('field__label') }}>{{ label }}</div>
    {% endif %}
    <div class="field__items">
      {% for item in items %}
        <div{{ item.attributes.addClass('field__item') }}>{{ item.content }}</div>
      {% endfor %}
    </div>
  </div>
{% else %}
  {% for item in items %}
    <div{{ attributes.addClass('field', 'field--name-' ~ field_name|clean_class, 'field--type-' ~ field_type|clean_class) }}>
      {% if label %}
        <div{{ title_attributes.addClass('field__label') }}>{{ label }}</div>
      {% endif %}
      <div{{ item.attributes.addClass('field__item') }}>{{ item.content }}</div>
    </div>
  {% endfor %}
{% endif %}

Twig Filters & Functions

Common Filters

{# Translate #}
{{ 'Hello World'|t }}

{# Clean class name #}
{{ 'Field Name'|clean_class }}

{# Format date #}
{{ node.created.value|date('F j, Y') }}

{# Safe join #}
{{ items|safe_join(', ') }}

{# Render #}
{{ content|render }}

{# Without (remove array elements) #}
{{ content|without('field_image') }}

{# Placeholder #}
{{ 'Hello @name'|t({'@name': name}) }}

Common Functions

{# Attach library #}
{{ attach_library('mytheme/modal') }}

{# URL #}
<a href="{{ url('entity.node.canonical', {'node': node.id}) }}">Link</a>

{# Path #}
<a href="{{ path('entity.node.canonical', {'node': node.id}) }}">Link</a>

{# File URL #}
{{ file_url('public://image.jpg') }}

{# Create attribute #}
{% set attributes = create_attribute() %}
{{ attributes.addClass('custom-class') }}

Breakpoints (mytheme.breakpoints.yml)

mytheme.mobile:
  label: Mobile
  mediaQuery: 'screen and (min-width: 0px)'
  weight: 0
  multipliers:
    - 1x
    - 2x

mytheme.tablet:
  label: Tablet
  mediaQuery: 'screen and (min-width: 768px)'
  weight: 1
  multipliers:
    - 1x
    - 2x

mytheme.desktop:
  label: Desktop
  mediaQuery: 'screen and (min-width: 1024px)'
  weight: 2
  multipliers:
    - 1x
    - 2x

mytheme.wide:
  label: Wide
  mediaQuery: 'screen and (min-width: 1440px)'
  weight: 3
  multipliers:
    - 1x

Debug Mode

Enable Twig debugging in development:

sites/default/services.yml

parameters:
  twig.config:
    debug: true
    auto_reload: true
    cache: false

Then clear cache:

ddev drush cr

Theme Development Workflow

  1. Enable development settings

    # Copy and enable development settings
    cp sites/example.settings.local.php sites/default/settings.local.php
    
  2. Disable CSS/JS aggregation

    • Go to /admin/config/development/performance
    • Uncheck "Aggregate CSS files"
    • Uncheck "Aggregate JavaScript files"
  3. Clear cache frequently

    ddev drush cr
    
  4. Use Twig debugging

    • Check HTML source for template suggestions
    • Look for <!-- BEGIN OUTPUT --> comments
  5. Rebuild theme registry

    ddev drush drush cr
    

Best Practices

  1. BEM Methodology: Use BEM for CSS class naming
  2. Component-based: Build reusable components
  3. Accessibility: Follow WCAG guidelines
  4. Performance: Optimize images, minimize CSS/JS
  5. Mobile-first: Design for mobile, enhance for desktop
  6. Semantic HTML: Use proper HTML5 elements
  7. Template suggestions: Use specific templates when needed
  8. Libraries: Group related CSS/JS in libraries
  9. Preprocessing: Keep logic in .theme file, not templates
  10. Documentation: Comment complex template logic

Common Template Suggestions

page--front.html.twig                  # Front page
page--node--123.html.twig              # Specific node
page--node--article.html.twig          # Content type
node--article--full.html.twig          # Bundle and view mode
node--123.html.twig                    # Specific node
block--system-branding-block.html.twig # Specific block
field--node--title--article.html.twig  # Specific field
views-view--blog--page-1.html.twig     # View and display

Useful Resources