2026年11月12日

2026年11月12日

Customize WordPress Comments with a wp_list_comments Callback

This article is a localized edition of the Japanese original. Read the Japanese original

In a classic theme, the WordPress comment section is built from the comments.php template, wp_list_comments(), which outputs the list of comments, and comment_form(), which outputs the form. If you pass a callback function to wp_list_comments(), you can build the HTML for each comment however you like. The functions and arguments in this article were checked against the function reference on developer.wordpress.org and the WordPress source code (wp-includes/comment-template.php, class-walker-comment.php) as of November 2026.

Block themes (such as the default themes from Twenty Twenty-Two onward) do not use comments.php; you build the comment display with the "Comments" block in the Site Editor. This article is for classic themes. Edits made directly to theme files are lost when the theme updates, so work in a child theme.

A basic comments.php

Calling comments_template() in a single-post template (such as single.php) loads the theme's comments.php. Here is a basic version that uses a callback function.

<?php
// comments.php
// On a password-protected post, output nothing until the password has been entered
if ( post_password_required() ) {
	return;
}
?>
<div id="comments" class="comments-area">

	<?php if ( have_comments() ) : ?>
		<h2 class="comments-title">
			<?php comments_number( 'No comments yet', '1 comment', '% comments' ); ?>
		</h2>

		<ol class="comment-list">
			<?php
			wp_list_comments(
				array(
					'style'       => 'ol',
					'callback'    => 'mytheme_comment',
					'avatar_size' => 48,
				)
			);
			?>
		</ol>

		<?php
		the_comments_pagination(
			array(
				'prev_text' => 'Previous comments',
				'next_text' => 'Next comments',
			)
		);
		?>
	<?php endif; ?>

	<?php if ( ! comments_open() && get_comments_number() ) : ?>
		<p class="no-comments">Comments are closed for this post.</p>
	<?php endif; ?>

	<?php comment_form( mytheme_comment_form_args() ); ?>

</div>
  • The three arguments of comments_number() are the text for zero, one, and two or more comments. % is replaced with the count.
  • the_comments_pagination() outputs links only when you have chosen to break comments into pages in "Settings" → "Discussion" (設定 → ディスカッション in the Japanese UI). The number per page is set on the same screen.
  • comment_form() outputs nothing on posts that are closed to comments.

Build the HTML for each comment with a callback

The function you pass as the callback of wp_list_comments() is called once per comment with three arguments: ( $comment, $args, $depth ). This function outputs the opening tag (such as <li>), but not the closing tag. WordPress adds </li> after it has output the child comments (replies).

<?php
// functions.php
function mytheme_comment( $comment, $args, $depth ) {
	$tag = ( 'div' === $args['style'] ) ? 'div' : 'li';

	// Show pingbacks and trackbacks as a simple one-liner
	if ( in_array( $comment->comment_type, array( 'pingback', 'trackback' ), true ) ) {
		?>
		<<?php echo $tag; ?> id="comment-<?php comment_ID(); ?>" <?php comment_class( 'pingback', $comment ); ?>>
			<div class="comment-body">Pingback: <?php comment_author_link( $comment ); ?></div>
		<?php
		return;
	}

	$post           = get_post( $comment->comment_post_ID );
	$is_post_author = $post && $comment->user_id && (int) $comment->user_id === (int) $post->post_author;
	$parent_class   = empty( $args['has_children'] ) ? '' : 'parent';
	?>
	<<?php echo $tag; ?> id="comment-<?php comment_ID(); ?>" <?php comment_class( $parent_class, $comment ); ?>>
		<article id="div-comment-<?php comment_ID(); ?>" class="comment-body">

			<header class="comment-meta">
				<div class="comment-author vcard">
					<?php
					if ( 0 !== (int) $args['avatar_size'] ) {
						echo get_avatar( $comment, $args['avatar_size'] );
					}
					?>
					<span class="fn"><?php comment_author_link( $comment ); ?></span>
					<?php if ( $is_post_author ) : ?>
						<span class="post-author-badge">Author</span>
					<?php endif; ?>
				</div>

				<a class="comment-date" href="<?php echo esc_url( get_comment_link( $comment ) ); ?>">
					<time datetime="<?php comment_time( 'c' ); ?>">
						<?php comment_date( 'F j, Y', $comment ); ?> <?php comment_time( 'H:i' ); ?>
					</time>
				</a>

				<?php if ( '0' === $comment->comment_approved ) : ?>
					<p class="comment-awaiting-moderation">Your comment is awaiting moderation.</p>
				<?php endif; ?>
			</header>

			<div class="comment-content">
				<?php comment_text(); ?>
			</div>

			<?php
			comment_reply_link(
				array_merge(
					$args,
					array(
						'add_below' => 'div-comment',
						'depth'     => $depth,
						'max_depth' => $args['max_depth'],
						'before'    => '<div class="reply">',
						'after'     => '</div>',
					)
				)
			);
			?>
		</article>
	<?php
	// Do not output </li> (WordPress adds it)
}

Key points in the code:

  • Reply link: Match the add_below of comment_reply_link() with the first part of the id on the element wrapping the comment body (div-comment-コメントID, that is, div-comment- followed by the comment ID). When someone clicks reply, the form moves below this element. max_depth is the nesting depth set in the threaded (nested) comments setting in "Settings" → "Discussion"; comments at that depth get no reply link.
  • Post-author badge: For comments the post author wrote while logged in, comment_class() automatically adds the bypostauthor class. If you only want to change the look, you can use CSS such as .bypostauthor > .comment-body { … }. The example above also prints a text badge, so it compares the comment's user ID with the post author's ID after casting both to integers (values read from the database are strings, so comparing them as is with === does not match).
  • Awaiting moderation: comment_approved is the string '0', and a comment awaiting moderation is shown only in the browser of the person who wrote it.
  • Avatar: Setting avatar_size to 0 hides the avatar in this example. If avatars are turned off in "Settings" → "Discussion", get_avatar() returns nothing.

Load the reply script

To move the form below the comment when someone clicks a reply link, load the comment-reply script bundled with WordPress. In a classic theme, the theme has to load it.

<?php
// functions.php
add_action( 'wp_enqueue_scripts', function () {
	if ( is_singular() && comments_open() && get_option( 'thread_comments' ) ) {
		wp_enqueue_script( 'comment-reply' );
	}
} );

Without it, replying still works, but the page reloads and the form stays at the bottom of the page.

Change the comment form text

Passing an array of arguments to comment_form() lets you change the heading and button text. Here is the mytheme_comment_form_args() used in comments.php.

<?php
// functions.php
function mytheme_comment_form_args() {
	return array(
		'title_reply'          => 'Leave a comment',
		'title_reply_to'       => 'Reply to %s', // %s is the name being replied to
		'cancel_reply_link'    => 'Cancel reply',
		'label_submit'         => 'Post comment',
		'comment_notes_before' => '<p class="comment-notes">Your email address will not be published.</p>',
		'comment_field'        => '<p class="comment-form-comment"><label for="comment">Comment</label><textarea id="comment" name="comment" cols="45" rows="6" maxlength="65525" required></textarea></p>',
	);
}

// Remove the "Website" field
add_filter( 'comment_form_default_fields', function ( $fields ) {
	unset( $fields['url'] );
	return $fields;
} );
  • For the name and email fields, removing or changing only what you need with the comment_form_default_fields filter, as above, is safer than replacing them all with fields. The default fields include the value that refills the previously entered name and email, and the required-field indicators.
  • Do not change the name attributes of the fields (author, email, url, comment). WordPress reads the values by these names.
  • The checkbox "Save my name, email, and website in this browser for the next time I comment." appears when it is enabled in "Settings" → "Discussion". Even if you pass fields, WordPress adds this field when it is missing.

How to add a field and save and display its value is covered in "How to customize the WordPress comment form".

Check the result

  1. On a post that accepts comments, post comments both logged out and logged in, and check the list and the awaiting-moderation message.
  2. Click a reply link and check that the form moves below that comment and returns with "Cancel reply".
  3. Post a comment from the post author's account and check that the badge appears.
  4. Turn on paged comments in "Settings" → "Discussion" and check that the pagination links appear.

Related articles

Questions about WordPress? The publisher of this site is based in Japan and replies in Japanese. Contact (Japanese)

お気軽にご相談ください

お見積りへ お問い合わせへ
▲