Master custom CTA plugin development for WordPress. This tutorial guides you to build powerful, customizable CTAs with color pickers, images & shortcodes. Gain full control!
Are you tired of relying on bloated plugins or scattered hardcoded HTML for your Call-to-Action (CTA) blocks? Do you crave complete control over your CTAs, from their content and colors to their very appearance, all managed in one central place?
If you answered yes, then this tutorial is for you!
Table of Contents
Master Your CTAs: Build a Dynamic & Customizable WordPress Plugin
In this in-depth guide, we’re going to build a custom WordPress plugin from scratch. This isn’t just a simple shortcode; we’re creating a robust, site-wide customizable CTA management system that will empower you to:
- Create multiple, unique CTA blocks for different purposes.
- Include images, prominent titles, catchy subtitles, descriptive paragraphs, and engaging buttons.
- Visually pick button background and text colors directly from your WordPress admin.
- Display your beautiful CTAs anywhere on your site using a simple shortcode.
- Apply stunning custom styling with enhanced image shadows, elegant frames, and smooth button hover effects.
Let’s dive in and take control of your Call-to-Actions!
Section 1: Setting Up Your Plugin Base
Every WordPress plugin starts with a dedicated folder and a main PHP file.
Step 1.1: Create Your Plugin Folder & Main File
First, connect to your WordPress site using an FTP client (like FileZilla) or through your hosting provider’s File Manager. Navigate to the wp-content/plugins/ directory.
Inside wp-content/plugins/, create a new folder named my-custom-cta.
Now, inside this my-custom-cta folder, create a new PHP file named my-custom-cta.php. This will be your main plugin file.
Open my-custom-cta.php and add the following basic plugin header comments. This tells WordPress about your plugin.
<?php
/*
Plugin Name: My Custom CTA
Plugin URI: https://example.com/
Description: A simple WordPress plugin to create, manage, and display multiple Call-to-Action blocks via shortcodes.
Version: 1.3
Author: Your Name
Author URI: https://example.com/
License: GPL2
License URI: https://www.gnu.org/licenses/gpl-2.0.html
Text Domain: my-custom-cta
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
// Your plugin code will go here
Step 1.2: The Core Plugin Class
We’ll structure our plugin using an object-oriented approach with a main class. This keeps our code organized and prevents conflicts with other plugins.
Below the initial comments and the if ( ! defined( 'ABSPATH' ) ) block, add the basic class structure:
// ... (previous code) ...
/**
* My Custom CTA Class
*/
class My_Custom_CTA {
private $option_name = 'my_custom_ctas'; // Option name to store all CTAs
public function __construct() {
// WordPress Hooks go here to initiate plugin functionality
add_action( 'admin_menu', array( $this, 'add_admin_menu' ) );
add_action( 'admin_init', array( $this, 'register_settings' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_scripts' ) );
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_frontend_styles' ) );
add_shortcode( 'my_custom_cta', array( $this, 'render_cta_shortcode' ) );
}
// All our plugin methods will be defined below this constructor
} // End of My_Custom_CTA class
new My_Custom_CTA(); // Initialize the plugin
The __construct() method is like the plugin’s initializer. Here, we’re hooking into various WordPress actions:
admin_menu: To add our settings page to the WordPress admin menu.admin_init: To register our plugin’s settings.admin_enqueue_scripts: To load CSS and JavaScript specifically for our admin page.wp_enqueue_scripts: To load CSS for the front-end display of our CTAs.add_shortcode: To register the shortcode users will use to display CTAs.
Section 2: Crafting the Admin Settings Page
This is where you’ll create and manage all your CTA blocks.
Step 2.1: Add the Menu Item
Inside your My_Custom_CTA class, add the add_admin_menu method:
// ... (inside My_Custom_CTA class) ...
/**
* Add admin menu page
*/
public function add_admin_menu() {
add_menu_page(
__( 'My Custom CTAs', 'my-custom-cta' ), // Page title
__( 'Custom CTAs', 'my-custom-cta' ), // Menu title
'manage_options', // Capability required
'my-custom-cta', // Menu slug
array( $this, 'settings_page_html' ), // Function to render the page
'dashicons-megaphone', // Icon
60 // Position in menu
);
}
// ... (rest of class) ...
This code adds a new top-level menu item called “Custom CTAs” to your WordPress admin sidebar. Clicking it will load the content generated by settings_page_html().
Step 2.2: Implement the Settings API
The WordPress Settings API is the standard way to handle plugin options. It manages saving, retrieving, and sanitizing your settings.
// ... (inside My_Custom_CTA class) ...
/**
* Register plugin settings
*/
public function register_settings() {
register_setting(
'my_custom_cta_group', // Option group
$this->option_name, // Option name (my_custom_ctas)
array( $this, 'sanitize_ctas_data' ) // Sanitization callback
);
}
/**
* Sanitize and validate CTA data before saving
*/
public function sanitize_ctas_data( $input ) {
$clean_ctas = array();
if ( ! empty( $input ) && is_array( $input ) ) {
foreach ( $input as $id => $cta ) {
$clean_cta = array();
$clean_cta['name'] = sanitize_text_field( $cta['name'] ?? '' );
$clean_cta['image_url'] = esc_url_raw( $cta['image_url'] ?? '' );
$clean_cta['title'] = sanitize_text_field( $cta['title'] ?? '' );
$clean_cta['subtitle'] = sanitize_text_field( $cta['subtitle'] ?? '' );
$clean_cta['paragraph'] = wp_kses_post( $cta['paragraph'] ?? '' ); // Allow basic HTML
$clean_cta['button_label'] = sanitize_text_field( $cta['button_label'] ?? '' );
$clean_cta['button_link'] = esc_url_raw( $cta['button_link'] ?? '' );
$clean_cta['button_bg_color'] = sanitize_hex_color( $cta['button_bg_color'] ?? '' );
$clean_cta['button_text_color'] = sanitize_hex_color( $cta['button_text_color'] ?? '' );
$clean_ctas[ sanitize_key( $id ) ] = $clean_cta;
}
}
return $clean_ctas;
}
// ... (rest of class) ...
register_setting(): This tells WordPress about our option group (my_custom_cta_group) and the option name (my_custom_ctas) where all our CTA data will be stored as a single array.sanitize_ctas_data(): This is a crucial security step. Before any data is saved to the database, it passes through this function. We use WordPress’s built-in sanitization functions (sanitize_text_field,esc_url_raw,wp_kses_post,sanitize_hex_color) to ensure only clean, safe data is stored.
Step 2.3: Building Dynamic CTA Fields
Now for the HTML of our settings page. We’ll create a main container and a template for new CTAs.
// ... (inside My_Custom_CTA class) ...
/**
* Settings page HTML
*/
public function settings_page_html() {
?>
<div class="wrap">
<h1><?php _e( 'My Custom Call-to-Action Settings', 'my-custom-cta' ); ?></h1>
<?php settings_errors(); // Displays messages from settings API ?>
<form method="post" action="options.php" class="my-custom-cta-form">
<?php settings_fields( 'my_custom_cta_group' ); // Security fields ?>
<?php do_settings_sections( 'my_custom_cta_group' ); // Not strictly needed here, but good practice ?>
<div id="cta-container">
<?php
// Load existing CTAs and render their fields
$ctas = get_option( $this->option_name, array() );
if ( ! empty( $ctas ) ) {
foreach ( $ctas as $id => $cta ) {
$this->render_cta_fields( $id, $cta );
}
}
?>
</div>
<p>
<button type="button" id="add-new-cta" class="button button-primary">
<?php _e( 'Add New CTA', 'my-custom-cta' ); ?>
</button>
</p>
<?php submit_button( __( 'Save All CTAs', 'my-custom-cta' ) ); ?>
</form>
<div id="cta-template" style="display: none;">
<?php $this->render_cta_fields( 'NEW_ID', array() ); // Template for new CTAs ?>
</div>
</div>
<?php
}
/**
* Render individual CTA fields (private helper method)
* This function is called for each existing CTA and for the new CTA template.
*/
private function render_cta_fields( $id, $cta_data ) {
// Retrieve data for the current CTA or set empty defaults for a new one
$cta_name = $cta_data['name'] ?? '';
$image_url = $cta_data['image_url'] ?? '';
$title = $cta_data['title'] ?? '';
$subtitle = $cta_data['subtitle'] ?? '';
$paragraph = $cta_data['paragraph'] ?? '';
$button_label = $cta_data['button_label'] ?? '';
$button_link = $cta_data['button_link'] ?? '';
$button_bg_color = $cta_data['button_bg_color'] ?? '';
$button_text_color = $cta_data['button_text_color'] ?? '';
$is_new = ( $id === 'NEW_ID' ); // Check if this is the template for a new CTA
$display_id = $is_new ? '' : $id; // Don't show ID for template
?>
<div class="cta-block" data-id="<?php echo esc_attr( $id ); ?>">
<span class="cta-toggle dashicons dashicons-arrow-down"></span>
<h3>
<?php _e( 'CTA:', 'my-custom-cta' ); ?> <span class="cta-block-name"><?php echo esc_html( $cta_name ?: '(Untitled CTA)' ); ?></span>
<span class="cta-shortcode-display" <?php echo $is_new ? 'style="display:none;"' : ''; ?>>
<?php _e( 'Shortcode:', 'my-custom-cta' ); ?> <code>[my_custom_cta id="<?php echo esc_attr( $display_id ); ?>"]</code>
</span>
<button type="button" class="button button-secondary delete-cta" title="<?php esc_attr_e( 'Delete this CTA', 'my-custom-cta' ); ?>"><span class="dashicons dashicons-trash"></span></button>
</h3>
<div class="cta-content" <?php echo $is_new ? '' : 'style="display:none;"'; ?>>
<table class="form-table">
<tbody>
<tr>
<th scope="row"><label for="<?php echo esc_attr( $this->option_name . '[' . $id . '][name]' ); ?>"><?php _e( 'CTA Internal Name (for identification):', 'my-custom-cta' ); ?></label></th>
<td>
<input type="text"
name="<?php echo esc_attr( $this->option_name . '[' . $id . '][name]' ); ?>"
id="<?php echo esc_attr( $this->option_name . '[' . $id . '][name]' ); ?>"
value="<?php echo esc_attr( $cta_name ); ?>"
class="regular-text cta-name-input"
placeholder="<?php esc_attr_e( 'e.g., Homepage Banner CTA', 'my-custom-cta' ); ?>"
required>
<p class="description"><?php _e( 'This name helps you identify the CTA in the backend. It also forms part of the shortcode ID.', 'my-custom-cta' ); ?></p>
</td>
</tr>
<tr>
<th scope="row"><label for="<?php echo esc_attr( $this->option_name . '[' . $id . '][image_url]' ); ?>"><?php _e( 'Image for CTA:', 'my-custom-cta' ); ?></label></th>
<td>
<div class="my-custom-cta-image-preview">
<?php if ( $image_url ) : ?>
<img src="<?php echo esc_url( $image_url ); ?>" style="max-width:200px; height:auto;"/>
<?php endif; ?>
</div>
<input type="hidden"
name="<?php echo esc_attr( $this->option_name . '[' . $id . '][image_url]' ); ?>"
id="<?php echo esc_attr( $this->option_name . '[' . $id . '][image_url]' ); ?>"
class="regular-text cta-image-url"
value="<?php echo esc_attr( $image_url ); ?>">
<button type="button" class="button button-secondary upload-image-button">
<?php _e( 'Select Image', 'my-custom-cta' ); ?>
</button>
<?php if ( $image_url ) : // Show remove button only if image exists ?>
<button type="button" class="button button-secondary remove-image-button">
<?php _e( 'Remove Image', 'my-custom-cta' ); ?>
</button>
<?php endif; ?>
</td>
</tr>
<tr>
<th scope="row"><label for="<?php echo esc_attr( $this->option_name . '[' . $id . '][title]' ); ?>"><?php _e( 'Title (H3):', 'my-custom-cta' ); ?></label></th>
<td>
<input type="text"
name="<?php echo esc_attr( $this->option_name . '[' . $id . '][title]' ); ?>"
id="<?php echo esc_attr( $this->option_name . '[' . $id . '][title]' ); ?>"
value="<?php echo esc_attr( $title ); ?>"
class="regular-text"
placeholder="<?php esc_attr_e( 'e.g., Discover Our New Product', 'my-custom-cta' ); ?>">
</td>
</tr>
<tr>
<th scope="row"><label for="<?php echo esc_attr( $this->option_name . '[' . $id . '][subtitle]' ); ?>"><?php _e( 'Subtitle:', 'my-custom-cta' ); ?></label></th>
<td>
<input type="text"
name="<?php echo esc_attr( $this->option_name . '[' . $id . '][subtitle]' ); ?>"
id="<?php echo esc_attr( $this->option_name . '[' . $id . '][subtitle]' ); ?>"
value="<?php echo esc_attr( $subtitle ); ?>"
class="regular-text"
placeholder="<?php esc_attr_e( 'e.g., Limited Time Offer!', 'my-custom-cta' ); ?>">
</td>
</tr>
<tr>
<th scope="row"><label for="<?php echo esc_attr( $this->option_name . '[' . $id . '][paragraph]' ); ?>"><?php _e( 'Paragraph of Text:', 'my-custom-cta' ); ?></label></th>
<td>
<textarea name="<?php echo esc_attr( $this->option_name . '[' . $id . '][paragraph]' ); ?>"
id="<?php echo esc_attr( $this->option_name . '[' . $id . '][paragraph]' ); ?>"
rows="5"
class="large-text"><?php echo esc_textarea( $paragraph ); ?></textarea>
</td>
</tr>
<tr>
<th scope="row"><label for="<?php echo esc_attr( $this->option_name . '[' . $id . '][button_label]' ); ?>"><?php _e( 'Button Label:', 'my-custom-cta' ); ?></label></th>
<td>
<input type="text"
name="<?php echo esc_attr( $this->option_name . '[' . $id . '][button_label]' ); ?>"
id="<?php echo esc_attr( $this->option_name . '[' . $id . '][button_label]' ); ?>"
value="<?php echo esc_attr( $button_label ); ?>"
class="regular-text"
placeholder="<?php esc_attr_e( 'e.g., Learn More', 'my-custom-cta' ); ?>">
</td>
</tr>
<tr>
<th scope="row"><label for="<?php echo esc_attr( $this->option_name . '[' . $id . '][button_link]' ); ?>"><?php _e( 'Button Link (URL):', 'my-custom-cta' ); ?></label></th>
<td>
<input type="url"
name="<?php echo esc_attr( $this->option_name . '[' . $id . '][button_link]' ); ?>"
id="<?php echo esc_attr( $this->option_name . '[' . $id . '][button_link]' ); ?>"
value="<?php echo esc_attr( $button_link ); ?>"
class="regular-text"
placeholder="<?php esc_attr_e( 'e.g., https://yourwebsite.com/page', 'my-custom-cta' ); ?>">
</td>
</tr>
<tr>
<th scope="row"><label for="<?php echo esc_attr( $this->option_name . '[' . $id . '][button_bg_color]' ); ?>"><?php _e( 'Button Background Color:', 'my-custom-cta' ); ?></label></th>
<td>
<input type="text"
name="<?php echo esc_attr( $this->option_name . '[' . $id . '][button_bg_color]' ); ?>"
id="<?php echo esc_attr( $this->option_name . '[' . $id . '][button_bg_color]' ); ?>"
value="<?php echo esc_attr( $button_bg_color ); ?>"
class="my-custom-cta-color-field"
data-default-color="#0073aa">
</td>
</tr>
<tr>
<th scope="row"><label for="<?php echo esc_attr( $this->option_name . '[' . $id . '][button_text_color]' ); ?>"><?php _e( 'Button Text Color:', 'my-custom-cta' ); ?></label></th>
<td>
<input type="text"
name="<?php echo esc_attr( $this->option_name . '[' . $id . '][button_text_color]' ); ?>"
id="<?php echo esc_attr( $this->option_name . '[' . $id . '][button_text_color]' ); ?>"
value="<?php echo esc_attr( $button_text_color ); ?>"
class="my-custom-cta-color-field"
data-default-color="#ffffff">
</td>
</tr>
</tbody>
</table>
</div>
</div>
<?php
}
The settings_page_html() function generates the main admin page. It loops through any existing CTAs and calls render_cta_fields() for each. It also includes an “Add New CTA” button and a hidden cta-template that will be cloned by JavaScript when a new CTA is added.
The render_cta_fields() function is the workhorse. It outputs the HTML for each individual CTA block, including all the input fields. Notice the my-custom-cta-color-field class on our color inputs – this is key for the WordPress color picker!
Section 3: Enqueuing Styles and Scripts
For our admin interface to look good and be interactive, and for our frontend CTAs to be styled, we need to load CSS and JavaScript files.
Step 3.1: Admin-Side Assets (enqueue_admin_scripts)
Add this method inside your My_Custom_CTA class:
// ... (inside My_Custom_CTA class) ...
/**
* Enqueue admin scripts and styles
*/
public function enqueue_admin_scripts( $hook ) {
// Only load scripts/styles on our specific admin page
if ( 'toplevel_page_my-custom-cta' != $hook ) {
return;
}
// Enqueue media uploader scripts (for image uploads)
wp_enqueue_media();
// Enqueue WordPress color picker scripts and styles
wp_enqueue_style( 'wp-color-picker' );
wp_enqueue_script( 'wp-color-picker' );
// Enqueue our custom admin styles
wp_enqueue_style( 'my-custom-cta-admin-style', plugin_dir_url( __FILE__ ) . 'css/admin-style.css', array( 'wp-color-picker' ) );
// Enqueue our custom admin JavaScript
wp_enqueue_script(
'my-custom-cta-admin-script',
plugin_dir_url( __FILE__ ) . 'js/admin-script.js',
array( 'jquery', 'wp-mediaelement', 'wp-color-picker' ), // Dependencies
null, // Version
true // In footer
);
// Pass PHP data to our JavaScript file
wp_localize_script(
'my-custom-cta-admin-script',
'myCustomCtaAdmin',
array(
'ajax_url' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'my-custom-cta-nonce' ),
'media_title' => __( 'Select CTA Image', 'my-custom-cta' ),
'media_button' => __( 'Use This Image', 'my-custom-cta' ),
)
);
}
// ... (rest of class) ...
This method is crucial. It loads:
wp_enqueue_media(): Necessary for the “Select Image” button to open the WordPress media library.
wp-color-picker: This is WordPress’s built-in color picker. By enqueuing its assets, we can instantly turn our color input fields into interactive color pickers.
- Our custom
admin-style.cssandadmin-script.js. wp_localize_script(): A handy function to pass small bits of dynamic PHP data (like text strings or nonces) to your JavaScript.
Step 3.2: Frontend-Side Assets (enqueue_frontend_styles)
Add this method inside your My_Custom_CTA class:
// ... (inside My_Custom_CTA class) ...
/**
* Enqueue frontend styles
*/
public function enqueue_frontend_styles() {
wp_enqueue_style( 'my-custom-cta-frontend-style', plugin_dir_url( __FILE__ ) . 'css/frontend-style.css' );
}
// ... (rest of class) ...
This simple function ensures that the frontend-style.css file is loaded on the public-facing side of your website, applying all the visual magic to your CTAs.
Section 4: The Activation Hook – Auto-Generating Assets
This is where the magic of “single PHP file” managing separate CSS/JS files happens. When you activate your plugin, this special function will create the css and js folders, and populate the admin-style.css, frontend-style.css, and admin-script.js files with their content.
Important Note: If you make changes to the CSS or JavaScript code within your my-custom-cta.php file, you will need to Deactivate and then Reactivate your plugin from the WordPress Plugins page for those changes to be written to the external CSS/JS files.
Add the following code outside your My_Custom_CTA class, at the very bottom of your my-custom-cta.php file:
// ... (End of My_Custom_CTA class and its initialization) ...
// Create necessary folders and files on plugin activation and update their content
register_activation_hook( __FILE__, 'my_custom_cta_activate' );
function my_custom_cta_activate() {
$plugin_dir = plugin_dir_path( __FILE__ );
$css_dir = $plugin_dir . 'css/';
$js_dir = $plugin_dir . 'js/';
// Create CSS directory if it doesn't exist
if ( ! file_exists( $css_dir ) ) {
wp_mkdir_p( $css_dir );
}
// Create JS directory if it doesn't exist
if ( ! file_exists( $js_dir ) ) {
wp_mkdir_p( $js_dir );
}
// Always update admin-style.css with the latest content
$admin_css_file = $css_dir . 'admin-style.css';
file_put_contents( $admin_css_file,
'.cta-block {
background: #fff;
border: 1px solid #c3c4c7;
margin-bottom: 15px;
padding: 10px 20px;
position: relative;
}
.cta-block h3 {
margin: 0;
padding-right: 100px; /* Space for delete button */
position: relative;
}
.cta-block h3 .cta-toggle {
cursor: pointer;
float: left;
margin-right: 10px;
font-size: 20px;
line-height: 1;
width: 20px;
height: 20px;
text-align: center;
vertical-align: middle;
margin-top: 2px;
}
.cta-block h3 .cta-toggle.dashicons-arrow-down:before {
content: "\\f140"; /* down arrow */
}
.cta-block h3 .cta-toggle.dashicons-arrow-up:before {
content: "\\f142"; /* up arrow */
}
.cta-block .delete-cta {
position: absolute;
right: 0;
top: 0;
color: #a00;
border: none;
background: none;
box-shadow: none;
cursor: pointer;
}
.cta-block .delete-cta:hover {
color: #dc3232;
}
.my-custom-cta-image-preview img {
border: 1px solid #ddd;
padding: 5px;
background: #f9f9f9;
margin-bottom: 10px;
display: block;
}
.cta-shortcode-display {
font-size: 0.9em;
color: #555;
margin-left: 15px;
}
.cta-shortcode-display code {
background: #f0f0f0;
padding: 2px 5px;
border-radius: 3px;
border: 1px solid #e0e0e0;
}
#add-new-cta {
margin-top: 20px;
}
/* Styling for color picker */
.wp-picker-container {
display: inline-block; /* Adjust if necessary */
vertical-align: middle;
}
.my-custom-cta-color-field {
width: 80px !important; /* Make color input smaller */
}'
);
// Always update frontend-style.css with the latest content
$frontend_css_file = $css_dir . 'frontend-style.css';
file_put_contents( $frontend_css_file,
'.my-custom-cta-block {
border: 1px solid #eee;
padding: 20px;
margin-bottom: 20px;
display: flex;
flex-wrap: wrap;
align-items: center;
background: #f9f9f9;
box-shadow: 0 4px 10px rgba(0,0,0,0.08); /* More prominent subtle shadow */
border-radius: 8px; /* Slightly rounded corners for the block */
overflow: hidden; /* Important for border-radius on nested elements */
transition: transform 0.3s ease, box-shadow 0.3s ease; /* Smooth transition for hover effects on the block itself */
}
.my-custom-cta-block:hover {
transform: translateY(-3px); /* Subtle lift effect on hover */
box-shadow: 0 6px 15px rgba(0,0,0,0.12); /* Increase shadow on hover */
}
.my-custom-cta-image {
flex: 0 0 30%; /* Take 30% width */
max-width: 30%;
margin-right: 20px;
/* No padding here as the frame will provide it */
}
.my-custom-cta-image img {
max-width: 100%;
height: auto;
display: block;
border-radius: 4px; /* Slightly rounded corners for the image itself */
/* Enhanced Shadow and Frame */
border: 6px solid #fff; /* A white frame */
padding: 3px; /* Small inner padding for the frame effect */
background-color: #fff; /* Ensure background is white if border is transparent */
box-shadow: 0 10px 30px rgba(0,0,0,0.3), 0 0 0 1px rgba(0,0,0,0.05) inset; /* Enhanced shadow + subtle inner line */
transition: transform 0.3s ease, box-shadow 0.3s ease; /* Smooth transition for image hover */
}
.my-custom-cta-block:hover .my-custom-cta-image img {
transform: scale(1.02); /* Slightly zoom image on block hover */
box-shadow: 0 15px 40px rgba(0,0,0,0.4), 0 0 0 1px rgba(0,0,0,0.08) inset; /* Even stronger shadow on hover */
}
.my-custom-cta-content {
flex: 1; /* Take remaining width */
}
.my-custom-cta-title {
font-size: 1.8em;
margin-top: 0;
margin-bottom: 10px;
color: #333;
line-height: 1.2; /* Improve readability */
}
.my-custom-cta-subtitle {
font-size: 1.2em;
margin-bottom: 10px;
color: #666;
font-weight: 400; /* Ensure subtitle is not too bold */
}
.my-custom-cta-paragraph {
margin-bottom: 15px;
line-height: 1.6;
color: #444;
}
.my-custom-cta-button .button {
border: none; /* Make sure default border is removed if inline color is applied */
padding: 12px 25px; /* Slightly larger padding */
border-radius: 5px;
text-decoration: none;
transition: all 0.3s ease; /* Smooth transition for hover effects */
cursor: pointer;
display: inline-block; /* Ensure it behaves like a block for padding/margins */
font-weight: 600; /* Make button text a bit bolder */
letter-spacing: 0.5px; /* Add slight letter spacing */
box-shadow: 0 2px 5px rgba(0,0,0,0.1); /* Subtle shadow on button */
}
.my-custom-cta-button .button:hover {
filter: brightness(1.15); /* Make it brighter on hover */
transform: translateY(-1px); /* Slight lift for the button itself */
box-shadow: 0 4px 8px rgba(0,0,0,0.15); /* More prominent shadow on hover */
}
/* Responsive adjustments */
@media (max-width: 768px) {
.my-custom-cta-block {
flex-direction: column;
text-align: center;
padding: 15px; /* Adjust padding for smaller screens */
}
.my-custom-cta-image {
flex: none;
max-width: 80%; /* Image takes more width on mobile */
margin-right: 0;
margin-bottom: 20px;
order: -1; /* Place image above text on mobile */
}
.my-custom-cta-image img {
width: 100%; /* Ensure image fills its container */
}
.my-custom-cta-content {
width: 100%; /* Content takes full width */
}
}'
);
// Always update admin-script.js with the latest content
$admin_js_file = $js_dir . 'admin-script.js';
file_put_contents( $admin_js_file,
'(function($) {
$(document).ready(function() {
var mediaUploader;
// Function to initialize color pickers
function initializeColorPickers() {
if (typeof $.fn.wpColorPicker !== \'undefined\') {
$(\'.my-custom-cta-color-field\').each(function() {
if ($(this).data(\'wpColorPicker\') !== undefined) {
$(this).wpColorPicker(\'destroy\');
}
});
$(\'.my-custom-cta-color-field\').wpColorPicker();
}
}
// Initialize color pickers on page load
initializeColorPickers();
// Add new CTA block
$(\'#add-new-cta\').on(\'click\', function() {
var template = $(\'#cta-template\').html();
var newId = \'cta-\' + Date.now();
var newBlock = $(template.replace(/NEW_ID/g, newId));
newBlock.find(\'.cta-name-input\').attr(\'required\', true);
newBlock.find(\'.cta-shortcode-display\').hide();
$(\'#cta-container\').append(newBlock);
newBlock.find(\'.cta-content\').show();
newBlock.find(\'.cta-toggle\').removeClass(\'dashicons-arrow-down\').addClass(\'dashicons-arrow-up\');
// Re-initialize color pickers for the new block
newBlock.find(\'.my-custom-cta-color-field\').wpColorPicker();
});
// Toggle CTA block content
$(document).on(\'click\', \'.cta-toggle, .cta-block h3\', function(e) {
if ($(e.target).is(\'.delete-cta\') || $(e.target).closest(\'.delete-cta\').length) {
return;
}
var $block = $(this).closest(\'.cta-block\');
$block.find(\'.cta-content\').slideToggle(200);
$(this).toggleClass(\'dashicons-arrow-down dashicons-arrow-up\');
});
// Delete CTA block
$(document).on(\'click\', \'.delete-cta\', function() {
if (confirm(\'Are you sure you want to delete this CTA? This action cannot be undone.\')) {
$(this).closest(\'.cta-block\').remove();
}
});
// Update CTA name display
$(document).on(\'input\', \'.cta-name-input\', function() {
var ctaName = $(this).val().trim();
var $block = $(this).closest(\'.cta-block\');
var $nameDisplay = $block.find(\'.cta-block-name\');
if (ctaName) {
$nameDisplay.text(ctaName);
} else {
$nameDisplay.text(\'(Untitled CTA)\');
}
});
// Media Uploader for image
$(document).on(\'click\', \'.upload-image-button\', function(e) {
e.preventDefault();
var $button = $(this);
var $imageInput = $button.siblings(\'.cta-image-url\');
var $imagePreview = $button.siblings(\'.my-custom-cta-image-preview\');
if (mediaUploader) {
mediaUploader.open();
return;
}
mediaUploader = wp.media({
title: myCustomCtaAdmin.media_title,
button: {
text: myCustomCtaAdmin.media_button
},
multiple: false
});
mediaUploader.on(\'select\', function() {
var attachment = mediaUploader.state().get(\'selection\').first().toJSON();
$imageInput.val(attachment.url);
$imagePreview.html(\'<img src="\' + attachment.url + \'" style="max-width:200px; height:auto;"/>\');
if ($button.siblings(\'.remove-image-button\').length === 0) {
$button.after(\'<button type="button" class="button button-secondary remove-image-button">Remove Image</button>\');
}
});
mediaUploader.open();
});
// Remove image
$(document).on(\'click\', \'.remove-image-button\', function() {
var $button = $(this);
var $imageInput = $button.siblings(\'.cta-image-url\');
var $imagePreview = $button.siblings(\'.my-custom-cta-image-preview\');
$imageInput.val(\'\');
$imagePreview.html(\'\');
$button.remove();
});
});
})(jQuery);'
);
}
This section is perhaps the most unique part of our plugin. It dynamically creates and updates the admin-style.css, frontend-style.css, and admin-script.js files whenever the plugin is activated or reactivated. This means you only need to manage the code in my-custom-cta.php!
Section 5: Displaying Your CTAs with Shortcodes
Now that we can create and manage our CTAs, let’s learn how to display them anywhere on our site.
Add the render_cta_shortcode method inside your My_Custom_CTA class:
// ... (inside My_Custom_CTA class) ...
/**
* Render CTA shortcode
* Usage: [my_custom_cta id="your_cta_id"]
*/
public function render_cta_shortcode( $atts ) {
// Parse shortcode attributes, setting 'id' as default
$atts = shortcode_atts(
array(
'id' => '',
),
$atts,
'my_custom_cta'
);
$cta_id = sanitize_key( $atts['id'] ); // Sanitize the provided ID
if ( empty( $cta_id ) ) {
return '<p style="color:red;">' . __( 'Error: CTA ID is missing in shortcode.', 'my-custom-cta' ) . '</p>';
}
// Retrieve all saved CTAs
$ctas = get_option( $this->option_name, array() );
// Get the specific CTA data by ID
$cta_data = $ctas[ $cta_id ] ?? null;
if ( ! $cta_data ) {
return '<p style="color:red;">' . sprintf( __( 'Error: CTA with ID "%s" not found.', 'my-custom-cta' ), esc_html( $cta_id ) ) . '</p>';
}
// Prepare inline styles for the button (from color pickers)
$button_styles = '';
if ( ! empty( $cta_data['button_bg_color'] ) ) {
$button_styles .= 'background-color:' . esc_attr( $cta_data['button_bg_color'] ) . ';';
}
if ( ! empty( $cta_data['button_text_color'] ) ) {
$button_styles .= 'color:' . esc_attr( $cta_data['button_text_color'] ) . ';';
}
if ( ! empty( $button_styles ) ) {
$button_styles = rtrim($button_styles, ';') . ' !important;'; // Use !important for robustness
}
// Use output buffering to capture the HTML output
ob_start();
?>
<div class="my-custom-cta-block">
<?php if ( ! empty( $cta_data['image_url'] ) ) : ?>
<div class="my-custom-cta-image">
<img src="<?php echo esc_url( $cta_data['image_url'] ); ?>" alt="<?php echo esc_attr( $cta_data['title'] ); ?>">
</div>
<?php endif; ?>
<div class="my-custom-cta-content">
<?php if ( ! empty( $cta_data['title'] ) ) : ?>
<h3 class="my-custom-cta-title"><?php echo esc_html( $cta_data['title'] ); ?></h3>
<?php endif; ?>
<?php if ( ! empty( $cta_data['subtitle'] ) ) : ?>
<p class="my-custom-cta-subtitle"><?php echo esc_html( $cta_data['subtitle'] ); ?></p>
<?php endif; ?>
<?php if ( ! empty( $cta_data['paragraph'] ) ) : ?>
<div class="my-custom-cta-paragraph"><?php echo wp_kses_post( $cta_data['paragraph'] ); ?></div>
<?php endif; ?>
<?php if ( ! empty( $cta_data['button_label'] ) && ! empty( $cta_data['button_link'] ) ) : ?>
<div class="my-custom-cta-button">
<a href="<?php echo esc_url( $cta_data['button_link'] ); ?>" class="button" style="<?php echo esc_attr( $button_styles ); ?>"><?php echo esc_html( $cta_data['button_label'] ); ?></a>
</div>
<?php endif; ?>
</div>
</div>
<?php
return ob_get_clean(); // Return the captured HTML
}
This method is called when WordPress encounters your shortcode. It:
- Retrieves the CTA
idfrom the shortcode attributes. - Fetches the corresponding CTA data from your saved options.
- Dynamically applies the chosen background and text colors to the button using inline styles.
- Outputs the full HTML structure for your CTA, complete with image, text, and button, applying all the classes we’ve prepared for styling.
Step 5.2: How to Use the Shortcode
Once you’ve saved and activated your plugin, you can place your CTAs anywhere on your WordPress site.
- Go to Custom CTAs in your WordPress admin menu.
- Create a new CTA or edit an existing one. Give it a unique “CTA Internal Name” (e.g., “Homepage Banner”).
- Save All CTAs.
- After saving, you’ll see a shortcode displayed next to your CTA’s name: (the ID will match your internal name).
- Copy this shortcode.
- Paste it into any:
- WordPress post or page (using a “Shortcode” block in the Block Editor, or directly in the Classic Editor).
- Text Widget.
- Page builder module that accepts shortcodes.
Section 6: Final Touches & Best Practices
Now that you’ve built and implemented your custom CTA plugin, here are some important tips:
- File Permissions: If you ever encounter issues with your CSS or JS files not updating after plugin reactivation, check your server’s file permissions. The
cssandjsfolders should typically be755, and the.cssand.jsfiles themselves644. - Clear Caches: Always, always, always clear your caches after making plugin changes! This includes:
- WordPress Caching Plugins: If you use one (WP Super Cache, LiteSpeed Cache, W3 Total Cache, etc.).
- Server-Side Caching: If your host provides it (e.g., Cloudflare, Varnish).
- Browser Cache: Perform a hard refresh (Ctrl+F5 or Cmd+Shift+R) to ensure your browser loads the freshest files.
- Security: We’ve used WordPress’s sanitization functions, which is a great start. Always be mindful of input validation and output escaping in any custom code.
- Further Customization Ideas:
- More Layouts: Add options in the admin to choose between image-left, image-right, or image-top layouts.
- Custom Fonts: Integrate Google Fonts or other font options for titles/buttons.
- Animations: Use a JavaScript library like AOS (Animate On Scroll) to make CTAs animate as they come into view.
- Pre-made Templates: Allow users to choose from a few pre-designed CTA templates.
One Last Tip
In some server, I have noticed that the color customizer feature for the CTA button only works AFTER YOU HAVE SAVED THE CTA.
Conclusion: Empowering Your WordPress Site
You’ve just built a powerful, flexible, and completely custom Call-to-Action plugin for your WordPress site! This not only gives you full control over your marketing messages but also deepens your understanding of WordPress plugin development.
By mastering this technique, you can create tailor-made solutions that perfectly fit your site’s needs, without relying on bloated third-party plugins.
Go forth and create compelling Call-to-Actions that convert!
And here (below) is the live and interactive version of the CTA plugin for WordPress:
5 Minutes Tutorial
Making a Word Count Google Chrome Extension
Digital Designer, blog writer and also a tech enthusiast.
He loves to write content for blogs, podcasts, design websites, logos, brochures, banners and anything that sends a message to an audience.
You can contact Daniele at the link below:



