Creating new EBT modules

08/02/2023, by Ivan

Easiest way to learn how EBT modules work it's creating a new EBT module. Usually I copy EBT Text module.

Because, it's simplest EBT module and it contains all basic EBT settings. 

Copy existing EBT Text module as boilerplate

EBT Text module contains next folders:
/ebt_text/config/install - with configs for EBT Text block type and field instances. Other EBT modules can contain configs for paragraph types and field storages.
/ebt_text/templates - here is block--block-content--ebt-text.html.twig template for content block which can be used in Block layout page and block--inline-block--ebt-text.html.twig template for Layout Builder inline blocks.
/ebt_text/tests - here are tests for EBT modules, now only one test for installation module.

And other regular drupal module files: composer.json,, You can read more about creating custom Drupal modules in official documentation:

I will create a new module EBT Countdown which will use this javascript plugin FlipDown:


Fork Github repository and submit it to Packagist

All 3rd party libraries should be forked and placed on Packagist, for example:


Then Composer can download them as usual packagist libraries. These 3rd party libraries must have "type": "drupal-library", and they will be download in /libraries folder, by default:

EBT library

Let's fork github repository for FlipDown.

It might be unusual to fork git repositories, instead of using source git repositories. But I believe it will be easier to use EBT modules without manipulations with main composer.json file to add external links to repositories in it. Imagine, how it's hard for newbies to install composer, update composer.json file manually and place repository source in right place. With own packagist library the process of installation is getting simple. So let's keep all 3rd party libraries on packagist.


fork git repository

On forking git repo page you can rename repository. You must keep it clean, without Capital letters and special chars, dash - and underscore _ are working fine.

Rename repository

Now we have a new git repo:

Then we need to add composer.json file in this repository with "type": "drupal-library":

git add composer.json
git commit -m 'Add Composer.json file'
git push origin master

Here is composer.json file:

If you check your current tags in new git repo, it will be empty:

git tag

Git tags

Usually I follow version from source repo, for example if  latest version of library was 1.4.6, I just increment minor version to 1.4.7. FlipDown repo didn't have any tags or releases, so I created 1.0.0 version for new repo:

git tag 1.0.0
git push origin 1.0.0

We need new tag, because it contains our new composer.json with "type": "library"

Why we can't simply copy javascript library inside module?

We can copy only libraries under GPL license, but usually javascipt libraries uses MIT license. It's possible to do technically, but it's prohibited by rules:

Let's submit FlipDown library on

Submit new library on

If you submitted library and forgot to add composer.json with "type": "drupal-library", don't worry, just add composer.json file and create new tag for your git repository. This tag will be automatically pushed on packagist.

Here is packagist library page for FlipDown:

Packagist library

Be sure that you have drupal-library type on packagist page.

Let's go back to our drupal files and copy ebt_text folder, I will name new module ebt_countdown:

EBT Countdown module

We need to:

- remove configs in /config/install, we will export new configs later

- replace all ebt_text mention with ebt_countdown

- rename files to have "countdown" instead of "text"

- update texts for module description in and files.

I will commit each step separately in git, so you could see updates step by step:

git clone

Git history

Now we have boilerplate for our module and can push changes on

Create module project on

Let's go on site on adding project page: add content

We need to add Module project:

Name: Extra Block Types (EBT): Countdown
Project type: Full project
Short name: ebt_countdown
Maintenance status: Actively maintained
Development status: Under active development
Module categories: Content, Content Display
Ecosystem: Extra Block Types (EBT): Core (3191928)

Create new drupal project

In Description field I usually insert full list of available EBT modules:

Extra Block Types: Countdown module provides ability to add a block with animated countdown.

EBT allows to choose styles for countdown via UI.

EBT modules provide ability to add different blocks in Layout Builder in few clicks. You can install separate block types from this bunch of EBT modules:
  <li><a href="" title="EBT Accordion / FAQ">EBT Accordion / FAQ</a></li>
  <li><a href="" title="EBT Basic Button">EBT Basic Button</a></li>
  <li><a href="" title="EBT Bootstrap Button">EBT Bootstrap Button</a></li>
  <li><a href="" title="EBT Call to Action">EBT Call to Action</a>
  <li><a href="" title="EBT Tabs">EBT Carousel</a>
  <li><a href="" title="EBT Counter">EBT Counter</a>
  <li><a href="">EBT Image Gallery</a></li>
  <li><a href="" title="EBT Quote">EBT Quote</a></li>
  <li><a href="">EBT Slick Slider</a></li>
  <li><a href="">EBT Slideshow</a></li>
  <li><a href="">EBT Stats</a></li>
  <li><a href="" title="EBT Tabs">EBT Tabs</a></li>
  <li><a href="">EBT Text</a></li>
  <li><a href="">EBT Timeline</a></li>
  <li><a href="">EBT Webform</a></li>
  <li><a href="">EBT Webform Popup</a></li>

