Comment form customization reloaded

Previously on the CogitActive Saga:
A downside of this approach (i.e. using a placeholder to provide a brief synopsis of my Comment Policy) is that the placeholder attribute accepts only text. In other words, I would not be able to add a link to my Comment Policy from there.

Customizing my comment form (without any plugin) was quite a business; yet, I managed to 1) remove the Website field, 2) change the default “Leave a Reply” title, and 3) add a brief synopsis of my Comment Policy as a placeholder text in the Comment field (see Customizing the WordPress comment form). I wanted to provide a link to my Comment Policy as well. However, I had to put this project on hold because the placeholder attribute is not accepting HTML markup. Nonetheless, I looked at my achievements with satisfaction. Let me emphasize that – at the time – I didn’t know much about coding.

I could have let my comment form like this (I actually did for a while; see right), if something would have not bothered me during the writing of the aforementioned post (i.e. a week ago). As explained in the latter, finding the PHP codes (to implement the above changes) was no easy task. In particular, they were not listed – at the time – as “User Contributed Notes” on the WordPress website. Fortunately, things have changed and I was glad to see that the codes I implemented were matching those now proposed in WordPress articles. Except…

First, let me remind you that I took advantage of WordPress filter hooks to implement my changes. Specifically, I used the comment_form_default_fields filter to remove the Website field and the comment_form_defaults filter for the two other tasks. As detailed in the previous post, for the magic to happen, I also created Callback functions (to achieve the desired modifications). Fortunately, the code I used to remove the Website field ended up being the same as the (official) one now listed in WordPress. Good! However, this was not the case for the code to change the title. When I compared the one I found (and used) with the one now provided in WordPress, I noticed that the latter was using $defaults rather than $args in the Callback function.

I could have ignore this – after all the code was working just fine – if it hadn’t been for CogitActive modus operandi. I first tried to find some explanations in articles on the subject matter (i.e. how to change the title of the WordPress comment form using a filter). Oh naïve! Then, I searched specifically about the comment_form_defaults filter. With the exception of the WordPress article on this hook, I couldn’t find anything relevant.

apply_filters( ‘comment_form_defaults’, array $defaults )
Filters the comment form default arguments.

Clearly, the informative value of this article is limited; yet, quite surprisingly, $args was nowhere to be found in it. Briefly, the above function – apply_filters() – is the actual function to apply the filter named comment_form_defaults. The last part – $defaults – is what to filter. In this particular case, the parameter $defaults is an array. Good luck with that!

Definitively, I could not fathom why some people are using $args and others $defaults. Now, the best way to understand what a hook does is to look at where it occurs in the source code. Therefore, I followed the View on Trac link (from the above WordPress article). Armed with my rudimentary knowledge of PHP, I began my investigation of the ‘comment_form_defaults’ filter.

Detective work

Actually, I began my investigation at line 2188 (instead of 2430), that is at the beginning of the code to output a complete commenting form.

$args vs. $defaults

The comment at the very beginning of the code (line 2191), which is also mentioned in the WordPress article about the comment_form() function, could have settle the discussion:

Most strings and form fields may be controlled through the $args array passed into the function.

Okay. However, why does the User Contributed Note in the WordPress article about the comment_form_defaults hook uses $defaults instead of $args?

To answer this question, I started to dissect the comment_form() function code. Let’s play Hot and Cold!

Line 2252: first occurrence of $args as one of the function parameters. Okay – cold! The second occurrence – in line 2273 – was probably more relevant to the task at hand: $args = wp_parse_args( $args );. If I understand correctly, this code merges the $args (user defined) arguments into the array that serves as the defaults and stores the outcome in $args. In other words, this allows the function to accept multiple $args arguments (i.e. the default ones and the user defined ones). Okay, better. Hot?

In PHP, an array allows to store multiple values under a single variable. In the present case, it is an associative array, meaning that each value is assigned to a specific key. Interestingly, the keys in questions (e.g. ‘comment_field’, ‘title_reply’… ) are exactly the same as those listed under the $args argument in the WordPress article about the comment_form() function. They are indeed the default arguments for $args.

