# 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) ```yaml 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) ```yaml 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 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 ```twig
{{ page.header }} {{ page.primary_menu }}
{{ page.breadcrumb }} {{ page.highlighted }}
{{ page.content }}
{% if page.sidebar_first %} {% endif %} {% if page.sidebar_second %} {% endif %}
{% if page.footer %} {% endif %}
``` ### node.html.twig ```twig {{ title_prefix }} {% if not page %} {{ label }} {% endif %} {{ title_suffix }} {% if display_submitted %}
{{ author_picture }} {% trans %}Submitted by {{ author_name }} on {{ date }}{% endtrans %} {{ metadata }}
{% endif %} {{ content }} ``` ### block.html.twig ```twig {{ title_prefix }} {% if label %} {{ label }} {% endif %} {{ title_suffix }} {% block content %} {{ content }} {% endblock %} ``` ### field.html.twig ```twig {% if multiple %} {% if label %} {{ label }} {% endif %}
{% for item in items %} {{ item.content }}
{% endfor %} {% else %} {% for item in items %} {% if label %} {{ label }} {% endif %} {{ item.content }} {% endfor %} {% endif %} ``` ## Twig Filters & Functions ### Common Filters ```twig {# 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 ```twig {# Attach library #} {{ attach_library('mytheme/modal') }} {# URL #} Link {# Path #} Link {# File URL #} {{ file_url('public://image.jpg') }} {# Create attribute #} {% set attributes = create_attribute() %} {{ attributes.addClass('custom-class') }} ``` ## Breakpoints (mytheme.breakpoints.yml) ```yaml 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** ```yaml parameters: twig.config: debug: true auto_reload: true cache: false ``` Then clear cache: ```bash ddev drush cr ``` ## Theme Development Workflow 1. **Enable development settings** ```bash # 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** ```bash ddev drush cr ``` 4. **Use Twig debugging** - Check HTML source for template suggestions - Look for `` comments 5. **Rebuild theme registry** ```bash 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 - Theming Guide: https://www.drupal.org/docs/theming-drupal - Twig Documentation: https://twig.symfony.com/doc/ - Template Suggestions: https://www.drupal.org/docs/theming-drupal/twig-in-drupal/debugging-twig-templates