All EBT block types contain default widget for EBT blocks, which has Design options for:
<li>CSS Box (margins, paddings, borders)</li>
<li>Background using color, image (including parallax and cover), video (Youtube)</li>
<li>Edge to edge, Container width</li>

More about EBT blocks read on EBT Core module page:

<div class="container">
<div class="panel-layout capricorn pane-bundle-cta-multiple">
  <div class="panel-pane pane-fieldable-panels-pane pane-vid-10729 d-sponsor pane-bundle-cta pane-fpid-422">
   <div class="pane-content">
    <div class="fieldable-panels-pane pane-style-full nographic pane-style-supporter">
     <div class="pane-title pane-bundle-cta-multiple">
       <h3>Need another Extra Block Type?</h3>

       <a class="button-link" target="_blank" href="" title="DrupalBook">Contact Us</a>

       <a href="" title="DrupalBook"  target="_blank"><img src="/files/logo.svg__1.png" alt="DrupalBook logo" width="85" /></a>

Now we have Drupal module project page on

On Version Control tab you can see instructions how to add remote origin for you local git repository:

Drupal project version control

After initial commit you should create a new branch to follow major version of other EBT modules, now it's 1.4.x.

Now we can start to add new functionality for our module. The process is similar with custom module development, we will create block type, add fields, include css/js assets.

Start building EBT Countdown functionality

Step 1. Creating EBT Countdown block type

First of all, we need to create a new block type EBT Countdown:


Create new block type

It's required that machine name starts from ebt_, so usually I name block types starts from EBT, then machine name is automatically being generated in right way. Should block type machine name match to module name? Yes, it's good to have for consistancy and be sure that another EBT module didn't have the same machine name. Block type machine name must start from ebt_, becuase it's needed to override templates in modules instead of theme folder, see ebt_core_theme_registry_alter() function in ebt_core module.

Now we can add EBT settings field, it's needed for all EBT modules. We should add existing field EBT Settings: field_ebt_settings:

Add EBT Settings field

EBT Settings is common field from EBT Core module, it provides DOM Box, background, spacing and width settings.

As we need countdown to the date, we should have timestamp date field. Let's add it too:

create date field

I added ebt_ for machine-readable name, but it's not required here. It can be field_countdown_date as well. We also have default body and title field for block, so it will be enough for countdown block.

Usually for EBT modules we have horizontal tabs on Edit form:

Manage form display

It's not required, but it's good to split content and settings, because we have a lot of settings for blocks. 

Parent field group should be Tabs with settings Direction Horizontal, and Width Breakpoint 120 (or any small number): 

Tabs settings

Now we can block type, let's enable EBT Countdown module, so templates for block type will be applied:


Enable EBT Countdown

Also you will need to enable Layout Builder module and enable layout builder for any content type for example for Basic Page.


Drupal EBT Countdown

When you create a new Page, you will be able to add blocks on page Layout:

EBT block

And what we have on the page:

EBT block view

Step 2. Include 3rd party libraries to EBT modules

Now we can include our 3rd party library. We have levmyshkin/flipdown library in composer.json, but we have this new module as custom module, so we need to install this library manually with composer:

composer require levmyshkin/flipdown

New library should be placed in libraries folder automatically:

Install flipdown

Let's add ebt_countdown.libraries.yml file and include there flipdown css/js and javascript file ebt_flipdown/js/ebt_countdown.js where later we will init flipdown plugin:


      /libraries/flipdown/dist/flipdown.min.css: { minified: true }
    /libraries/flipdown/dist/flipdown.min.js: { minified: true }
    js/ebt_countdown.js: {}

For files in /libraries folder we use slash at the beginning, so it's absolute path.