Filters the comment form default arguments.
@param array $defaults The default comment form arguments.

The above comment introduces the filter called comment_form_defaults that is in line 2430:

$args = wp_parse_args( $args, apply_filters( 'comment_form_defaults', $defaults ) );

I assumed (wrongly) that the explanation was right after (lines 2432-2433 in the code):

// Ensure that the filtered args contain all required default values.
$args = array_merge( $defaults, $args );

To sum up, at that point I thought that the parameter to be used with the comment_form_defaults filter is $defaults. For this reason, I decided to replace $args with $defaults in my code (see Customizing the WordPress comment form). As expected, it works just as well!


Opening Pandora’s Box (i.e. inspecting the code) proved actually enlightening beyond my initial questioning. Specifically, I discovered that the comment_field argument (lines 2362-2369) defaulted to:

'comment_field'        => sprintf(
	'<p class="comment-form-comment">%s %s</p>',
		'<label for="comment">%s</label>',
		_x( 'Comment', 'noun' )
	'<textarea id="comment" name="comment" cols="45" rows="8" maxlength="65525" required="required"></textarea>'

And not to1:

'<p class="comment-form-comment"><label for="comment">' . _x( 'Comment', 'noun' ) . '</label><textarea id="comment" name="comment" cols="45" rows="8" aria-required="true"></textarea></p>'

As I said in the previous post, the comment_form() function – introduced with WordPress 3.0 – had been updated six times since! Playing “spot the difference”2, I detected three changes between the two pieces of code:

  • First, the new code has a limit of 65525 characters for the text area (see the maxlength attribute of the <textarea> tag). This modification – implemented in WordPress 4.5 – aimed at informing the commenters (before they submit their comment) that they reach this limit.
  • Second, the required attribute replaces the aria-required attribute (in the <textarea> tag). While both indicate that user input is required (before the form can be submitted), the former – new in HTML5 – is now the recommended way to go; the latter being used only for backward compatibility.
  • Third, the new code uses the sprintf() PHP function to output the formatted string; a much cleaner way to proceed with many advantages3 over the other method.


Without going into details, this function replaces the % (in the string specified in format; see below) by variable passed as an argument. The syntax of this PHP function is indeed as follow:

sprintf(format, arg1, arg2)

where, the format parameter specifies the string and how to format the variables in it, and arg1, arg2 (etc.) are the arguments to be inserted at the first, second (etc., respectively) % sign in the format string.

Simply, the function works step-be-step, inserting arg1 at the first % sign, arg2 at the second % sign, etc. For example, this code:

sprintf('<p>CogitActive % and % knowledge with a unique voice</p>', 'integrates (Cogit)', 'shares (Active)')

is equivalent to:

<p>CogitActive integrates (Cogit) and shares (Active) knowledge with a unique voice</p>

Now, there are several possible formats for the %. In particular, %s indicates that the variable to be inserted is a string. Accordingly, the ‘new’ code (see above) could be resolved to:

'<p class="comment-form-comment"><label for="comment">_x( 'Comment', 'noun' )</label> <textarea id="comment" name="comment" cols="45" rows="8" maxlength="65525" required="required"></textarea></p>',

Given these premises, I could use either one of these approaches (i.e. with sprintf() as in the ‘new’ code or without as in the ‘old’ code) – as long as I would fix the two other differences (i.e. “maxlength” and “required”). I opted for the first option, just adding my placeholder attribute (with its associated text) to the original code (see later).

Adding a link to my Comment Policy

Not content to stop there, I decided to tackle the issue with the link to my Comment Policy. As explained in the previous post, using the placeholder attribute (to provide a brief synopsis of my Comment Policy) would not allow me to add a link – the attribute accepting only text (i.e. no HTML). Therefore, to carry out my initial project, I would have no choice but to add this link in the text before the comment field, namely in the comment_notes_before argument.

Your email address will not be published. Required fields are marked *

To avoid a bulky form, I decided to replace the first sentence with my own moderation notice (including the link to my Comment Policy).

Customizing the ‘comment_notes_before’ argument

Guess what! Exactly as for the above example, the comment_notes_before argument (lines 2392-2399), which output the above text in the comment form, is also using the sprintf() function. In other words, it does NOT default (anymore) to the following code4:

'<p class="comment-notes">' . __( 'Your email address will not be published.' ) . ( $req ? $required_text : '' ) . '</p>'

Given my initial success in implementing code with the sprintf() function (see above), I thought I could simply use the following piece of code in my Callback function:

$defaults[‘comment_notes_before '] = sprintf(
	'<p class="comment-notes">%s%s</p>',
		'<span id="email-notes">%s</span>',
		__( 'All comments are moderated according to our <a href="">Comment Policy</a>.' )
	( $req ? $required_text : '' )

I should have known better, though! Using this code results in this output:

All comments are moderated according to our Comment Policy.

What happens to the “Required fields are marked *”?

The piece of code that output the missing statement is ( $req ? $required_text : '' ). If my PHP is correct, this means that if $req is true returns $required_text, otherwise (i.e. if $req is false) returns '' (i.e. nothing). In this specific case, $req should be true5 and, accordingly, $required_text should be returned – the latter being the aforementioned text (defined in line 2345). The problem is that $req and $required_text are defined earlier in WordPress code (line 2278 and line 2345, respectively), but nowhere to be found in my Callback function.

Insofar I just wanted to replace “Your email address will not be published” with my own moderation notice, my first idea (to fix this issue) was to use the PHP str_replace() function. Specifically, this function replaces some characters with some others in a string. Its syntax is str_replace(find, replace, string, count); the latter parameter, which counts the number of replacement, being optional. Simply, I had to specify the value to find (i.e. the text I wanted to replace), then the value to replace the former with (i.e. my own text), and last, to indicate the string to be searched (i.e. ‘comment_notes_before’ in $defaults). The resulting code (replacing the aforementioned one) is as follow:

$defaults['comment_notes_before'] = str_replace ( 'Your email address will not be published.', 'All comments are moderated according to our <a href="">Comment Policy</a>.', $defaults['comment_notes_before'] );

This workaround is working just fine; I actually thought it was a clever hack. Yet, not being at ease with the str_replace() function, I decided to tackle the problem differently. Simply, I added to the initial code (see above) the $req and $required_text definitions (copied from line 2278 and line 2345-2349, respectively):

$req = get_option( 'require_name_email' );
$required_text = sprintf(
        /* translators: %s: Asterisk symbol (*). */
        ' ' . __( 'Required fields are marked %s' ),
        '<span class="required">*</span>'

Styling the link

Why the link is not formatted according to my theme style?

Surprisingly, the Comment Policy link was not underlined as the other links (i.e. elsewhere on the page). Going through the style.css file of the Twenty Seventeen theme, I figured out why. To make a long story short, Twenty Seventeen styles the <a> element (as well as its pseudo-classes :focus and :hover) differently for some, but not all, classes (e.g. .entry-content). Neither the .comment-notes class nor the #email-notes id (let alone #moderation-notes) were targeted. To implement this style to the .comment-notes class as well, I added the following code in the style.css file of my child theme:

.comment-notes a {
	-webkit-box-shadow: inset 0 -1px 0 rgba(15, 15, 15, 1);
	box-shadow: inset 0 -1px 0 rgba(15, 15, 15, 1);
	-webkit-transition: color 80ms ease-in, -webkit-box-shadow 130ms ease-in-out;
	transition: color 80ms ease-in, -webkit-box-shadow 130ms ease-in-out;
	transition: color 80ms ease-in, box-shadow 130ms ease-in-out;
	transition: color 80ms ease-in, box-shadow 130ms ease-in-out, -webkit-box-shadow 130ms ease-in-out;
.comment-notes a:focus,
.comment-notes a :hover {
	color: #000;
	-webkit-box-shadow: inset 0 0 0 rgba(0, 0, 0, 0), 0 3px 0 rgba(0, 0, 0, 1);
	box-shadow: inset 0 0 0 rgba(0, 0, 0, 0), 0 3px 0 rgba(0, 0, 0, 1);

Customizing the ‘label_submit’ argument

Comments submitted for consideration are not immediately visible. All comments require approval. Please be patient while we review your contribution to ensure the best experience for our visitors.CogitActive

A direct consequence of comment moderation is that comments are not directly published. This can be a potential frustration for commenters, especially if they are expecting their comment to be posted immediately. In keeping with this issue, the Post Comment button is misleading. Therefore, I wanted to rephrase its label to “Submit Comment”. After going through all the aforementioned challenges, this task was straightforward – that is as simple as adding this line of code to my Callback function:

$defaults['label_submit'] = __('Submit Comment');

Final code

I hope I didn’t loose you going through all these technicalities. Anyway, you may wonder what code I added to my functions.php file. At the time of this writing, the code is:

function comment_form ($defaults) {
	$defaults['title_reply'] = __('Add value to this post');
	$defaults['label_submit'] = __('Submit Comment');
	$defaults['comment_field'] = sprintf(
		'<p class="comment-form-comment">%s %s</p>',
			'<label for="comment">%s</label>',
			_x( 'Comment', 'noun' )
		'<textarea id="comment" name="comment" placeholder="Comments are expected to offer constructive (either positive or negative) criticism in order to be as useful as possible to all readers.
For quick comments, please consider using the reaction buttons (Like, Agree, Disagree and/or Thank You) instead." cols="45" rows="8" maxlength="65525" required="required"></textarea>'
	$req      = get_option( 'require_name_email' );
	$required_text = sprintf(
		/* translators: %s: Asterisk symbol (*). */
		' ' . __( 'Required fields are marked %s' ),
		'<span class="required">*</span>'
	$defaults['comment_notes_before'] = sprintf(
		'<p class="comment-notes">%s%s</p>',
			'<span id="moderation-notes">%s</span>',
			__( 'All comments are moderated according to our <a href="">Comment Policy</a>.' )
		( $req ? $required_text : '' )
	return $defaults;

It ain’t over ’til it’s over

You may also wonder why I did use the class selector (i.e. .comment-notes) rather than the id selector (i.e. #moderation-notes) to target the specific element when applying the new CSS rules. I am afraid that – in this specific scenario (i.e. styling the link to my Comment Policy) – there is no logical reason (to choose one over the other). Similarly, when I styled the placeholders (see Customizing the WordPress comment form), I used .comment-form-comment over #comment. Again, no other reason than seeing this practice repeatedly.

That being said, both approaches are not always similar. In particular, unlike classes, an id is unique in a web page. Accordingly, while an id selector can identify only one element (in a web page), a class can be used to identify more than one. Knowing this difference can help deciding when to use class vs. id selectors.

1 As described in the Function Reference/comment form article (in the codex); the one available at the time I implemented my initial customization. ^
2 Code is poetry! This phrase, coined by WordPress, implies that coding is a craft that can exhibit beauty. Coding, however beautiful it may be, remains a complex skill to master. Trying to make this post a little playful will not eclipse this reality. ^
3 See this Why use sprintf function in PHP? question for some examples. My (un)educated guess is that WordPress opted for this function for translation purposes. Besides, a benefit of using this function (over the other approach) is that you are able to format only selected sections of data (i.e. not the entire output). ^
4 Not only is this code provided in the (old) codex WordPress article1 about the comment_form() function, but also by old articles explaining how to customize this part of the comment form. I could complain about the bad practice of recycling articles without updating their content, but I don’t want to sound like a broken record. ^
5 See line 2278. Briefly, this code asks if the option “require_name_email” exists (or has a value). This option, which defaults to “Yes”, specifies that the comment author must fill out his/her name and email. Therefore, TRUE should be passed to $req. ^

What do you think?
  • Like 
  • Agree 
  • Disagree 
  • Thank you