Custom Drupal Theme Development

Our company is engaged in the development, support and maintenance of sites of any complexity. From simple one-page sites to large-scale cluster systems built on micro services. Experience of developers is confirmed by certificates from vendors.
Development and maintenance of all types of websites:
Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Our competencies:
Development stages
Latest works
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    822
  • image_crm_chasseurs_493_0.webp
    CRM development for Chasseurs
    847
  • image_website-sbh_0.png
    Website development for SBH Partners
    999
  • image_website-_0.png
    Website development for Red Pear
    451

Developing Custom Drupal Themes

Drupal themes are a set of Twig templates, CSS, JS, and YAML configuration files. Development from scratch or based on a base theme. Contrib themes like Bootstrap or Gin are used as starting points or for the admin interface.

Theme Structure

web/themes/custom/my_theme/
├── my_theme.info.yml          # theme description
├── my_theme.libraries.yml     # CSS/JS libraries
├── my_theme.theme             # PHP theme hooks
├── config/
│   └── install/               # config installed with theme
├── css/
│   ├── base.css
│   ├── layout.css
│   └── components/
├── js/
│   └── main.js
├── images/
├── fonts/
├── templates/
│   ├── layout/
│   │   ├── html.html.twig
│   │   └── page.html.twig
│   ├── content/
│   │   ├── node.html.twig
│   │   ├── node--article.html.twig
│   │   └── node--article--teaser.html.twig
│   ├── block/
│   │   └── block.html.twig
│   └── field/
│       └── field--body.html.twig
└── screenshot.png

my_theme.info.yml

name: My Theme
type: theme
description: 'Custom project theme'
core_version_requirement: ^10
base theme: stable9   # or false if from scratch
libraries:
  - my_theme/global
regions:
  header: Header
  primary_menu: 'Primary menu'
  breadcrumb: Breadcrumb
  highlighted: Highlighted
  content: Content
  sidebar_first: 'Sidebar first'
  footer: Footer

CSS/JS Libraries

# my_theme.libraries.yml
global:
  version: VERSION
  css:
    base:
      css/base.css: {}
    layout:
      css/layout.css: {}
      css/components/header.css: {}
  js:
    js/main.js: { defer: true }
  dependencies:
    - core/drupal
    - core/jquery

# Separate library for slider — loaded only when needed
slider:
  version: VERSION
  css:
    component:
      css/components/slider.css: {}
  js:
    js/slider.js: {}
  dependencies:
    - core/once

Attach libraries in template:

{# In node--landing.html.twig #}
{{ attach_library('my_theme/slider') }}

Twig Templates

Drupal uses template hierarchy — the more specific the name, the higher the priority:

node.html.twig                      # all nodes
node--article.html.twig             # article type
node--article--full.html.twig       # article type, full view
node--123.html.twig                 # specific node by id

Example of overriding article template:

{# templates/content/node--article--full.html.twig #}
<article{{ attributes.addClass('article', 'article--full') }}>
  {% if label %}
    <h1{{ title_attributes.addClass('article__title') }}>
      {{ label }}
    </h1>
  {% endif %}

  <div class="article__meta">
    {% if display_submitted %}
      <span class="article__author">{{ author_name }}</span>
      <time class="article__date" datetime="{{ date.attributes.datetime }}">
        {{ date }}
      </time>
    {% endif %}

    {% if content.field_tags %}
      <div class="article__tags">
        {{ content.field_tags }}
      </div>
    {% endif %}
  </div>

  {% if content.field_image %}
    <div class="article__cover">
      {{ content.field_image }}
    </div>
  {% endif %}

  <div class="article__body prose">
    {{ content.body }}
  </div>

  {# Render remaining fields except those already output #}
  {{ content|without('body', 'field_tags', 'field_image', 'links') }}
</article>

PHP Hooks in .theme File

// my_theme.theme

/**
 * Add variables to page template.
 */
function my_theme_preprocess_page(array &$variables): void {
  $variables['site_name'] = \Drupal::config('system.site')->get('name');
  $variables['is_front'] = \Drupal::service('path.matcher')->isFrontPage();

  // Breadcrumbs with custom logic
  $route = \Drupal::routeMatch();
  if ($node = $route->getParameter('node')) {
    $variables['node_type'] = $node->bundle();
  }
}

/**
 * Variables for node template.
 */
function my_theme_preprocess_node(array &$variables): void {
  $node = $variables['node'];

  if ($node->bundle() === 'article') {
    $variables['reading_time'] = my_theme_calculate_reading_time($node->get('body')->value);
  }
}

function my_theme_calculate_reading_time(string $html): int {
  $text = strip_tags($html);
  $words = str_word_count($text);
  return (int) ceil($words / 200); // 200 words per minute
}

/**
 * Override template suggestions — for debugging.
 */
function my_theme_theme_suggestions_node_alter(array &$suggestions, array $variables): void {
  $node = $variables['elements']['#node'];
  $view_mode = $variables['elements']['#view_mode'];

  // Add suggestion by material type field
  if ($node->hasField('field_material_type') && !$node->get('field_material_type')->isEmpty()) {
    $type = $node->get('field_material_type')->value;
    $suggestions[] = 'node__' . $node->bundle() . '__' . $type;
  }
}

/**
 * Form alter — add classes.
 */
function my_theme_form_alter(array &$form, FormStateInterface $form_state, string $form_id): void {
  if ($form_id === 'contact_message_feedback_form') {
    $form['#attributes']['class'][] = 'contact-form';
    $form['actions']['submit']['#attributes']['class'][] = 'btn btn--primary';
  }
}

Responsive Images and Image Styles

# config/install/image.style.article_cover.yml
langcode: en
status: true
id: article_cover
label: 'Article cover'
effects:
  uuid1:
    id: image_scale_and_crop
    data:
      anchor: center-center
      width: 1200
      height: 630

Use responsive_image instead of plain image in template:

{% if content.field_image %}
  {{- content.field_image -}}
{% endif %}

Responsive image group is configured in /admin/config/media/responsive-image-style via UI or YAML.

Frontend Build (optional)

If theme uses npm build:

// package.json
{
  "scripts": {
    "build": "postcss css/src -o css --map",
    "watch": "postcss css/src -o css --watch"
  }
}

Or Vite/Webpack if you need ES modules, TypeScript:

// vite.config.js
export default {
  build: {
    outDir: 'dist',
    rollupOptions: {
      input: { main: 'js/src/main.ts' },
      output: { entryFileNames: 'js/[name].js' }
    }
  },
  css: { postcss: './postcss.config.js' }
}

Debug Templates

In settings.local.php enable Twig debug:

$config['system.performance']['css']['preprocess'] = FALSE;
$config['system.performance']['js']['preprocess'] = FALSE;
$settings['cache']['bins']['render'] = 'cache.backend.null';

// Shows template names in HTML comments
$config['twig.settings']['debug'] = TRUE;

After enabling, page source will show all used templates and available suggestions. This is the main tool when developing themes.

Timelines

Base theme with templates for main content types, responsive, with libraries: 5–8 days. With animations, complex JS, responsive images, custom blocks: 10–15 days.