• Skip to secondary menu
  • Skip to main content
  • Skip to primary sidebar
  • Home
  • Projects
  • Products
  • Themes
  • Tools
  • Request for Quote

Vengala Vinay

Having 12+ Years of Experience in Software Development

  • Home
  • WordPress
  • PHP
    • Codeigniter
  • Django
  • Magento
  • Selenium
  • Server
Home » How to build custom Elementor custom widgets extensions utilizing modern WordPress Options API schemas

How to build custom Elementor custom widgets extensions utilizing modern WordPress Options API schemas

Leveraging the WordPress Options API for Advanced Elementor Widget Configuration

For enterprise-level WordPress deployments and complex plugin architectures, extending Elementor requires more than just basic widget functionality. A robust configuration system is paramount for maintainability, scalability, and user experience. This post details how to build custom Elementor widget extensions that harness the power and flexibility of the WordPress Options API, specifically focusing on modern schema definitions for structured data management.

Structuring Widget Settings with Options API Schemas

The traditional approach to storing widget settings often involves direct database entries or simple `update_option()` calls. However, for complex widgets with numerous fields, nested structures, or conditional logic, this becomes unwieldy. The WordPress Options API, when combined with a well-defined schema, offers a more organized and programmatic way to manage these settings. We’ll define our schema using PHP arrays, which can then be translated into UI elements within Elementor’s controls.

Defining a Widget Schema

Consider a hypothetical “Advanced Call to Action” widget that requires settings for text, button URL, button text, background color, and an optional image. We can represent this configuration using a structured PHP array that mirrors the expected data types and nesting.

This schema will serve as the blueprint for both the Elementor controls and the data validation/sanitization process.

<?php
/**
 * Schema for the Advanced Call to Action widget settings.
 *
 * @return array Widget settings schema.
 */
function get_advanced_cta_widget_schema() {
    return [
        'settings' => [
            'cta_title' => [
                'type'    => 'text',
                'label'   => esc_html__( 'Call to Action Title', 'your-text-domain' ),
                'default' => '',
                'sanitize_callback' => 'sanitize_text_field',
            ],
            'cta_description' => [
                'type'    => 'textarea',
                'label'   => esc_html__( 'Call to Action Description', 'your-text-domain' ),
                'default' => '',
                'sanitize_callback' => 'wp_kses_post',
            ],
            'cta_button_text' => [
                'type'    => 'text',
                'label'   => esc_html__( 'Button Text', 'your-text-domain' ),
                'default' => esc_html__( 'Learn More', 'your-text-domain' ),
                'sanitize_callback' => 'sanitize_text_field',
            ],
            'cta_button_url' => [
                'type'    => 'url',
                'label'   => esc_html__( 'Button URL', 'your-text-domain' ),
                'default' => '#',
                'sanitize_callback' => 'esc_url_raw',
            ],
            'cta_background_color' => [
                'type'    => 'color',
                'label'   => esc_html__( 'Background Color', 'your-text-domain' ),
                'default' => '#f0f0f0',
                'sanitize_callback' => 'sanitize_hex_color',
            ],
            'cta_image' => [
                'type'    => 'media',
                'label'   => esc_html__( 'Optional Image', 'your-text-domain' ),
                'default' => [
                    'id' => 0,
                    'url' => '',
                ],
                'sanitize_callback' => [ 'Your_Widget_Class', 'sanitize_media_field' ], // Custom sanitizer
            ],
        ],
        'options_group' => 'advanced_cta_widget_options', // Unique identifier for the options group
        'option_name'   => 'advanced_cta_widget_settings', // The actual option name in wp_options
    ];
}

Integrating Schema with Elementor Controls

Elementor’s widget development framework allows us to dynamically generate controls based on a schema. This is typically done within the `_register_controls()` method of your custom widget class. By iterating through the defined schema, we can instantiate the appropriate Elementor control types.

<?php
use Elementor\Widget_Base;
use Elementor\Controls_Manager;
use Elementor\Core\Schemes\Typography; // Example for typography scheme

