WordPress uses CodeMirror for the CSS editor within its Customiser. CodeMirror provides support for syntax highlighting, linting, and code competition. Would it not be great to be able to use that in your own Widget?
>>> This article has been updated following the release of WordPress 5.8. WordPress 5.8 introduced the block editor to widget areas. Traditional widgets were still supported. However, the method, as described here, to monitor the visibility of CodeMirror element required updating.
First off, we are going to start with a basic bare-bones demo widget that allows the user to input some text to be displayed and the CSS styling to apply.

This is the code for our bare-bones widget, placed into WordPress’s plugins directory. E.g. plugins/demo-css-widget/demo-css-widget.php.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | <?php /* Plugin Name: Demo CSS Widget */ add_action('widgets_init', 'demo_on_widgets_init'); function demo_on_widgets_init() { register_widget('demo_css_widget'); } define('DEFAULT_DEMO_CCS', ".demo-css-widget-text {\n\tcolor:blue;\n\tfont-size:40px;\n}"); class demo_css_widget extends WP_Widget { public function __construct() { $widget_options = array( 'classname' => 'demo-css-widget', 'description' => 'Demo of CSS editor within a widget' ); parent::__construct('demo-css-widget', 'Demo CSS Widget', $widget_options); } public function widget($args, $instance) { $text = isset($instance['text']) ? $instance['text'] : ''; $CSS = isset($instance['css']) ? esc_html($instance['css']) : esc_html(DEFAULT_DEMO_CCS); echo $args['before_widget']; echo "<style type='text/css'>",$CSS,'</style>'; echo "<span class='demo-css-widget-text'>", esc_html($text), '</span>'; echo $args['after_widget']; } public function form($instance) { $text = isset($instance['text']) ? esc_attr($instance['text']) : ''; $CSS = isset($instance['css']) ? esc_textarea($instance['css']) : esc_textarea(DEFAULT_DEMO_CCS); ?> <div> Text: <input type='text' class='widefat' id='<?php echo $this->get_field_id( 'text' ); ?>' name='<?php echo $this->get_field_name( 'text' ); ?>' value='<?php echo $text; ?>' > CSS: <textarea class='widefat code content my-demo-css-editor-textarea' rows='16' cols='20' id='<?php echo $this->get_field_id( 'css' ); ?>' name='<?php echo $this->get_field_name( 'css' ); ?>'><?php echo esc_textarea( $CSS );?></textarea> <script type='text/javascript'> demo_init_widget_css_editor('<?php echo $this->get_field_id( 'css' ); ?>'); </script> </div> <?php } public function update($new_instance, $old_instance) { $new_instance['text'] = isset($new_instance['text']) ? sanitize_text_field($new_instance['text']) : ''; return $new_instance; } } |
When placed within a Sidebar or other widget area, the end result, using the widget’s default CSS it should look similar to this: –

