Skip to content
ThemesIonic — home
WordPress Tutorials

Adding Custom Classes to the WordPress Body Tag

The front end filter takes an array and the admin one takes a string. Swapping the two is the single most common reason a class appears on one side of the site and breaks the other.

3 min read beginner

The front end filter and its admin counterpart do not share a signature. body_class receives an array of class names and expects an array back. admin_body_class receives a space-separated string and expects a string back. Treating them alike is the reason a class that works on the site produces class="Array" in the dashboard.

Adding a class on the front end

add_filter( 'body_class', 'ti_body_class' );

function ti_body_class( $classes ) {
	if ( is_singular( 'post' ) && has_post_thumbnail() ) {
		$classes[] = 'has-hero';
	}

	return $classes;
}

Appending with $classes[] = preserves everything WordPress and the theme already added. Assigning a fresh array instead removes the class WordPress uses to identify the template, the post type, the logged-in state and much else, and the theme's own CSS stops matching. That failure looks like the stylesheet did not load.

The second argument holds any classes passed directly to the body_class() call in the template. It is rarely needed, but a theme that passes classes there is the reason a value appears that is in no filter.

The admin variant

add_filter( 'admin_body_class', 'ti_admin_body_class' );

function ti_admin_body_class( $classes ) {
	$screen = get_current_screen();

	if ( $screen && 'post' === $screen->id ) {
		$classes .= ' ti-post-editor';
	}

	return $classes;
}

Two details: concatenation rather than array append, and a leading space inside the appended string. Without the space the new name fuses onto the last existing one, producing a single class that matches nothing and quietly removes the one that was there.

Sanitising values that come from content

Anything derived from a post, a term or a user has to be cleaned before it becomes a class:

$term = get_queried_object();

if ( $term instanceof WP_Term ) {
	$classes[] = 'term-' . sanitize_html_class( $term->slug );
}

sanitize_html_class removes characters that cannot legally appear in a class attribute. Without it, a term named with an apostrophe or a quotation mark closes the attribute early, and the rest of the body tag becomes markup — an injection with a content editor rather than an attacker at the other end, but broken output either way.

What WordPress already gives you

Before adding anything, check whether the class exists. The default set is large and covers most of what themes ask for:

Class Present when
home, blog Front page and posts index
single, page, archive, search The template type
postid-123, page-id-45 The specific object
logged-in, admin-bar Viewer state
category-news, tag-css Terms on the current post

Adding single-post to a page that already has single and postid-12 is work that produces nothing. The reason to add a class is a condition WordPress does not already express — a custom field value, a computed state, a business rule.

The theme has to ask for them

The filter supplies values to the body_class() function. A theme whose header.php contains a hard-coded <body> never calls it, so nothing you add appears. The fix is in the template rather than the filter:

<body <?php body_class(); ?>>

A block theme has no header.php. It renders the document shell itself and applies body classes through the block template system, which means the filter still works but the surrounding markup is not yours to edit. If a class is missing there, the cause is the rendering path rather than a missing call, and the background is in what a WordPress block theme is.

Using the class rather than adding more

Once the class exists, styling belongs in the stylesheet, not in more PHP:

.has-hero .entry-title { margin-block-start: 0; }

This is the cheapest way to vary a layout by template without duplicating template files, and it keeps the decision in one place. Where the CSS itself should live is covered in adding custom CSS to WordPress.

Verify on both sides

View source on the front end and confirm the class appears on the body tag alongside the defaults. Then open a post in the editor and confirm the admin class appears and that no stray Array is in the attribute.

If the front end works and the admin shows Array, the signatures were swapped. If neither works, the theme is not calling the function. If the class appears but styles do not apply, the problem has moved to CSS specificity rather than the filter, and the trail continues at custom CSS.

Frequently asked

Because admin_body_class passes a string, not an array. Returning an array from it produces an array-to-string conversion notice and a body tag with the word Array in it.
Because the theme's body tag does not call the body_class function. The filter only supplies values to that function, so a hard-coded body tag never receives them.
Yes, whenever any part of it comes from post data, a term name or user input. sanitize_html_class strips anything that cannot legally appear in a class attribute.
Tagged CSS

Related guides