class Your_Advanced_CTA_Widget extends Widget_Base {

    // ... other widget methods ...

    protected function _register_controls() {
        $schema = get_advanced_cta_widget_schema();
        $option_name = $schema['option_name'];

        // Group controls for better organization
        $this->start_controls_section(
            'section_cta_content',
            [
                'label' => esc_html__( 'Call to Action Content', 'your-text-domain' ),
                'tab'   => Controls_Manager::TAB_CONTENT,
            ]
        );

        foreach ( $schema['settings'] as $key => $field_config ) {
            $control_args = [
                'label' => $field_config['label'],
                'type'  => $this->get_elementor_control_type( $field_config['type'] ), // Helper to map schema types to Elementor types
                'default' => $field_config['default'],
                'placeholder' => isset( $field_config['placeholder'] ) ? $field_config['placeholder'] : '',
                'condition' => isset( $field_config['condition'] ) ? $field_config['condition'] : [],
            ];

            // Handle specific control types that require different arguments
            if ( 'media' === $field_config['type'] ) {
                $control_args['show_label'] = false; // Often media controls don't need a separate label
                $control_args['type'] = Controls_Manager::MEDIA;
            } elseif ( 'color' === $field_config['type'] ) {
                $control_args['scheme'] = [
                    'type' => Typography::COLOR,
                    'value' => isset( $field_config['value'] ) ? $field_config['value'] : '',
                ];
            }

            // Add the control
            $this->add_control( $key, $control_args );
        }

        $this->end_controls_section();

        // Add a section for styling if needed, potentially also schema-driven
        $this->start_controls_section(
            'section_cta_style',
            [
                'label' => esc_html__( 'Styling', 'your-text-domain' ),
                'tab'   => Controls_Manager::TAB_STYLE,
            ]
        );

        $this->add_control(
            'cta_text_color',
            [
                'label' => esc_html__( 'Text Color', 'your-text-domain' ),
                'type'  => Controls_Manager::COLOR,
                'scheme' => [
                    'type' => Typography::COLOR,
                    'value' => Typography::COLOR,
                ],
                'selectors' => [
                    '{{WRAPPER}} .advanced-cta-title' => 'color: {{VALUE}}',
                    '{{WRAPPER}} .advanced-cta-description' => 'color: {{VALUE}}',
                ],
            ]
        );

        $this->add_control(
            'cta_button_color',
            [
                'label' => esc_html__( 'Button Background Color', 'your-text-domain' ),
                'type'  => Controls_Manager::COLOR,
                'selectors' => [
                    '{{WRAPPER}} .advanced-cta-button' => 'background-color: {{VALUE}};',
                ],
            ]
        );

        $this->end_controls_section();
    }

    /**
     * Helper to map schema types to Elementor control types.
     *
     * @param string $schema_type The schema type.
     * @return string Elementor control type.
     */
    protected function get_elementor_control_type( $schema_type ) {
        $mapping = [
            'text'    => Controls_Manager::TEXT,
            'textarea' => Controls_Manager::TEXTAREA,
            'url'     => Controls_Manager::URL,
            'color'   => Controls_Manager::COLOR,
            'media'   => Controls_Manager::MEDIA,
            // Add more mappings as needed
        ];
        return $mapping[ $schema_type ] ?? Controls_Manager::TEXT; // Default to TEXT if not found
    }

    // ... rest of the widget class ...
}

Storing and Retrieving Settings via Options API

The key advantage of using the Options API is centralized storage. Instead of each widget instance storing its settings individually, we can store all settings for a particular widget type in a single option. This is particularly beneficial for widgets that are used repeatedly across a site, or for managing global widget settings.

When the widget is saved, Elementor’s internal mechanisms will typically save the control values. We need to hook into this process to ensure our settings are stored correctly using `update_option()`. Conversely, when rendering the widget, we retrieve these settings using `get_option()`.

<?php
// In your widget class, within the render() method or a helper function