(function ($, Drupal) {

   * EBT Countdown behavior.
  Drupal.behaviors.ebtCountDown = {
    attach: function (context, settings) {


})(jQuery, Drupal);

And we should include new ebt_countdown library in temlates, don't forget what we have two templates:

{{ attach_library('ebt_countdown/ebt_countdown') }}

Drupal templates

Clear caches and check that your javascript files on the page:

Add javascript file

We will pass date from PHP to javascript in drupalSettings. So we should extend our ebt_countdown.libraries.yml file with dependencies. Also we will use once() function:

    - core/once
    - core/drupalSettings

Step 3. Include own field widget for EBT Settings, pass variables to javascript

In EBT modules settings are not being passed to javascript by default. We need to override field widget class EbtSettingsDefaultWidget:



namespace Drupal\ebt_countdown\Plugin\Field\FieldWidget;

use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\ebt_core\Plugin\Field\FieldWidget\EbtSettingsDefaultWidget;

 * Plugin implementation of the 'ebt_settings_countdown' widget.
 * @FieldWidget(
 *   id = "ebt_settings_countdown",
 *   label = @Translation("EBT Countdown settings"),
 *   field_types = {
 *     "ebt_settings"
 *   }
 * )
class EbtSettingsCountDownWidget extends EbtSettingsDefaultWidget {

   * {@inheritdoc}
  public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
    $element = parent::formElement($items, $delta, $element, $form, $form_state);

    $element['ebt_settings']['pass_options_to_javascript'] = [
      '#type' => 'hidden',
      '#value' => TRUE,

    return $element;

   * {@inheritdoc}
  public function massageFormValues(array $values, array $form, FormStateInterface $form_state) {
    foreach ($values as &$value) {
      $value += ['ebt_settings' => []];
    return $values;


Now we can select this field widget for EBT Settings field:


Drupal settings

Let's resave our block and check drupalSettings javascript variable. Now all options from EBT Settings field will be passed in javascript:

javascript variable

At the end of block-revision-id-* we have Block revision ID, so we have unique keys for all blocks. For inine blocks we use plugin ID plugin-id-block-contentd202c374-f31b-4f7e-8a0d-12842a1422ff. So we have unique IDs in Layout Builder.

FlipDown plugin has option for light/dark theme, so let's pass this setting field in our field widget EbtSettingsCountDownWidget:

    $element['ebt_settings']['color_theme'] = [
      '#title' => $this->t('Color theme'),
      '#type' => 'radios',
      '#options' => [
        'dark' => $this->t('Dark'),
        'light' => $this->t('Light'),
      '#default_value' => $items[$delta]->ebt_settings['color_theme'] ?? 'dark',
      '#description' => $this->t('Select color theme for countdown'),
      '#weight' => '3',

Theme color

Then we can get theme color value in javascript:

javascript value

Step 4. Initialize FlipDown plugin for EBT Countdown block

We passed variables from settings to javascript, but we need to pass also Date value from content to javascript. We will create empty div with attribute data-date="", where we will place date and time from Date field. For inline-block (blocks in Layout Builder) we will use block_revision_id to define unique ID for block:


      class="ebt-countdown-date ebt-countdown-inline-block flipdown"
      id="block-id-{{ configuration.block_revision_id }}"
      data-date="{{ content.field_ebt_countdown_date[0]['#attributes']['datetime']|date('U') }}">

Inline Block countdown

For content blocks from Block Layout page we will use plugin_id:


      class="ebt-countdown-date ebt-countdown-block-content flipdown"
      id="block-id-{{ plugin_id|clean_class }}"
      data-date="{{ content.field_ebt_countdown_date[0]['#attributes']['datetime']|date('U') }}">

EBT countdown block

If you are not sure about where field data is, you can install Twig Debugger module and print content.field_ebt_countdown_date on the page {{ dump(content.field_ebt_countdown_date) }}:

And we used date('U') twig filter to transform date to timestamp.

Now we can include custom javascript and initialize FlipDown.


(function ($, Drupal) {

   * EBT Countdown behavior.
  Drupal.behaviors.ebtCountDown = {
    attach: function (context, settings) {
      var countdowns = once('ebt-countdown-block', '.ebt-countdown-date', context);
      countdowns.forEach(function(countdown) {
        var countdownTimestamp = parseInt(countdown.getAttribute('data-date'));
        var countdownId = countdown.getAttribute('id');
        new FlipDown(countdownTimestamp, countdownId, {
          theme: "dark",

})(jQuery, Drupal);

Don't forget to clear caches to see updates. After that FlipDown should be working on the page:


Step 5. Styling new EBT Countdown block

As you see even default FlipDown styles are not working great. There are two lines of numbers even on desktop. But we can easily fix it with custom styles. You can copy gulpfile.js and package.json files for compiling scss to css from EBT Counter module:


// --------------------------------------------------
// Load Plugins
// --------------------------------------------------

var gulp = require('gulp'),
    sass = require('gulp-dart-scss'),
    postcss = require("gulp-postcss"),
    autoprefixer = require("autoprefixer"),
    cssnano = require("cssnano"),
    notify = require('gulp-notify'),
    sassUnicode = require('gulp-sass-unicode');

var config = {
    // main scss files that import partials
    scssSrc: 'scss/*.scss',
    // all scss files in the scss directory
    allScss: 'scss/**/*.scss',
    // the destination directory for our css
    cssDest: 'css/',
    // all js files the js directory
    allJs: 'assets/js/**/*.js',
    // all img files
    allImgs: 'assets/img/**/*'

// Define tasks after requiring dependencies
function style() {

  return gulp.src(config.allScss)

  gulp.task('sass:watch', function () {'./scss/**/*.scss', ['sass']);

// Expose the task by exporting it
// This allows you to run it from the commandline using
// $ gulp style = style;

function watch(){
    // takes in the location of the files to watch for changes
    // and the name of the function we want to run on change'scss/**/*.scss', style)

// Don't forget to expose the task! = watch


  "name": "ebt_styles",
  "version": "1.0.0",
  "description": "Run npm install and then gulp watch",
  "main": "gulpfile.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "autoprefixer": "^10.2.5",
    "cssnano": "^5.0.2",
    "gulp": "^4.0.2",
    "gulp-dart-scss": "^1.1.0",
    "gulp-notify": "^4.0.0",
    "gulp-postcss": "^9.0.0",
    "gulp-sass-unicode": "^1.0.5",
    "gulp-sourcemaps": "^3.0.0"
  "dependencies": {
    "cucumber": "*",
    "postcss": "^8.2.13"

package-lock.json file will be generated after you run:

npm install

And you can start gulp task with:

gulp watch

Now we will add scss files:


.flipdown {
  width: 580px;

So flipdown.css file will be automatically generated from flipdown.scss. And we can include .css file in .libraries.yml:

      /libraries/flipdown/dist/flipdown.min.css: { minified: true }
      css/flipdown.css: { }

Let's clear cache and see results:

EBT countdown

Now it looks better!

Can we use plain css without compiling from scss?

Yes, we can, but writing scss is more convient for the most developers.


Step 6. Extend settings form with FlipDown plugin options

FlipDown plugin has few options to change display:

  • theme
  • headings

We already created new field widget for EBT Settings EbtSettingsCountDownWidget, now we will extend this form with new fields:


    $element['ebt_settings']['color_theme'] = [
      '#title' => $this->t('Color theme'),
      '#type' => 'radios',
      '#options' => [
        'dark' => $this->t('Dark'),
        'light' => $this->t('Light'),
      '#default_value' => $items[$delta]->ebt_settings['color_theme'] ?? 'dark',
      '#description' => $this->t('Select color theme for countdown'),
      '#weight' => '3',

    $element['ebt_settings']['styles'] = [
      '#title' => $this->t('Styles'),
      '#type' => 'radios',
      '#options' => [
        'default' => $this->t('Default'),
        'new_year' => $this->t('New Year'),
      '#default_value' => $items[$delta]->ebt_settings['styles'] ?? 'default',
      '#description' => $this->t('Select special style for countdown'),
      '#weight' => '4',

    $element['ebt_settings']['heading_days'] = [
      '#title' => $this->t('Heading Days'),
      '#type' => 'textfield',
      '#default_value' => $items[$delta]->ebt_settings['heading_days'] ?? $this->t('Days'),
      '#description' => $this->t('Header for Days counter'),
      '#weight' => '5',

    $element['ebt_settings']['heading_hours'] = [
      '#title' => $this->t('Heading Hours'),
      '#type' => 'textfield',
      '#default_value' => $items[$delta]->ebt_settings['heading_hours'] ?? $this->t('Hours'),
      '#description' => $this->t('Header for Hours counter'),
      '#weight' => '6',

    $element['ebt_settings']['heading_minutes'] = [
      '#title' => $this->t('Heading Minutes'),
      '#type' => 'textfield',
      '#default_value' => $items[$delta]->ebt_settings['heading_minutes'] ?? $this->t('Minutes'),
      '#description' => $this->t('Header for Minutes counter'),
      '#weight' => '7',

    $element['ebt_settings']['heading_seconds'] = [
      '#title' => $this->t('Heading Seconds'),
      '#type' => 'textfield',
      '#default_value' => $items[$delta]->ebt_settings['heading_seconds'] ?? $this->t('Seconds'),
      '#description' => $this->t('Header for Seconds counter'),
      '#weight' => '8',
    return $element;

Update settings form

So we can use Headings for translations:

Add headings

We have all EBT settings values in javascript, so I just added ID in template for easier getting right unique key.

.setAttribute('id', 'plugin-id-' ~ plugin_id|clean_class)
.setAttribute('id', 'block-revision-id-' ~ configuration.block_revision_id)

Options keys have pattern the same with IDs:


  // Use plugin_id for Block content.
  $build['#attached']['drupalSettings'][$bundle]['block-revision-id-' . $revision_id[0]['value']] = $block_options;
  $uuid = $entity->get('uuid')->getValue();
  $block_options = [
    'blockClass' => 'plugin-id-block-content' . $uuid[0]['value'],
    'options' => $options,
  $build['#attached']['drupalSettings'][$bundle]['plugin-id-block-content' . $uuid[0]['value']] = $block_options;

So we could use options from javascript drupalSettings:

EBT settings


(function ($, Drupal) {

   * EBT Countdown behavior.
  Drupal.behaviors.ebtCountDown = {
    attach: function (context, settings) {
      var countdowns = once('ebt-countdown-block', '.ebt-countdown-date', context);
      countdowns.forEach(function(countdown) {
        // Get block ID.
        var countdownWrapper = countdown.closest('.ebt-block-countdown');
        var countdownWrapperId = countdownWrapper.getAttribute('id');
        // Get block EBT settings.
        var ebtOptions = drupalSettings['ebtCountdown'][countdownWrapperId];
        // Prepare options for javascript plugin.
        var countdownTimestamp = parseInt(countdown.getAttribute('data-date'));
        var countdownId = countdown.getAttribute('id');
        // Init javascript plugin.
        new FlipDown(countdownTimestamp, countdownId, {
          theme: ebtOptions['options']['color_theme'],
          headings: [

})(jQuery, Drupal);

I also added $element['ebt_settings']['styles'] in field widget, so we will use it in template for adding new class for entire block.

  set classes = [
  'ebt-block-' ~ plugin_id|clean_class,
  'block-' ~ configuration.provider|clean_class,
  'block-' ~ plugin_id|clean_class,
  'plugin-id-' ~ plugin_id|clean_class,
{% if content.field_ebt_settings['#object'].field_ebt_settings.ebt_settings.styles == 'new_year' %}
  {{ attach_library('ebt_countdown/new_year') }}
{% endif %}

 It will include new_year library for New Year style, but now we need to create this library.


      css/new-year.css: { }

And here is styles for new-year block:


.ebt-block-countdown.new_year {
  background: url(../img/snowflakes.webp) center center repeat;

Here is result of adding new style:

New Year EBT block

You can add any number of styles for new or existing EBT modules. You can also suggest own styles for any EBT module, just create issue on for it:

Step 7. Export configs for EBT block type, paragraph type and fields

I think we finished with adding features to EBT Countdown, it's time to export configs and deploy changes on We should copy all configs related to EBT Countdown to /ebt_countdown/config/install folder.

After that enable EBT module on Extend page /admin/modules. New EBT block type and other settings will be installed from /config/install folder from config files:


EBT module configs

We don't need to add language.* configs, because some drupal sites have only one language and Language module can be disabled.

Usually I copy all files and checking that I have copy in config/install folder.

Copy of configs

Now we need to remove uuid and hashes from configs in config/install folder

Remove uuid

As we used another Drupal modules we should include them in .info file as dependencies.

Drupal dependencies


  - drupal:datetime

Step 8. Deploy on and testing

We created new project on before:

I will use 1.4.x branch as main branch, for consistancy with other EBT modules:

Drupal EBT module

So all releases will be started from 1.4.0 versions:

git tag 1.4.0
git push origin 1.4.0

You can also create -alpha, -beta version before creating stable 1.4.0 version.

We need to wait 10 days before module can be opted into security advisory coverage.

EBT Countdown

So we can test our new module and fix bugs.

Step 9. Add file

Don't forget to add file, you can see example in other EBT modules:

Thank you for using EBT modules! Feel free to ask or suggest any ideas:

Create an issue on

Contact EBT modules developer

Or message me on LinkedIn