Cookbook

Cookbook: Register a Custom HTML Builder Widget

2 min read

Notifal 3.0.0 lets plugins add reusable items to the HTML Builder Widgets palette.

A widget is an HTML shortcut, not a separate frontend component. Clicking or dragging it inserts default_html. The saved HTML remains the source of truth, and the root element's data-notifal-widget attribute lets the builder recognize the widget later.

Hook reference: Hooks: Templates and Rendering.

Use FilterHooks::TEMPLATE_HTML_BUILDER_WIDGETS, which maps to:

notifal/template/html_builder/widgets

PHP registration is recommended because Notifal sends the same definition to:

  • The HTML Builder widget palette
  • Noti (hosted AI) in the HTML Builder
  • The OnPage notification AI wizard

Widget definition fields

Every field must be a non-empty string.

KeyPurpose
idUnique stable identifier; should use lowercase letters and hyphens
labelTranslated name shown in the palette
categoryStable category key, such as basic, media, actions, or layout
category_labelTranslated category heading
iconNotifal icon name shown on the widget card
descriptionShort translated explanation
default_htmlHTML inserted by click or drag
ai_snippetHTML structure supplied to AI prompts

The root element in both HTML fields should use:

data-notifal-widget="your-widget-id"

The attribute value must exactly match id.

Complete plugin example

<?php
/**
 * Plugin Name: My Notifal HTML Builder Widgets
 * Description: Registers a reusable promo badge in the Notifal HTML Builder.
 * Version: 1.0.0
 * Requires PHP: 7.4
 */

defined( 'ABSPATH' ) || exit;

use Notifal\Infrastructure\WordPress\Hooks\ActionHooks;
use Notifal\Infrastructure\WordPress\Hooks\FilterHooks;

/**
 * Attach the extension after all plugins have loaded.
 *
 * @return void
 */
function my_notifal_bootstrap_html_builder_widgets(): void {
    // Stop when the base plugin or its hook constants are unavailable.
    if ( ! class_exists( ActionHooks::class ) || ! class_exists( FilterHooks::class ) ) {
        return;
    }

    // Wait until Notifal has initialized all base services.
    add_action(
        ActionHooks::PLUGIN_INIT,
        'my_notifal_register_html_builder_widgets'
    );
}

// Run after WordPress has loaded every active plugin.
add_action(
    'plugins_loaded',
    'my_notifal_bootstrap_html_builder_widgets',
    20
);

/**
 * Register the HTML Builder widget filter.
 *
 * @return void
 */
function my_notifal_register_html_builder_widgets(): void {
    // Attach the palette filter before the HTML Builder assets are prepared.
    add_filter(
        FilterHooks::TEMPLATE_HTML_BUILDER_WIDGETS,
        'my_notifal_add_promo_badge_widget'
    );
}

/**
 * Add a promo badge widget to the HTML Builder.
 *
 * @param array<int, array<string, string>> $widgets Existing widget definitions.
 * @return array<int, array<string, string>> Filtered widget definitions.
 */
function my_notifal_add_promo_badge_widget( array $widgets ): array {
    // Keep inserted HTML self-contained and use a namespaced class.
    $widget_html = '<span data-notifal-widget="promo-badge" class="my-notifal-promo-badge">New</span>';

    // Append a complete definition; incomplete rows are discarded by Notifal.
    $widgets[] = [
        'id'             => 'promo-badge',
        'label'          => __( 'Promo Badge', 'my-notifal-widgets' ),
        'category'       => 'basic',
        'category_label' => __( 'Basic', 'my-notifal-widgets' ),
        'icon'           => 'layers',
        'description'    => __( 'Add a small promotional badge.', 'my-notifal-widgets' ),
        'default_html'   => $widget_html,
        'ai_snippet'     => $widget_html,
    ];

    // Always return the complete widget list.
    return $widgets;
}
Important: Do not instantiate Notifal services directly. Use hook constants and register after notifal/initialized.

Add widget styles

The widget inserts HTML only. Add its CSS from your plugin using normal WordPress enqueue APIs.

Use a namespaced class to prevent conflicts:

.my-notifal-promo-badge {
  display: inline-flex;
  padding: 4px 8px;
  border-radius: 999px;
  background: #7e2bd2;
  color: #ffffff;
  font-size: 12px;
  font-weight: 700;
}