protected function render() {
    $schema = get_advanced_cta_widget_schema();
    $option_name = $schema['option_name'];
    $widget_settings = get_option( $option_name, [] ); // Retrieve settings from the option

    // Ensure we have an array, even if the option is not set yet
    if ( ! is_array( $widget_settings ) ) {
        $widget_settings = [];
    }

    // Merge default values from schema if not present in stored settings
    foreach ( $schema['settings'] as $key => $field_config ) {
        if ( ! isset( $widget_settings[ $key ] ) && isset( $field_config['default'] ) ) {
            $widget_settings[ $key ] = $field_config['default'];
        }
    }

    // Extract individual settings for easier access
    $cta_title = isset( $widget_settings['cta_title'] ) ? $widget_settings['cta_title'] : '';
    $cta_description = isset( $widget_settings['cta_description'] ) ? $widget_settings['cta_description'] : '';
    $cta_button_text = isset( $widget_settings['cta_button_text'] ) ? $widget_settings['cta_button_text'] : '';
    $cta_button_url = isset( $widget_settings['cta_button_url'] ) ? $widget_settings['cta_button_url'] : '#';
    $cta_background_color = isset( $widget_settings['cta_background_color'] ) ? $widget_settings['cta_background_color'] : '#f0f0f0';
    $cta_image = isset( $widget_settings['cta_image'] ) ? $widget_settings['cta_image'] : [];

    // Sanitize and escape output for security
    $cta_title = esc_html( $cta_title );
    $cta_description = wp_kses_post( $cta_description );
    $cta_button_text = esc_html( $cta_button_text );
    $cta_button_url = esc_url( $cta_button_url );
    $cta_background_color = sanitize_hex_color( $cta_background_color ); // Already sanitized on save, but good practice

    // Render the HTML
    ?>
    <div class="advanced-cta-wrapper" style="background-color: <?php echo esc_attr( $cta_background_color ); ?> padding: 20px;">
        <h3 class="advanced-cta-title"><?php echo $cta_title; ?></h3>
        <div class="advanced-cta-description"><?php echo $cta_description; ?></div>
        <a href="<?php echo esc_url( $cta_button_url ); ?>" class="advanced-cta-button"><?php echo esc_html( $cta_button_text ); ?></a>
        <?php
        if ( ! empty( $cta_image['url'] ) ) {
            ?>
            <img src="<?php echo esc_url( $cta_image['url'] ); ?>" alt="CTA Image" style="max-width: 100px; margin-top: 15px;" />
            <?php
        }
        ?>
    </div>
    <?php
}

// Hook into Elementor's save process to update the option
add_action( 'elementor/element/parse_element', function( $element ) {
    if ( $element->get_type() === 'widget' && $element->get_widget_type() === 'your-advanced-cta-widget-slug' ) { // Replace with your widget slug
        $schema = get_advanced_cta_widget_schema();
        $option_name = $schema['option_name'];
        $new_settings = [];

        // Collect settings from the element's controls
        foreach ( $schema['settings'] as $key => $field_config ) {
            if ( $element->has_control( $key ) ) {
                $value = $element->get_settings( $key );

                // Apply specific sanitization based on schema
                if ( isset( $field_config['sanitize_callback'] ) ) {
                    if ( is_array( $field_config['sanitize_callback'] ) ) {
                        // For custom static methods like 'Your_Widget_Class', 'sanitize_media_field'
                        $new_settings[ $key ] = call_user_func( $field_config['sanitize_callback'], $value );
                    } else {
                        // For built-in WordPress sanitization functions
                        $new_settings[ $key ] = $field_config['sanitize_callback']( $value );
                    }
                } else {
                    // Default sanitization if none specified
                    $new_settings[ $key ] = sanitize_text_field( $value );
                }
            }
        }

        // Update the option in the database
        update_option( $option_name, $new_settings );
    }
} );

// Custom sanitization example for media fields
class Your_Widget_Class {
    public static function sanitize_media_field( $media_data ) {
        if ( ! is_array( $media_data ) || empty( $media_data['id'] ) ) {
            return [ 'id' => 0, 'url' => '' ];
        }
        $image_id = absint( $media_data['id'] );
        $image_url = esc_url_raw( $media_data['url'] );
        return [ 'id' => $image_id, 'url' => $image_url ];
    }
}

