---
url: 'https://dxpr.com/hc/documentation/learn-dxpr-builder/development/custom-elements'
title: 'Creating Custom Elements | DXPR'
author:
  name: docs-sync
  url: 'https://dxpr.com/users/docs-sync'
date: '2026-10-23T14:31:17+00:00'
copyright: 'Copyright DXPR. All rights reserved.'
type: article
summary: 'Register your own content elements in JavaScript'
tags:
  - 'Build on it'
image: 'https://dxpr.com/themes/custom/dxpr_com/images/dxpr-share-card.jpg'
published: true
og:
  site_name: DXPR
  updated_time: '2026-10-23T14:31:17+0000'
  'image:alt': 'DXPR, powered by Drupal: AI-powered Drupal for marketers.'
schema:
  '@context': 'https://schema.org'
  '@graph':
    -
      mainEntity: {  }
    -
      '@type': TechArticle
      headline: 'Creating Custom Elements'
      description: 'Register your own content elements in JavaScript'
      image:
        '@type': ImageObject
        url: 'https://dxpr.com/themes/custom/dxpr_com/images/dxpr-share-card.jpg'
        width: '1200'
        height: '630'
      datePublished: '2026-10-23T14:31:17+0000'
      dateModified: '2026-10-23T14:31:17+0000'
      author:
        '@type': Organization
        '@id': 'https://dxpr.com/#organization'
        name: DXPR
        url: 'https://dxpr.com/'
        logo:
          '@type': ImageObject
          url: 'https://dxpr.com/themes/custom/dxpr_com/dxpr-logo-dark.svg'
          width: '1348'
          height: '501'
      publisher:
        '@type': Organization
        '@id': 'https://dxpr.com/#organization'
        name: DXPR
        url: 'https://dxpr.com/'
        logo:
          '@type': ImageObject
          url: 'https://dxpr.com/themes/custom/dxpr_com/dxpr-logo-dark.svg'
          width: '1348'
          height: '501'
      mainEntityOfPage: 'https://dxpr.com/hc/documentation/learn-dxpr-builder/development/custom-elements'
      articleSection: 'Build on it'
    -
      '@type': WebPage
      '@id': 'https://dxpr.com/hc/documentation/learn-dxpr-builder/development/custom-elements'
      description: 'Register your own content elements in JavaScript'
      breadcrumb:
        '@type': BreadcrumbList
        itemListElement:
          -
            '@type': ListItem
            position: 1
            name: Home
            item: 'https://dxpr.com/'
      publisher:
        '@type': Organization
        '@id': 'https://dxpr.com/#organization'
        name: DXPR
      inLanguage: en
---
[Help Center](/hc)/[Documentation](/hc/documentation) /[Learn DXPR Builder](/hc/documentation/learn-dxpr-builder) /[Extend](/hc/documentation/learn-dxpr-builder/extend) /[Build on it](/hc/documentation/learn-dxpr-builder/extend/build-on-it) # Creating Custom Elements.

Register your own content elements in JavaScript

 Learn DXPR BuilderExtendBuild on it

   ## In this guide

 Related guides

- [Hooks API](/hc/documentation/learn-dxpr-builder/development/hooks-api)
- [Architecture](/hc/documentation/learn-dxpr-builder/development/architecture)

Every DXPR Builder element is a JavaScript object pushed onto `window.dxprBuilder.dxpr_elements`. The editor turns each object into an element class with the Style and Animation tabs already attached. This page shows how the shipped elements are defined in `dxpr_builder/dxpr_elements.js`, how a module adds its own definition to the editor, and what the resulting HTML looks like. There are no server-side Twig templates per element; elements render in the browser.

## How the editor registers elements

The editor loads six scripts in this order, defined in `dxpr_builder_library_info_build()` in `dxpr_builder.module` as the `dxpr_builder/editor.builder` library:

1. `dxpr_templates.js`: client-side twig.js templates compiled from `dxpr_builder/templates/`.
2. `dxpr_global.js`: shared helpers and the CKEditor 5 integration.
3. `dxpr_events.js`
4. `dxpr_param_types.js`: pushes one object per input type onto `window.dxprBuilder.dxpr_param_types`.
5. `dxpr_elements.js`: creates the `window.dxprBuilder.dxpr_elements`array and pushes one definition per element.
6. `dxpr_builder.js`: the editor. When this file executes it calls `create_dxpr_elements()`, which loops over `window.dxprBuilder.dxpr_elements` and for each definition:
    - calls `register_animated_element(base, is_container, Class)`, so the class extends `AnimatedElement` (Style tab, Animation tab, HTML ID, HTML classes and placement parameters);
    - appends the inherited parameters to the definition's `params`;
    - converts every parameter through `make_param_type()`, which matches `param.type` against the registered parameter types;
    - mixes the definition's properties into the class prototype.