So far, we are only using a plain old, nothing fancy, <textarea> to edit our CSS. Using CodeMirror will require some JavaScript. To do this we add the following to our widget’s PHP code:-
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | if (is_admin()) { add_action('admin_enqueue_scripts', 'demo_on_admin_enqueue_scripts'); } function demo_on_admin_enqueue_scripts() { wp_register_script('demo-css-widget-admin.js', plugins_url('demo-css-widget/admin.js'), array('jquery'), '1.0.0'); wp_enqueue_script('demo-css-widget-admin.js'); // Are we using the widget block editor introduced into WP 5.8? if( function_exists('wp_use_widgets_block_editor') ) { $use_widgets_block_editor = wp_use_widgets_block_editor(); } else { $use_widgets_block_editor = false; } $demo_cm_settings['codeEditor'] = wp_enqueue_code_editor( array( 'type' => 'text/css', 'codemirror' => array( 'indentUnit' => 2, 'tabSize' => 2, 'lineNumbers' => true ), 'use_widgets_block_editor' => $use_widgets_block_editor, ) ); wp_localize_script('jquery', 'demo_cm_settings', $demo_cm_settings); // ensure WordPress's scripts are enqueued wp_enqueue_script('wp-theme-plugin-editor'); wp_enqueue_style('wp-codemirror'); } |
$demo_cm_settings is used to specify settings to be passed to CodeMirror. We are creating these in PHP and localising them to a JavaScript object called demo_cm_settings. This object will be available within our JavaScript file admin.js. You can view available options in CodeMirror’s manual here.
When using CodeMirror we specify an existing <textarea>. CodeMirror dynamically modifies the webpage, creating its own elements and takes the existing content from our <textarea> and places it into its own interface.
Before we go into the details of our JavaScript we need to amend the Widget’s form rendering to invoke our JavaScript function.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | public function form($instance) { $text = isset($instance['text']) ? esc_attr($instance['text']) : ''; $CSS = isset($instance['css']) ? esc_textarea($instance['css']) : esc_textarea(DEFAULT_DEMO_CCS); ?> Text: <input type='text' class='widefat' id='<?php echo $this->get_field_id('text'); ?>' name='<?php echo $this->get_field_name('text'); ?>' value='<?php echo $text;?>' > CSS: <textarea class='widefat code content my-demo-css-editor-textarea' rows='16' cols='20' id='<?php echo $this->get_field_id('css'); ?>' name='<?php echo $this->get_field_name('css'); ?>'><?php echo $CSS ?></textarea> <script type='text/javascript'> demo_init_widget_css_editor('<?php echo $this->get_field_id('css'); ?>'); </script> <?php } |
The next step is to create our supporting JavaScript file that contains our main function demo_init_widget_css_editor(). I found that if the textarea was not visible when initialising CodeMirror the UI did not render correctly. To overcome this we are checking WordPress’s widget container to see if it is expanded or not. If not, we are creating a MutationObserver to delay initialisation until it is.
>>> From WordPress 5.8 traditional widgets are wrapped with new elements. As such, we need to monitor the visibility on the textarea by looking at different parent.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 | function demo_init_widget_css_editor(textarea_id) { //Ignore template version created by WordPress on the widgets admin screen if (textarea_id.indexOf('__i__') < 0) { // Are we using WP 5.8+ with block based widgets? if (demo_cm_settings.use_widgets_block_editor) { // when using the block based widget editor (introduced in WP 5.8) // when the widget is displayed, the parent of the item with the class '.widget-inside' // has another parent with the class 'wp-block-legacy-widget__edit-form' that has the // hidden attribute set when the widget form is not on display // Only when our textarea is visible can we initialize CodeMirror. I.e. when parent div // 'wp-block-legacy-widget__edit-form' is not hidden. var widget_insides_parent = jQuery('#' + textarea_id).parents('.widget-inside').first().parent().parent(); if (widget_insides_parent.attr('hidden') === undefined) { demo_init_widget_codemirror(textarea_id); } else { // Not currently on display - We need to wait until the textarea becomes visible var only_once = false; var observer = new MutationObserver(function (mutations) { if (false == only_once) { if (widget_insides_parent.attr('hidden') === undefined) { only_once = true; // prevent multiple initilizations demo_init_widget_codemirror(textarea_id); } } }); var config = { attributes: true }; observer.observe(widget_insides_parent[0], config); } } else { // when the widget is displayed, the parent of the item with the class .widget-inside // is assigned class 'open' - this in an operation performed by WordPress // Only when our textarea is visible can we initialize CodeMirror var widget_inside_parent = jQuery('#' + textarea_id).parents('.widget-inside').first().parent(); if (widget_inside_parent.attr('class').indexOf('open') >= 0) { demo_init_widget_codemirror(textarea_id); } else { // Not currently on display - We need to wait until the textarea becomes visible var only_once = false; var observer = new MutationObserver(function (mutations) { if (false === only_once) { if (widget_inside_parent.attr('class').indexOf('open') >= 0) { only_once = true; // prevent multiple initilizations demo_init_widget_codemirror(textarea_id); } } }); var config = { attributes: true }; observer.observe(widget_inside_parent[0], config); } } } } function demo_init_widget_codemirror(textarea_id) { var ta = jQuery('#' + textarea_id); //avoid initialization of the same textarea more than once //avoids issues with multiple calls when widget is displayed within WordPress's customizer var flagged = ta.attr('done-code-mirror-flag'); if (undefined === flagged || 'done' !== flagged ) { ta.attr('done-code-mirror-flag', 'done'); var cei = wp.codeEditor.initialize(ta, demo_cm_settings.codeEditor); cei.codemirror.on('change', function (cm, obj) { //Copy the content of the CodeMirror editor into the textarea. cm.save(); //Invoke the change event on the textarea, so the form's Save button becomes enabled jQuery('#' + textarea_id).trigger('change'); }); } } jQuery(document).ready(function () { //The 'widget-added' event is Invoked when a widget is added to //a SideBar or WordPress's Widgets Admin Page jQuery(document).on('widget-added', function (event, widgetContainer) { var is_this_my_widget = widgetContainer.find('.my-demo-css-editor-textarea'); if (is_this_my_widget.length > 0) { demo_init_widget_css_editor(is_this_my_widget.attr('id')); } }); }); |
In function demo_init_widget_codemirror() a listener on CodeMirror’s change event is created. When invoked we obtain the text from the CodeMirror object and place that into our now hidden <textarea>. We also invoke the textarea’s change event to ensure that WordPress enables the form’s save button.
In the document ready() function a listener for WordPress’s widget-added event is created. This is to ensure that we initialise CodeMirror when our widget is added to a widget area (e.g. a sidebar) when working with WordPress’s customizer or admin pages. We are checking to see if the new widget is an instance of our widget by looking for an item with class my-demo-css-editor-textarea. This is the class name we have given to our <textarea> element.
After putting all this together our widget’s form should now look similar to this:

And that’s it. Here is final version of our demo-css-widget.php.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 | <?php /* Plugin Name: Demo CSS Widget */ add_action('widgets_init', 'demo_on_widgets_init'); if (is_admin()) { add_action('admin_enqueue_scripts', 'demo_on_admin_enqueue_scripts'); } function demo_on_widgets_init() { register_widget('demo_css_widget'); } function demo_on_admin_enqueue_scripts() { wp_register_script('demo-css-widget-admin.js', plugins_url('demo-css-widget/admin.js'), array('jquery'), '1.0.0'); wp_enqueue_script('demo-css-widget-admin.js'); // Are we using the widget block editor introduced into WP 5.8? if( function_exists('wp_use_widgets_block_editor') ) { $use_widgets_block_editor = wp_use_widgets_block_editor(); } else { $use_widgets_block_editor = false; } $demo_cm_settings['codeEditor'] = wp_enqueue_code_editor( array( 'type' => 'text/css', 'codemirror' => array( 'indentUnit' => 2, 'tabSize' => 2, 'lineNumbers' => true ), 'use_widgets_block_editor' => $use_widgets_block_editor, ) ); wp_localize_script('jquery', 'demo_cm_settings', $demo_cm_settings); // ensure WordPress's scripts are enqueued wp_enqueue_script('wp-theme-plugin-editor'); wp_enqueue_style('wp-codemirror'); } define('DEFAULT_DEMO_CCS', ".demo-css-widget-text {\n\tcolor:blue;\n\tfont-size:40px;\n}"); class demo_css_widget extends WP_Widget { public function __construct() { $widget_options = array( 'classname' => 'demo-css-widget', 'description' => 'Demo of CSS editor within a widget' ); parent::__construct('demo-css-widget', 'Demo CSS Widget', $widget_options); } public function widget($args, $instance) { $text = isset($instance['text']) ? $instance['text'] : ''; $CSS = isset($instance['css']) ? esc_html($instance['css']) : esc_html(DEFAULT_DEMO_CCS); echo $args['before_widget']; echo "<style type='text/css'>", $CSS, '</style>'; echo "<span class='demo-css-widget-text'>", esc_html($text), '</span>'; echo $args['after_widget']; } public function form($instance) { $text = isset($instance['text']) ? esc_attr($instance['text']) : ''; $CSS = isset($instance['css']) ? esc_textarea($instance['css']) : esc_textarea(DEFAULT_DEMO_CCS); ?> <div> Text: <input type='text' class='widefat' id='<?php echo $this->get_field_id( 'text' ); ?>' name='<?php echo $this->get_field_name( 'text' ); ?>' value='<?php echo $text; ?>' > CSS: <textarea class='widefat code content my-demo-css-editor-textarea' rows='16' cols='20' id='<?php echo $this->get_field_id( 'css' ); ?>' name='<?php echo $this->get_field_name( 'css' ); ?>'><?php echo esc_textarea( $CSS );?></textarea> <script type='text/javascript'> demo_init_widget_css_editor('<?php echo $this->get_field_id( 'css' ); ?>'); </script> </div> <script type='text/javascript'> demo_init_widget_css_editor('<?php echo $this->get_field_id('css'); ?>'); </script> <?php } public function update($new_instance, $old_instance) { $new_instance['text'] = isset($new_instance['text']) ? sanitize_text_field($new_instance['text']) : ''; return $new_instance; } } |