
Drupal Frontend
- 248 installs
- 27 repo stars
- Updated October 19, 2025
- omedia/drupal-skill
Bootstrap and customize a Drupal custom theme from a starter template (info.yml, libraries, Twig, CSS reset, Drupal.behaviors JS).
About
Drupal frontend is an agent skill packaged as a minimal custom Drupal theme template for solo builders shipping content sites or SaaS-adjacent portals on Drupal. It belongs in the Build phase frontend subphase because the artifact is presentation layer structure: metadata in info.yml, asset registration in libraries.yml, preprocessing hooks in the .theme file, a page Twig shell, normalized base CSS, and a behaviors-based JavaScript stub. The readme doubles as copy-paste scaffolding—rename THEMENAME placeholders, drop the folder under themes/custom, and iterate styling without rebuilding boilerplate from memory. Drupal.behaviors attach is the correct extension point for progressive enhancement inside Drupal’s AJAX and BigPipe contexts, which agents often get wrong when pasting generic frontend snippets. Complexity is beginner-friendly if you already run a Drupal stack locally; intermediate if you need design systems or component libraries on top. Install it when you want a consistent agent-guided path from blank theme directory to a loadable custom theme, not when you need decoupled React or headless GraphQL frontends.
- Starter theme layout: .info.yml, .libraries.yml, .theme, page.html.twig, css/base/reset.css, js/custom.js
- Documented rename flow: THEMENAME/THEMELABEL → machine and human labels
- Drupal.behaviors attach pattern for context-safe JS
- Base CSS reset and 16px root typography defaults
Drupal Frontend by the numbers
- 248 all-time installs (skills.sh)
- +7 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #804 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/omedia/drupal-skill --skill drupal-frontendAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 248 |
|---|---|
| repo stars | ★ 27 |
| Security audit | 3 / 3 scanners passed |
| Last updated | October 19, 2025 |
| Repository | omedia/drupal-skill ↗ |
What it does
Bootstrap and customize a Drupal custom theme from a starter template (info.yml, libraries, Twig, CSS reset, Drupal.behaviors JS).
Files
Drupal Front End Development
Overview
Enable expert-level Drupal front end development capabilities. Provide comprehensive guidance for theme development, Twig templating, preprocessing, responsive design, and asset management for Drupal 8, 9, 10, and 11+.
When to Use This Skill
Invoke this skill when working with:
- Theme development: Creating or customizing Drupal themes
- Twig templates: Writing or modifying .twig template files
- Preprocessing: Implementing preprocess functions for templates
- Template suggestions: Adding custom template naming patterns
- CSS/JS libraries: Managing theme assets and dependencies
- Responsive design: Implementing breakpoints and mobile-first design
- Rendering system: Understanding Drupal's rendering pipeline
- Theme hooks: Implementing theme-related hooks and alterations
Core Capabilities
1. Theme Development
Build complete, standards-compliant Drupal themes with proper structure:
Quick start workflow: 1. Use theme template from assets/theme-template/ as starting point 2. Replace THEMENAME with your theme's machine name 3. Replace THEMELABEL with human-readable name 4. Customize templates, CSS, JS, and libraries 5. Enable theme and configure via Appearance UI
Theme components:
.info.yml- Theme metadata and configuration.libraries.yml- CSS/JS library definitions.theme- Preprocess functions and theme logic.breakpoints.yml- Responsive breakpointstemplates/- Twig template filescss/- Stylesheetsjs/- JavaScript files
Reference documentation:
references/theming.md- Complete theming guide with examples
2. Twig Template System
Master Twig syntax and Drupal-specific extensions:
Twig fundamentals:
{{ variable }}- Print variables{% if/for/set %}- Control structures and logic{# comment #}- Comments and documentation{{ var|filter }}- Apply filters to variables{% extends %}- Template inheritance{% block %}- Define overridable sections
Drupal-specific Twig:
{{ 'Text'|t }}- Translate strings{{ attach_library('theme/library') }}- Load CSS/JS{{ url('route.name') }}- Generate URLs{{ path('route.name') }}- Generate paths{{ file_url('public://image.jpg') }}- File URLs{{ content|without('field') }}- Exclude fields
Common templates:
page.html.twig- Page layout structurenode.html.twig- Node displayblock.html.twig- Block renderingfield.html.twig- Field outputviews-view.html.twig- Views output
3. Preprocessing Functions
Modify template variables before rendering:
Preprocess pattern:
function THEMENAME_preprocess_HOOK(&$variables) {
// Add or modify variables
// Access entities, services, configuration
// Prepare data for templates
}Common preprocess hooks:
hook_preprocess_page()- Page-level variableshook_preprocess_node()- Node-specific datahook_preprocess_block()- Block modificationshook_preprocess_field()- Field alterationshook_preprocess_html()- HTML document
Best practices:
- Keep logic in preprocess, markup in templates
- Use dependency injection when possible
- Cache properly with cache contexts/tags
- Document complex preprocessing
4. Template Suggestions
Provide specific templates for different contexts:
Suggestion patterns:
page--front.html.twig # Homepage
page--node--{nid}.html.twig # Specific node
page--node--{type}.html.twig # Content type
node--{type}--{viewmode}.html.twig # Type + view mode
block--{plugin-id}.html.twig # Specific block
field--{entity}--{field}.html.twig # Specific fieldAdding suggestions:
function THEMENAME_theme_suggestions_HOOK_alter(array &$suggestions, array $variables) {
// Add custom suggestions
$suggestions[] = 'custom__suggestion';
}5. CSS & JavaScript Management
Organize and load theme assets efficiently:
Library definition:
# THEMENAME.libraries.yml
global-styling:
version: 1.0
css:
base:
css/base/reset.css: {}
theme:
css/theme/styles.css: {}
js:
js/custom.js: {}
dependencies:
- core/drupalLoading libraries:
- Global: Define in
.info.yml - Conditional: Use
attach_library()in templates - Preprocessed: Attach in preprocess functions
Best practices:
- Separate CSS into layers (base, layout, component, theme)
- Use CSS aggregation in production
- Minimize JavaScript dependencies
- Leverage Drupal's asset library system
6. Responsive Design
Implement mobile-first, accessible designs:
Breakpoints definition:
# THEMENAME.breakpoints.yml
THEMENAME.mobile:
label: Mobile
mediaQuery: 'screen and (min-width: 0px)'
weight: 0
THEMENAME.tablet:
label: Tablet
mediaQuery: 'screen and (min-width: 768px)'
weight: 1
THEMENAME.desktop:
label: Desktop
mediaQuery: 'screen and (min-width: 1024px)'
weight: 2Mobile-first approach: 1. Design for smallest screens first 2. Enhance for larger viewports 3. Use responsive images 4. Test across devices 5. Follow accessibility standards (WCAG)
Development Workflow
Creating a New Theme
1. Scaffold the theme:
cp -r assets/theme-template/ /path/to/drupal/themes/custom/mytheme/
cd /path/to/drupal/themes/custom/mytheme/
# Rename files
mv THEMENAME.info.yml mytheme.info.yml
mv THEMENAME.libraries.yml mytheme.libraries.yml
mv THEMENAME.theme mytheme.theme2. Update theme configuration:
- Edit
mytheme.info.yml- Set name, regions, base theme - Edit
mytheme.libraries.yml- Define CSS/JS libraries - Replace
THEMENAMEwith your machine name - Replace
THEMELABELwith your label
3. Enable the theme:
ddev drush cr
# Enable via UI at /admin/appearance or:
ddev drush config:set system.theme default mytheme -y4. Enable Twig debugging:
- Copy
sites/default/default.services.ymltosites/default/services.yml - Set
twig.config.debug: true - Set
twig.config.auto_reload: true - Set
twig.config.cache: false - Run
ddev drush cr
5. Develop and iterate:
- Modify templates in
templates/ - Update CSS in
css/ - Clear cache frequently:
ddev drush cr - Check browser console for errors
Standard Development Workflow
1. Enable development settings (Twig debug, disable CSS/JS aggregation) 2. Create/modify templates based on suggestions from Twig debug 3. Implement preprocessing in .theme file for complex data manipulation 4. Add CSS/JS via libraries system 5. Test across browsers and devices 6. Clear cache after changes: ddev drush cr
Finding Template Names
With Twig debugging enabled, inspect HTML source:
<!-- FILE NAME SUGGESTIONS:
* page--node--123.html.twig
* page--node--article.html.twig
* page--node.html.twig
x page.html.twig
-->The x indicates the active template. Others are suggestions you can create.
Common Patterns
Adding Custom Variables in Preprocess
function mytheme_preprocess_page(&$variables) {
// Add site slogan
$variables['site_slogan'] = \Drupal::config('system.site')->get('slogan');
// Add current user
$variables['is_logged_in'] = \Drupal::currentUser()->isAuthenticated();
// Add custom class
$variables['attributes']['class'][] = 'custom-page-class';
}Template Inheritance
{# templates/page--article.html.twig #}
{% extends "page.html.twig" %}
{% block content %}
<div class="article-wrapper">
{{ parent() }}
</div>
{% endblock %}Conditional Library Loading
function mytheme_preprocess_node(&$variables) {
if ($variables['node']->bundle() == 'article') {
$variables['#attached']['library'][] = 'mytheme/article-styles';
}
}Accessing Field Values in Twig
{# Get field value #}
{{ node.field_custom.value }}
{# Check if field has value #}
{% if node.field_image|render %}
{{ content.field_image }}
{% endif %}
{# Loop through multi-value field #}
{% for item in node.field_tags %}
{{ item.entity.label }}
{% endfor %}Best Practices
Theme Development
1. Base theme: Choose appropriate base (Olivero, Claro, or none) 2. Structure: Organize CSS logically (base, layout, components, theme) 3. Naming: Use BEM or similar CSS methodology 4. Comments: Document complex Twig logic and preprocessing 5. Performance: Optimize images, minimize CSS/JS, lazy load when possible
Twig Templates
1. Logic: Keep complex logic in preprocess, not templates 2. Filters: Use appropriate filters (|t, |clean_class, |safe_join) 3. Whitespace: Use {% spaceless %} to control output 4. Debugging: Enable Twig debugging during development only 5. Suggestions: Use specific templates only when needed
Preprocessing
1. Services: Use dependency injection in theme service subscribers 2. Caching: Add proper cache contexts and tags 3. Performance: Avoid heavy operations in frequently-called preprocessors 4. Organization: Group related preprocessing logically
CSS & JavaScript
1. Libraries: Group related assets into logical libraries 2. Dependencies: Declare all library dependencies 3. Loading: Load globally only when necessary 4. Aggregation: Enable in production for performance
Accessibility
1. Semantic HTML: Use proper elements (header, nav, main, etc.) 2. ARIA: Add labels and roles when needed 3. Keyboard: Ensure keyboard navigation works 4. Contrast: Meet WCAG color contrast requirements 5. Alt text: Provide for all images
Responsive Design
1. Mobile-first: Design for small screens, enhance for large 2. Breakpoints: Define logical breakpoints 3. Images: Use responsive image styles 4. Testing: Test across devices and screen sizes 5. Performance: Optimize for mobile networks
Troubleshooting
Template Changes Not Showing
- Clear cache:
ddev drush cr - Verify file location and naming
- Check Twig debug for active template
- Ensure library is properly defined
CSS/JS Not Loading
- Check library definition in
.libraries.yml - Verify file paths are correct
- Ensure library is attached (globally or conditionally)
- Clear cache and check browser console
Variables Not Available in Template
- Check preprocess function naming
- Verify variables are being set correctly
- Use Twig debugging:
{{ dump() }} - Clear cache after preprocessing changes
Twig Debugging Not Working
- Verify
sites/default/services.ymlexists - Check
twig.config.debug: trueis set - Clear cache:
ddev drush cr - Check file permissions
Resources
Reference Documentation
- `references/theming.md` - Comprehensive theming guide
- Twig syntax and filters
- Template suggestions
- Preprocessing patterns
- Library management
- Responsive design
- Best practices
Asset Templates
- `assets/theme-template/` - Complete theme scaffold
.info.ymlwith regions and configuration.libraries.ymlwith CSS/JS examples.themewith preprocess examples- Page template
- CSS and JS starter files
Searching References
# Find specific Twig filter
grep -r "clean_class" references/
# Find preprocessing examples
grep -r "preprocess_node" references/
# Find library patterns
grep -r "libraries.yml" references/Version Compatibility
Drupal 8 vs 9 vs 10 vs 11
- Twig syntax: Consistent across all versions
- Base themes: Some legacy themes removed in 10+
- Libraries: Same structure across versions
- jQuery: Drupal 9+ doesn't load jQuery by default
- Claro: Admin theme standard in 9+
- Olivero: Frontend theme standard in 10+
Migration Notes
- When upgrading, check for deprecated base themes
- Review jQuery dependencies
- Test with current Twig version
- Check for removed core templates
See Also
- drupal-backend - Module development, hooks, APIs
- drupal-tooling - DDEV and Drush development tools
- Drupal Theming Guide - Official documentation
- Twig Documentation - Twig syntax reference
/**
* @file
* Basic CSS reset and normalization.
*/
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
font-size: 16px;
line-height: 1.5;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
color: #333;
background-color: #fff;
}
img {
max-width: 100%;
height: auto;
}
/**
* @file
* Custom JavaScript for THEMELABEL theme.
*/
(function (Drupal) {
'use strict';
Drupal.behaviors.THEMENAME = {
attach: function (context, settings) {
// Custom JavaScript code here
console.log('THEMELABEL theme loaded');
}
};
})(Drupal);
Theme Template
This is a basic Drupal theme template. To use it:
1. Copy this directory to your Drupal installation: themes/custom/yourthemename/ 2. Rename files from THEMENAME.* to yourthemename.* 3. Replace all instances of THEMENAME with your theme's machine name (lowercase, underscores) 4. Replace all instances of THEMELABEL with your theme's human-readable name 5. Customize the theme as needed
Files Included
THEMENAME.info.yml- Theme metadataTHEMENAME.libraries.yml- CSS/JS library definitionsTHEMENAME.theme- Theme functions and preprocessingtemplates/page.html.twig- Page templatecss/base/reset.css- Base CSS resetjs/custom.js- Custom JavaScript
Directory Structure
THEMENAME/
├── THEMENAME.info.yml
├── THEMENAME.libraries.yml
├── THEMENAME.theme
├── css/
│ └── base/
│ └── reset.css
├── js/
│ └── custom.js
└── templates/
└── page.html.twigYou can expand the CSS structure as needed:
css/
├── base/
│ └── reset.css
├── layout/
│ └── layout.css
├── components/
│ └── components.css
└── theme/
└── theme.cssNext Steps
After creating your theme:
1. Clear cache: ddev drush cr 2. Enable your theme: Go to /admin/appearance 3. Set as default theme 4. Start customizing!
Development Tips
- Enable Twig debugging in
sites/default/services.yml - Disable CSS/JS aggregation during development
- Clear cache frequently:
ddev drush cr - Use template suggestions for specific pages/content types
{#
/**
* @file
* Theme override to display a page.
*/
#}
<div class="layout-container">
{% if page.header %}
<header role="banner" class="site-header">
{{ page.header }}
</header>
{% endif %}
{% if page.primary_menu %}
<nav role="navigation" class="primary-menu">
{{ page.primary_menu }}
</nav>
{% endif %}
{% if page.breadcrumb %}
<div class="breadcrumb-wrapper">
{{ page.breadcrumb }}
</div>
{% endif %}
{% if page.highlighted %}
<div class="highlighted">
{{ page.highlighted }}
</div>
{% endif %}
<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" class="site-footer">
{{ page.footer }}
</footer>
{% endif %}
</div>
name: THEMELABEL
description: 'A custom Drupal theme.'
type: theme
core_version_requirement: ^9 || ^10 || ^11
package: Custom
# Base theme (uncomment one)
# base theme: stable9
# base theme: claro
# base theme: olivero
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
libraries:
- THEMENAME/global-styling
- THEMENAME/global-scripts
# Component libraries (optional)
# component-libraries:
# atoms:
# paths:
# - components/atoms
# Logo and favicon
# logo: images/logo.svg
# favicon: images/favicon.ico
global-styling:
version: 1.0
css:
base:
css/base/reset.css: {}
layout:
css/layout/layout.css: {}
component:
css/components/components.css: {}
theme:
css/theme/theme.css: {}
global-scripts:
version: 1.0
js:
js/custom.js: {}
dependencies:
- core/drupal
- core/drupalSettings
<?php
/**
* @file
* Functions to support theming in the THEMELABEL theme.
*/
use Drupal\Core\Form\FormStateInterface;
/**
* Implements hook_preprocess_HOOK() for page templates.
*/
function THEMENAME_preprocess_page(&$variables) {
// Add custom variables or modify existing ones.
}
/**
* Implements hook_preprocess_HOOK() for node templates.
*/
function THEMENAME_preprocess_node(&$variables) {
/** @var \Drupal\node\NodeInterface $node */
$node = $variables['node'];
// Add custom variables based on node.
}
/**
* Implements hook_theme_suggestions_HOOK_alter() for page templates.
*/
function THEMENAME_theme_suggestions_page_alter(array &$suggestions, array $variables) {
// Add custom template suggestions.
}
/**
* Implements hook_form_alter().
*/
function THEMENAME_form_alter(&$form, FormStateInterface $form_state, $form_id) {
// Modify forms as needed.
}
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: falseLibraries (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/normalizeTheme 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:
- 1xDebug Mode
Enable Twig debugging in development:
sites/default/services.yml
parameters:
twig.config:
debug: true
auto_reload: true
cache: falseThen clear cache:
ddev drush crTheme Development Workflow
1. Enable development settings
# Copy and enable development settings
cp sites/example.settings.local.php sites/default/settings.local.php2. Disable CSS/JS aggregation
- Go to
/admin/config/development/performance - Uncheck "Aggregate CSS files"
- Uncheck "Aggregate JavaScript files"
3. Clear cache frequently
ddev drush cr4. Use Twig debugging
- Check HTML source for template suggestions
- Look for
<!-- BEGIN OUTPUT -->comments
5. Rebuild theme registry
ddev drush drush crBest 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 displayUseful 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
Related skills
FAQ
Is Drupal Frontend safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.