A definition therefore has to be on the array after `dxpr_elements.js` has run and before `dxpr_builder.js` runs. All six files load with the `defer` attribute, so the document order of the script tags is the execution order.

## The element definition

This is the Alert element from `dxpr_builder/dxpr_elements.js`(source: `dxpr_builder/build/dxpr-elements/elements/az_alert.js`), trimmed to the properties every element needs. The original also sets `el_class`, an `onCancel` handler and a `focusout` listener.

```javascript
window.dxprBuilder.dxpr_elements.push({
  base: "az_alert",
  name: Drupal.t("Alert"),
  nameEn: "Alert",
  icon: "az-custom-icon az-alert-icon",
  largeSettingsWindow: true,
  hidden: false,
  params: [
    {
      type: "textarea",
      heading: Drupal.t("Message"),
      param_name: "content",
      value: Drupal.t(
        "This is a placeholder text. Your actual content will go here. Edit this to include your own information.",
      ),
    },
    {
      type: "dropdown",
      heading: Drupal.t("Type"),
      param_name: "type",
      description: Drupal.t("Select message type."),
      getText: (opt) => opt.text,
      getValue: (opt) => opt.value,
      getDefault: (param) => param.value["alert-success"].value,
      value: {
        "alert-success": { text: Drupal.t("Success"), value: "alert-success" },
        "alert-info": { text: Drupal.t("Info"), value: "alert-info" },
        "alert-warning": { text: Drupal.t("Warning"), value: "alert-warning" },
        "alert-danger": { text: Drupal.t("Danger"), value: "alert-danger" },
      },
      passthrough: true,
    },
  ],
  show_settings_on_create: true,
  has_content: true,
  render(...args) {
    const state = {
      class: `az-element az-alert alert ${this.attrs.type} ${this.get_el_classes()} ${this.get_content_classes()}`,
      style: this.attrs.style,
      message: this.attrs.content,
    };
    this.dom_element = this.baseclass.prototype.render_template(
      "elements/alert/alert",
      state,
    );
    this.baseclass.prototype.render.apply(this, args);
  },
  save_history() {
    const container = this.get_my_container();
    container.save_container_history(Drupal.t("Edit alert"));
  },
});

```

| Property | Purpose |
|---|---|
| `base` | Machine name. It is written to the saved HTML as `data-azb="az_alert"` and used to find the class when the page is parsed again. Keep the `az_` prefix. |
| `name`, `nameEn` | Label in the element picker and its untranslated form. |
| `icon` | CSS classes for the picker icon. The shipped icons are defined in `dxpr_builder/sass/dxpr-builder-backend/components/_custom-icons.scss`; a custom element needs its own CSS for its icon class. |
| `category` | Picker tab. The layout elements use `Drupal.t("Layout")`; blocks and views use `"CMS"`. Omit it for the default content tab. |
| `is_container` | `true` when the element holds other elements. |
| `largeSettingsWindow` | Opens the settings dialog at the larger size. |
| `hidden` | Hides the element from the picker while keeping it renderable. |
| `params` | Settings shown in the dialog. See below. |
| `show_settings_on_create` | Opens the settings dialog as soon as the element is dropped. |
| `has_content` | The element has an editable content area (`dom_content_element`). |
| `render(...args)` | Builds `this.dom_element` from `this.attrs`, then calls the base `render`. |
| `save_history()` | Pushes an undo entry with a label. |
| `parse_attrs_from_dom(element)` | Optional. Reads parameter values back out of saved markup, used together with `skip_data_azb` and `skip_params` by elements such as Image that store values in the `<img>` attributes instead of `data-azat-*`. |

### Parameters

Each entry in `params` has:

| Key | Meaning |
|---|---|
| `type` | One of the registered parameter types (next section). |
| `heading` | Field label. |
| `param_name` | Key in `this.attrs` and, in saved HTML, the `data-azat-<param_name>` attribute. `content` is special: it is the element body, not an attribute. |
| `value` | Default value. For `dropdown` it is an object of options with `getText`, `getValue` and `getDefault` callbacks. |
| `description` | Help text under the field. |
| `tab` | Groups the field into a named accordion, for example `Drupal.t("SEO")`. |
| `can_be_empty` | Store an empty string instead of dropping the attribute. |
| `safe` | Skip URL encoding of the stored value. |

The parameter types are the `type` values pushed onto `window.dxprBuilder.dxpr_param_types` in `dxpr_builder/dxpr_param_types.js`: `boxmodel`, `checkbox`, `checkboxes`, `colorpicker`, `datetime`, `dropdown`, `dxb_slider`, `html`, `icon`, `image`, `images`, `link`, `links`, `numericfield`, `row_layout`, `saved_datetime`, `social_links`, `textarea`, `textfield` and `video`. A type you do not register falls back to a plain text input (`BaseParamType`).