Handling Global Widget Settings and Overrides

A significant advantage of the Options API approach is the ability to manage global settings for a widget type. For instance, you might want a default background color or button style that applies to all instances of the “Advanced Call to Action” widget unless explicitly overridden. This is achieved by merging the global settings (stored in the option) with instance-specific settings (which Elementor handles by default).

In the `render()` method, we first retrieve the global settings from `get_option()`. Then, Elementor’s `get_settings()` method retrieves instance-specific settings. A strategic merge ensures that global defaults are used where instance settings are not provided.

<?php
// Modified render() method to incorporate global/instance merging

protected function render() {
    $schema = get_advanced_cta_widget_schema();
    $option_name = $schema['option_name'];
    $global_settings = get_option( $option_name, [] ); // Global settings

    // Ensure global settings are an array
    if ( ! is_array( $global_settings ) ) {
        $global_settings = [];
    }

    // Merge global defaults with instance settings
    $merged_settings = [];
    foreach ( $schema['settings'] as $key => $field_config ) {
        // Prioritize instance settings, then global settings, then schema defaults
        $instance_value = $this->get_settings( $key );
        $global_value = isset( $global_settings[ $key ] ) ? $global_settings[ $key ] : null;
        $default_value = $field_config['default'] ?? null;

        if ( ! empty( $instance_value ) ) {
            $merged_settings[ $key ] = $instance_value;
        } elseif ( $global_value !== null ) {
            $merged_settings[ $key ] = $global_value;
        } else {
            $merged_settings[ $key ] = $default_value;
        }
    }

    // Now use $merged_settings for rendering
    $cta_title = isset( $merged_settings['cta_title'] ) ? $merged_settings['cta_title'] : '';
    // ... extract and render other fields using $merged_settings ...

    // Example rendering with merged settings
    ?>
    <div class="advanced-cta-wrapper" style="background-color: <?php echo esc_attr( $merged_settings['cta_background_color'] ?? '#f0f0f0' ); ?> padding: 20px;">
        <h3 class="advanced-cta-title"><?php echo esc_html( $cta_title ); ?></h3>
        <!-- ... rest of the rendering ... -->
    </div>
    <?php
}

Admin Interface for Global Settings

To manage these global settings effectively, a dedicated admin page is recommended. This page would utilize the WordPress Settings API to create forms that directly update the option defined in our schema. This provides a clean, centralized UI for administrators to configure default widget behaviors without needing to edit individual widget instances.

The process involves:

  • Registering a menu page under the WordPress admin menu.
  • Defining settings using `register_setting()`, specifying the option name and sanitization callbacks.
  • Creating form fields using `add_settings_field()`, linking them to the registered setting.
  • Rendering the form using `settings_fields()` and `do_settings_sections()`.
<?php
// In your plugin's main file or an admin-specific file

function advanced_cta_register_admin_page() {
    add_options_page(
        __( 'Advanced CTA Widget Settings', 'your-text-domain' ),
        __( 'Advanced CTA Settings', 'your-text-domain' ),
        'manage_options',
        'advanced-cta-settings',
        'advanced_cta_render_settings_page'
    );
}
add_action( 'admin_menu', 'advanced_cta_register_admin_page' );

function advanced_cta_register_settings() {
    $schema = get_advanced_cta_widget_schema();
    $option_name = $schema['option_name'];
    $group_name = $schema['options_group'];

    register_setting( $group_name, $option_name, [
        'type' => 'array',
        'sanitize_callback' => 'advanced_cta_sanitize_global_settings', // Custom sanitization for the whole array
    ] );

    // Add settings fields based on the schema
    foreach ( $schema['settings'] as $key => $field_config ) {
        add_settings_field(
            $key, // ID
            $field_config['label'], // Title
            'advanced_cta_render_field', // Callback function to render the field
            'advanced-cta-settings', // Page slug
            $group_name, // Section slug (can be same as group name for simplicity)
            [ // Arguments passed to the callback
                'field_key' => $key,
                'field_config' => $field_config,
                'option_name' => $option_name,
            ]
        );
    }
}
add_action( 'admin_init', 'advanced_cta_register_settings' );