For production, enqueue your built stylesheet. Do not print inline <style> or <script> blocks from the registration callback.

If your widget must be portable inside exported template HTML, include the relevant CSS in the template's existing <style> block instead of relying on a site-specific plugin asset.

Write a useful AI snippet

ai_snippet teaches AI tools how and when to output the widget's structure.

Guidelines:

  • Keep the widget ID and required classes identical to default_html.
  • Include required data attributes.
  • Use realistic placeholder copy or supported Notifal tags.
  • Do not include secrets, site-specific IDs, nonces, or private URLs.
  • Prefer semantic HTML for readable text (p, span, headings, links, and buttons).

The AI may choose free HTML when your custom widget does not fit the requested design.

Optional JavaScript filter

The client registry also applies:

notifal.htmlBuilder.widgets

through wp.hooks.

/**
 * Add a client-only widget definition.
 *
 * @param {Array<Object>} widgets Existing HTML Builder widgets.
 * @returns {Array<Object>} Updated widget list.
 */
const addPromoBadgeWidget = (widgets) => {
  // Append a complete definition matching the PHP schema.
  widgets.push({
    id: 'promo-badge',
    label: 'Promo Badge',
    category: 'basic',
    category_label: 'Basic',
    icon: 'layers',
    description: 'Add a small promotional badge.',
    default_html:
      '<span data-notifal-widget="promo-badge" class="my-notifal-promo-badge">New</span>',
    ai_snippet:
      '<span data-notifal-widget="promo-badge" class="my-notifal-promo-badge">New</span>',
  });

  // Return the same array after adding the widget.
  return widgets;
};

// Register before the HTML Builder React application resolves its widget list.
window.wp.hooks.addFilter(
  'notifal.htmlBuilder.widgets',
  'my-notifal-widgets/promo-badge',
  addPromoBadgeWidget
);

Use the PHP filter when possible. A client-only widget is available in that browser session, but its definition is not automatically included in server-generated AI prompt configuration.

Your script must load on the HTML Builder screen before the React application initializes. The builder declares WordPress wp-hooks as a dependency.

Sanitization and security

Notifal sanitizes the resulting template when it is saved.

  • data-notifal-widget is preserved for supported HTML elements.
  • Inline on* event handlers are removed.
  • javascript: URLs are rejected.
  • PHP code is removed.
  • Script permissions follow the user's unfiltered_html capability.

Treat widget HTML as output:

  • Escape dynamic URLs with esc_url().
  • Escape text with esc_html().
  • Escape attribute values with esc_attr().
  • Do not put request data directly into default_html.
  • Do not use the widget registration filter to process form submissions.

User guide: HTML Sanitization and Allowed HTML.

Modify or remove a widget

Definitions are ordinary array rows. Match by id.

/**
 * Remove the core spacer widget.
 *
 * @param array<int, array<string, string>> $widgets Existing widgets.
 * @return array<int, array<string, string>> Widgets without the spacer.
 */
function my_notifal_remove_spacer_widget( array $widgets ): array {
    // Keep rows whose stable ID is not spacer.
    return array_values(
        array_filter(
            $widgets,
            static function ( array $widget ): bool {
                // Read the ID defensively before comparing it.
                $widget_id = isset( $widget['id'] )
                    ? sanitize_key( (string) $widget['id'] )
                    : '';

                // Return false only for the core spacer row.
                return 'spacer' !== $widget_id;
            }
        )
    );
}

Removing a definition does not remove previously inserted HTML. Existing templates remain normal HTML; they simply stop showing that widget badge if its identifying structure is changed.

Troubleshooting

ProblemCause or fix
Widget does not appearConfirm all eight fields are non-empty strings
Widget appears in the wrong groupUse the same category key and category_label as the intended group
Widget inserts but has no badgePut matching data-notifal-widget on the root element
Widget is missing from AI guidanceRegister it through PHP and provide a non-empty ai_snippet
Markup changes after saveReview HTML Builder sanitization rules and the current user's capability
JavaScript widget does not appearLoad the filter script before the HTML Builder app initializes

Cookbook series

  1. Cookbook: Modify Notification Before Display
  2. Cookbook: Add Custom Dynamic Tag
  3. Cookbook: Customize Template Import
  4. Cookbook: Extend Display Rules
  5. Cookbook: Register a Custom HTML Builder Widget (you are here)