Do not reuse a `param_name` that every element inherits, such as `color`, `background`, `el_class`, `hash` or the `an_*` animation names. The Style tab already owns `color`, so a second parameter with that name is never written to `data-azat-color`.

### What gets saved

`update_data()` in `dxpr_builder.js` writes the element to HTML:

- `data-azb="<base>"` on the root element;
- `data-azat-<param_name>="<value>"` for every parameter whose value differs from its default, URL-encoded unless the parameter sets `safe: true`;
- the Style tab parameters as inline `style` and classes, never as `data-azat-*` attributes;
- `data-azcnt="true"` on the content element when `has_content` is set.

Viewers who do not get the editor receive this HTML without its `data-az*` attributes: `DxprBuilderService::parseDocumentForCleanup()`strips them, except a fixed list it keeps on animated elements and on built-in dynamic elements such as Carousel. If the element needs JavaScript or CSS for visitors, put a `data-dxpr-builder-libraries="<key>"` attribute on its root: `parseDocumentForTemplateLibrary()` attaches `dxpr_builder/elements.<key>` for every key without a slash. The keys are the `elements.*` libraries in `dxpr_builder.libraries.yml`; a key with a slash is only honoured when it is the `library` of a registered icon set.

## Minimal working example

The example adds a "Notice" element that renders a paragraph with a chosen colour. It builds its DOM directly because the compiled twig.js templates in `dxpr_templates.js` are part of the editor bundle and cannot be extended by another module.

1. Create `mymodule/js/notice-element.js`:

    ```javascript
    window.dxprBuilder.dxpr_elements.push({
      base: "az_notice",
      name: Drupal.t("Notice"),
      nameEn: "Notice",
      icon: "az-custom-icon az-alert-icon",
      params: [
        {
          type: "textfield",
          heading: Drupal.t("Text"),
          param_name: "text",
          value: Drupal.t("Notice text"),
        },
        {
          type: "colorpicker",
          heading: Drupal.t("Colour"),
          param_name: "notice_color",
          value: "#0d6efd",
        },
      ],
      show_settings_on_create: true,
      render(...args) {
        const el = document.createElement("div");
        el.className = `az-element az-notice ${this.get_el_classes()}`;
        if (this.attrs.style) {
          el.setAttribute("style", this.attrs.style);
        }
        const p = document.createElement("p");
        p.style.color = this.attrs.notice_color;
        p.textContent = this.attrs.text;
        el.appendChild(p);
        this.dom_element = el;
        this.baseclass.prototype.render.apply(this, args);
      },
      save_history() {
        this.get_my_container().save_container_history(Drupal.t("Edit notice"));
      },
    });

    ```
2. Insert the file into the editor library between `dxpr_elements.js` (weight `-4`) and `dxpr_builder.js` (weight `-3`) in `mymodule.module`:

    ```php
    /**
     * Implements hook_library_info_alter().
     */
    function mymodule_library_info_alter(array &$libraries, string $extension): void {
      if ($extension === 'dxpr_builder' && isset($libraries['editor.builder'])) {
        $path = '/' . \Drupal::service('extension.list.module')->getPath('mymodule');
        $libraries['editor.builder']['js'][$path . '/js/notice-element.js'] = [
          'weight' => -3.5,
          'preprocess' => FALSE,
          'attributes' => ['defer' => TRUE],
        ];
      }
    }

    ```

    `preprocess: FALSE` and `defer` match the entries the module defines for its own files, so the file keeps its place in the sequence whether editor assets come from the CDN or from a local build.
3. Clear the cache and open a builder page:

    ```bash
    drush cr

    ```
4. Add the element from the picker, set its text and colour, and save. The field stores:

    ```html
    <div class="az-element az-notice" id="..." data-azb="az_notice"
         data-azat-text="Hello" data-azat-notice_color="%23198754">
      <p style="color: #198754;">Hello</p>
    </div>

    ```

    A visitor gets the same markup without the `data-az*`attributes. The paragraph keeps its inline colour, so no visitor library is needed for this element.

## Limits to know about

- **Profiles do not list custom elements.** The element checklist in a builder profile comes from a fixed list in `src/Form/DxprBuilderProfileForm.php` and `src/Service/Handler/ProfileHandler.php`. Elements outside that list are never hidden by a profile.
- **Icons for the picker** need CSS from your module; the `az-custom-icon` classes shipped by the builder cover the shipped elements only.
- **`DxprBuilderService::getDxprElementsFolders()`** and its `hook_dxpr_builder_elements_folders_alter()` return a list of `elements` folders in themes, but nothing in the editor reads that list. It is not a registration route.
- **Style and Animation tabs** are added for you by `register_animated_element()`; do not redefine those parameters.

## What's next?

- [Hooks API](/hc/documentation/learn-dxpr-builder/development/hooks-api) for the PHP hooks that add utility classes and icon sets.
- [Architecture](/hc/documentation/learn-dxpr-builder/development/architecture) for how the editor and the field formatter fit together.