function advanced_cta_render_settings_page() {
    $schema = get_advanced_cta_widget_schema();
    $group_name = $schema['options_group'];
    ?>
    <div class="wrap">
        <h1><?php echo esc_html( get_admin_page_title() ); ?></h1>
        <form action="options.php" method="post">
            <?php
            settings_fields( $group_name ); // Output nonce, action, and option_page fields
            do_settings_sections( 'advanced-cta-settings' ); // Render all fields for this page
            submit_button();
            ?>
        </form>
    </div>
    <?php
}

function advanced_cta_render_field( $args ) {
    $field_key = $args['field_key'];
    $field_config = $args['field_config'];
    $option_name = $args['option_name'];

    $global_settings = get_option( $option_name, [] );
    $value = isset( $global_settings[ $field_key ] ) ? $global_settings[ $field_key ] : ( $field_config['default'] ?? '' );

    // Basic rendering logic - needs expansion for different field types
    switch ( $field_config['type'] ) {
        case 'text':
        case 'url':
            printf(
                '<input type="%1$s" id="%2$s" name="%3$s[%2$s]" value="%4$s" class="regular-text" />',
                esc_attr( $field_config['type'] ),
                esc_attr( $field_key ),
                esc_attr( $option_name ),
                esc_attr( $value )
            );
            break;
        case 'textarea':
            printf(
                '<textarea id="%1$s" name="%2$s[%1$s]" rows="5" class="large-text"%3$s>%4$s</textarea>',
                esc_attr( $field_key ),
                esc_attr( $option_name ),
                '', // Placeholder for attributes
                esc_textarea( $value )
            );
            break;
        case 'color':
            // Requires wp_enqueue_script for wp-color-picker
            printf(
                '<input type="text" id="%1$s" name="%2$s[%1$s]" value="%3$s" class="regular-text wp-color-picker" />',
                esc_attr( $field_key ),
                esc_attr( $option_name ),
                esc_attr( $value )
            );
            break;
        // Add cases for 'media', 'select', 'checkbox', etc.
        default:
            printf(
                '<input type="text" id="%1$s" name="%2$s[%1$s]" value="%3$s" class="regular-text" />',
                esc_attr( $field_key ),
                esc_attr( $option_name ),
                esc_attr( $value )
            );
            break;
    }

    if ( isset( $field_config['description'] ) ) {
        printf( '<p class="description">%1$s</p>', esc_html( $field_config['description'] ) );
    }
}

// Custom sanitization for the entire options array
function advanced_cta_sanitize_global_settings( $input ) {
    $schema = get_advanced_cta_widget_schema();
    $sanitized_output = [];

    foreach ( $schema['settings'] as $key => $field_config ) {
        if ( isset( $input[ $key ] ) ) {
            $value = $input[ $key ];
            if ( isset( $field_config['sanitize_callback'] ) ) {
                if ( is_array( $field_config['sanitize_callback'] ) ) {
                    $sanitized_output[ $key ] = call_user_func( $field_config['sanitize_callback'], $value );
                } else {
                    $sanitized_output[ $key ] = $field_config['sanitize_callback']( $value );
                }
            } else {
                // Default sanitization
                $sanitized_output[ $key ] = sanitize_text_field( $value );
            }
        }
    }
    return $sanitized_output;
}

// Enqueue color picker script for the admin page
function advanced_cta_enqueue_admin_scripts( $hook_suffix ) {
    if ( 'settings_page_advanced-cta-settings' === $hook_suffix ) {
        wp_enqueue_style( 'wp-color-picker' );
        wp_enqueue_script( 'wp-color-picker' );
        // Add a small JS snippet to initialize the color picker
        ?>
        <script type="text/javascript">
        jQuery(document).ready(function($){
            $('.wp-color-picker').wpColorPicker();
        });
        </script>
        <?php
    }
}
add_action( 'admin_enqueue_scripts', 'advanced_cta_enqueue_admin_scripts' );

