Elementor Widgets
WordPress Plugin: Testimonial plugin focused on custom Elementor widget development.
by Abu Nesar · github.com/rmdabunesar/elementor-widgets · website
★ 0stars
0forks
Install
No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:
wp plugin install https://github.com/rmdabunesar/elementor-widgets/archive/refs/heads/main.zipElementor Custom Widget Development Guide
Requirements: WordPress 6.0+, PHP 8.0+, Elementor (free)
1. Project Structure
elementor-widgets/
├── elementor-widgets.php
├── assets/
│ ├── css/testimonial.min.css
│ └── js/testimonial.min.js
└── widgets/
└── class-testimonial-widget.php ← one file per widget
One widget class per file. File name: class-{widget-slug}.php.
2. Plugin Bootstrap
Singleton class wiring all hooks in one place.
if ( ! defined( 'ABSPATH' ) ) { exit; }
define( 'EW_VERSION', '1.0.0' );
define( 'EW_PATH', plugin_dir_path( __FILE__ ) );
define( 'EW_ASSETS_URL', plugin_dir_url( __FILE__ ) . 'assets/' );
final class Elementor_Widgets {
private static ?self $instance = null;
public static function instance(): self {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
private function __construct() {
add_action( 'elementor/elements/categories_registered', [ $this, 'register_widget_category' ] );
add_action( 'elementor/widgets/register', [ $this, 'register_widgets' ] );
add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_frontend_styles' ] );
add_action( 'wp_enqueue_scripts', [ $this, 'register_frontend_scripts' ] );
add_action( 'init', [ $this, 'load_textdomain' ] );
}
}
Elementor_Widgets::instance();
3. Register Category, Widget & Assets
// Custom panel category
public function register_widget_category( \Elementor\Elements_Manager $em ): void {
$em->add_category( 'ew-widgets', [
'title' => esc_html__( 'My Widgets', 'elementor-widgets' ),
'icon' => 'fa fa-plug',
] );
}
// Load and register each widget
public function register_widgets( \Elementor\Widgets_Manager $wm ): void {
require_once EW_PATH . 'widgets/class-testimonial-widget.php';
$wm->register( new \ElementorWidgets\Widgets\Testimonial_Widget() );
}
// Enqueue styles globally; register scripts only (Elementor loads on demand)
public function enqueue_frontend_styles(): void {
wp_enqueue_style( 'ew-testimonial-style', EW_ASSETS_URL . 'css/testimonial.min.css', [], EW_VERSION );
}
public function register_frontend_scripts(): void {
wp_register_script( 'ew-testimonial-script', EW_ASSETS_URL . 'js/testimonial.min.js', [ 'jquery' ], EW_VERSION, true );
}
4. Widget Class Skeleton
namespace ElementorWidgets\Widgets;
use Elementor\Widget_Base;
use Elementor\Controls_Manager;
if ( ! defined( 'ABSPATH' ) ) { exit; }
class Testimonial_Widget extends Widget_Base {
public function get_name(): string { return 'ew_testimonial'; }
public function get_title(): string { return esc_html__( 'Testimonial Showcase', 'elementor-widgets' ); }
public function get_icon(): string { return 'eicon-testimonial-carousel'; }
public function get_categories(): array { return [ 'ew-widgets' ]; }
public function get_keywords(): array { return [ 'testimonial', 'review', 'client' ]; }
public function get_style_depends(): array { return [ 'swiper', 'ew-testimonial-style' ]; }
public function get_script_depends(): array { return [ 'swiper', 'ew-testimonial-script' ]; }
protected function register_controls(): void {
$this->register_content_controls();
$this->register_style_controls();
}
protected function render(): void { /* see §6 */ }
}
Identity methods:
| Method | Purpose |
|---|---|
get_name() |
Unique snake_case slug |
get_title() |
Panel label — wrap in esc_html__() |
get_icon() |
eicon-* or fa fa-* |
get_categories() |
Must match a registered category slug |
get_style_depends() / get_script_depends() |
Handles Elementor enqueues per-page |
5. Controls — Content Tab
Wrap every group in a section, then add controls.
private function register_content_controls(): void {
$this->start_controls_section( 'section_layout', [
'label' => esc_html__( 'Layout', 'elementor-widgets' ),
'tab' => Controls_Manager::TAB_CONTENT,
] );
// SELECT
$this->add_control( 'layout', [
'label' => esc_html__( 'Display Layout', 'elementor-widgets' ),
'type' => Controls_Manager::SELECT,
'default' => 'grid',
'options' => [ 'grid' => 'Grid', 'carousel' => 'Carousel' ],
] );
// RESPONSIVE SELECT — generates CSS per breakpoint
$this->add_responsive_control( 'columns', [
'label' => esc_html__( 'Columns', 'elementor-widgets' ),
'type' => Controls_Manager::SELECT,
'default' => '3',
'tablet_default' => '2',
'mobile_default' => '1',
'options' => [ '1' => '1', '2' => '2', '3' => '3', '4' => '4' ],
'condition' => [ 'layout' => 'grid' ], // hide when not grid
'selectors' => [
'{{WRAPPER}} .ew-grid' => 'grid-template-columns: repeat({{VALUE}}, 1fr);',
],
] );
$this->end_controls_section();
}
Common control types: TEXT, TEXTAREA, SELECT, NUMBER, SWITCHER, MEDIA, REPEATER.
Repeater
$repeater = new \Elementor\Repeater();
$repeater->add_control( 'client_name', [
'label' => esc_html__( 'Client Name', 'elementor-widgets' ),
'type' => Controls_Manager::TEXT,
'default' => 'Jane Doe',
'dynamic' => [ 'active' => true ],
] );
$this->add_control( 'testimonials', [
'type' => Controls_Manager::REPEATER,
'fields' => $repeater->get_controls(),
'title_field' => '{{{ client_name }}}',
] );
Conditional controls
$this->add_control( 'autoplay_speed', [
'type' => Controls_Manager::NUMBER,
'condition' => [ 'carousel_autoplay' => 'yes' ], // key => expected value
] );
6. Controls — Style Tab
private function register_style_controls(): void {
$this->start_controls_section( 'section_card_style', [
'label' => esc_html__( 'Card', 'elementor-widgets' ),
'tab' => Controls_Manager::TAB_STYLE,
] );
// COLOR with inline CSS selector
$this->add_control( 'text_color', [
'type' => Controls_Manager::COLOR,
'default' => '#4a5568',
'selectors' => [ '{{WRAPPER}} .ew-text' => 'color: {{VALUE}};' ],
] );
// DIMENSIONS (padding / border-radius)
$this->add_responsive_control( 'card_padding', [
'type' => Controls_Manager::DIMENSIONS,
'size_units' => [ 'px', 'em', 'rem' ],
'selectors' => [
'{{WRAPPER}} .ew-card' => 'padding: {{TOP}}{{UNIT}} {{RIGHT}}{{UNIT}} {{BOTTOM}}{{UNIT}} {{LEFT}}{{UNIT}};',
],
] );
// SLIDER
$this->add_responsive_control( 'avatar_size', [
'type' => Controls_Manager::SLIDER,
'range' => [ 'px' => [ 'min' => 40, 'max' => 120 ] ],
'default' => [ 'size' => 64, 'unit' => 'px' ],
'selectors' => [ '{{WRAPPER}} .ew-avatar' => 'width: {{SIZE}}{{UNIT}}; height: {{SIZE}}{{UNIT}};' ],
] );
// GROUP CONTROLS
$this->add_group_control( \Elementor\Group_Control_Typography::get_type(), [
'name' => 'text_typography', 'selector' => '{{WRAPPER}} .ew-text',
] );
$this->add_group_control( \Elementor\Group_Control_Box_Shadow::get_type(), [
'name' => 'card_shadow', 'selector' => '{{WRAPPER}} .ew-card',
] );
$this->add_group_control( \Elementor\Group_Control_Border::get_type(), [
'name' => 'card_border', 'selector' => '{{WRAPPER}} .ew-card',
] );
$this->add_group_control( \Elementor\Group_Control_Background::get_type(), [
'name' => 'card_bg', 'types' => [ 'classic', 'gradient' ], 'selector' => '{{WRAPPER}} .ew-card',
] );
$this->end_controls_section();
}
7. Rendering
protected function render(): void {
$settings = $this->get_settings_for_display();
if ( empty( $settings['testimonials'] ) ) { return; }
foreach ( $settings['testimonials'] as $item ) {
$rating = (int) ( $item['rating'] ?? 5 );
?>
<div class="ew-card">
<div class="ew-stars"
aria-label="<?php printf( esc_attr__( '%d out of 5 stars', 'elementor-widgets' ), $rating ); ?>">
<?php echo esc_html( str_repeat( '★', $rating ) . str_repeat( '☆', 5 - $rating ) ); ?>
</div>
<blockquote class="ew-text">
<?php echo wp_kses_post( $item['testimonial_text'] ?? '' ); ?>
</blockquote>
<span class="ew-client-name"><?php echo esc_html( $item['client_name'] ?? '' ); ?></span>
</div>
<?php
}
}
Escaping quick-reference:
| Data | Function |
|---|---|
| Plain text | esc_html() |
| Attribute value | esc_attr() |
| URL | esc_url() |
| Trusted HTML | wp_kses_post() |
JSON → data-* |
wp_json_encode() + esc_attr() |
8. JavaScript Integration
Pass PHP config via data-*; read it in JS. Avoids inline `