Conclusion and Best Practices

By adopting the WordPress Options API with structured schemas for Elementor widget extensions, you gain a powerful, maintainable, and scalable solution for managing complex configurations. This approach is ideal for enterprise environments where consistency, global defaults, and centralized control are critical.

Key takeaways:

  • Define a clear, PHP-based schema for widget settings.
  • Dynamically generate Elementor controls from this schema.
  • Utilize `get_option()` and `update_option()` for centralized global settings.
  • Implement a merge strategy in the `render()` method to combine global and instance-specific settings.
  • Create a dedicated admin page using the Settings API for managing global configurations.
  • Always prioritize security through proper sanitization and escaping.

This methodology elevates custom Elementor widget development from simple UI elements to robust, configurable components that integrate seamlessly into larger WordPress ecosystems.

Primary Sidebar

A little about the Author

Having 12+ Years of Experience in Software Development, Vinay is a principal software architect, senior systems engineer, and elite technical consultant. He specializes in bespoke PHP/WordPress development, high-performance Magento 2 & Shopify architectures, custom plugin/theme development from scratch, and legacy code modernization (including VB6, VB.NET, PyQt, and Crystal Reports). Known for solving complex database bottlenecks, speed optimization (Core Web Vitals), and advanced security code auditing, Vinay engineers production-ready systems designed to scale under heavy concurrent load conditions.



Chat on WhatsApp

Recent Posts

  • How to construct high-throughput import engines for large custom subscription logs sets using custom XML/JSON parsers
  • How to implement custom Filesystem API endpoints with token authentication in Gutenberg blocks
  • How to analyze and reduce CPU consumption of custom Command Query Responsibility Segregation (CQRS) event mediators
  • Step-by-Step Guide: Refactoring legacy hooks to use Active Record Wrapper pattern in theme layers
  • Step-by-Step Guide to building a custom custom analytics tracker block for Gutenberg using Next.js headless configurations

Categories

  • apache (1)
  • Business & Monetization (390)
  • Centos (4)
  • Comparisons & Decision Making (55)
  • Debian (2)
  • Debugging & Troubleshooting (652)
  • Desktop Applications (14)
  • DevOps (7)
  • DevOps & Cloud Scaling (962)
  • Django (1)
  • Laravel (4)
  • Migration & Architecture (192)
  • Mobile Applications (24)
  • MySQL (1)
  • Performance & Optimization (868)
  • PHP (5)
  • PHP Development (38)
  • Plugins & Themes (244)
  • Programming Languages (9)
  • Python (20)
  • Ruby on Rails (1)
  • Security & Compliance (635)
  • SEO & Growth (492)
  • Server (23)
  • Ubuntu (9)
  • VB6 & VB.NET (8)
  • Web Applications & Frontend (19)
  • Web Assembly (Wasm) (2)
  • WordPress (22)
  • WordPress Plugin Development (319)
  • WordPress Theme Development (357)

Recent Posts

  • How to construct high-throughput import engines for large custom subscription logs sets using custom XML/JSON parsers
  • How to implement custom Filesystem API endpoints with token authentication in Gutenberg blocks
  • How to analyze and reduce CPU consumption of custom Command Query Responsibility Segregation (CQRS) event mediators

Top Categories

  • DevOps & Cloud Scaling (962)
  • Performance & Optimization (868)
  • Debugging & Troubleshooting (652)
  • Security & Compliance (635)
  • SEO & Growth (492)
  • Business & Monetization (390)

Our Products

  • ERP & LMS Systems (4)
  • Directories & Marketplaces (4)
  • Healthcare Portals (3)
  • Point of Sale (POS) (2)
  • E-Commerce Engines (2)

Our Services

  • E-Commerce Development (10)
  • WordPress Development (8)
  • Python & Desktop GUI (7)
  • General Consulting (7)
  • Legacy Modernization (5)
  • Mobile App Development (4)

Copyright © 2026 · Vinay Vengala