2026年11月13日

2026年11月13日

Switch WordPress Post Lists Between 1, 2 and 3 Columns with CSS Grid

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

To let readers switch your WordPress post list between 1, 2 and 3 columns with buttons, apply CSS Grid to the element that wraps the list and switch the column count with a data-columns attribute. If the JavaScript only rewrites that attribute and CSS decides all of the appearance, the result is hard to break and easy to fix.

The code in this article uses only functions available in WordPress 6.9 as of November 2026, plus CSS and JavaScript that work in major browsers. It assumes you are adding it to a child theme of a classic theme (a theme built from PHP template files).

Check first: a fixed column count can be set in the editor

If readers do not need to switch, and you only want to "show the list in 3 columns", you do not need any code. In a block theme, open the list template in the Site Editor, select the "Post Template" block inside the "Query Loop", and choose "Grid view" in the toolbar; you can then set the number of columns.

This article shows how to build it when you want readers to choose the layout.

Overall structure

File Role
functions.php Loads the CSS and JavaScript only on list pages
index.php (or archive.php) Outputs the switch buttons and the list with a data-columns attribute
css/layout-switch.css Sets the column count for each data-columns value
js/layout-switch.js Rewrites data-columns when a button is pressed and saves the choice

Step 1: Set the column count in CSS

Create css/layout-switch.css in your child theme folder.

.post-grid {
	display: grid;
	gap: 1.5rem;
	grid-template-columns: repeat(3, minmax(0, 1fr));
}

.post-grid[data-columns="1"] {
	grid-template-columns: minmax(0, 1fr);
}

.post-grid[data-columns="2"] {
	grid-template-columns: repeat(2, minmax(0, 1fr));
}

.post-grid[data-columns="3"] {
	grid-template-columns: repeat(3, minmax(0, 1fr));
}

.post-grid img {
	width: 100%;
	height: auto;
}

.layout-switch {
	display: flex;
	gap: 0.5rem;
	margin-bottom: 1.5rem;
}

.layout-switch button[aria-pressed="true"] {
	font-weight: bold;
	text-decoration: underline;
}

/* On smartphones, always use 1 column and hide the switch buttons */
@media (max-width: 781px) {
	.post-grid[data-columns] {
		grid-template-columns: minmax(0, 1fr);
	}

	.layout-switch {
		display: none;
	}
}

There are two key points.

  • Using minmax(0, 1fr) instead of 1fr keeps the columns evenly wide even when there are long URLs or large images. With 1fr alone, a column cannot shrink below its content's minimum width, and column widths can become uneven.
  • The column count is switched with a data-columns attribute on the list element itself. You could also add a class to a parent element and target it with a descendant selector, but if the element the JavaScript adds the class to and the CSS selector get out of sync, it stops working; keeping both on the same element's attribute makes mistakes less likely.

Step 2: Write the switch script

Create js/layout-switch.js in your child theme folder.

( function () {
	var grid = document.querySelector( '.post-grid' );
	var buttons = document.querySelectorAll( '.layout-switch [data-columns]' );
	if ( ! grid || ! buttons.length ) {
		return;
	}
	var STORAGE_KEY = 'mytheme-columns';

	function apply( columns ) {
		grid.setAttribute( 'data-columns', columns );
		buttons.forEach( function ( button ) {
			var pressed = button.getAttribute( 'data-columns' ) === columns;
			button.setAttribute( 'aria-pressed', pressed ? 'true' : 'false' );
		} );
	}

	// Use the previously chosen column count if there is one (ignored where storage is unavailable)
	var saved = null;
	try {
		saved = window.localStorage.getItem( STORAGE_KEY );
	} catch ( e ) {}
	if ( saved === '1' || saved === '2' || saved === '3' ) {
		apply( saved );
	}

	buttons.forEach( function ( button ) {
		button.addEventListener( 'click', function () {
			var columns = button.getAttribute( 'data-columns' );
			apply( columns );
			try {
				window.localStorage.setItem( STORAGE_KEY, columns );
			} catch ( e ) {}
		} );
	} );
} )();
  • The chosen column count is saved in the browser's localStorage, so the same layout is shown the next time the page opens. It is wrapped in try so that it does not stop with an error where saving is unavailable, such as in private browsing.
  • aria-pressed is an attribute that tells screen readers whether a button is in the pressed state. The CSS also uses it to change the look of the pressed button.

Step 3: Load them in functions.php

Do not write <link> or <script> directly in the template; load CSS and JavaScript with the wp_enqueue_scripts hook. Add this to your child theme's functions.php.

<?php
// Add to your child theme's functions.php
add_action( 'wp_enqueue_scripts', 'mytheme_layout_switch_assets' );
function mytheme_layout_switch_assets() {
	// Load only on post lists (blog home, archives, search results)
	if ( ! is_home() && ! is_archive() && ! is_search() ) {
		return;
	}
	$dir = get_stylesheet_directory_uri(); // For a parent theme, use get_template_directory_uri()
	wp_enqueue_style( 'mytheme-layout-switch', $dir . '/css/layout-switch.css', array(), '1.0.0' );
	wp_enqueue_script(
		'mytheme-layout-switch',
		$dir . '/js/layout-switch.js',
		array(),
		'1.0.0',
		array(
			'in_footer' => true,
			'strategy'  => 'defer',
		)
	);
}

Passing an array as the fifth argument of wp_enqueue_script() (specifying defer with strategy) is available from WordPress 6.3. A script with defer runs after the HTML has been parsed, so the script never fails to find the list element.

Add the code after the existing contents of your child theme's functions.php. The file already starts with <?php, so do not write a second one. The loading mechanism is explained in detail in How to load JS and CSS with wp_enqueue_scripts in WordPress.

Step 4: Write the list and buttons in the template

Copy the template that outputs your post list (index.php in many themes, archive.php for category and other lists) from the parent theme to the child theme and edit it. Write the list part like this.

<?php get_header(); ?>

<main id="main" class="site-main">
	<div class="layout-switch" role="group" aria-label="Number of columns in the list">
		<button type="button" data-columns="1" aria-pressed="false">1 column</button>
		<button type="button" data-columns="2" aria-pressed="false">2 columns</button>
		<button type="button" data-columns="3" aria-pressed="true">3 columns</button>
	</div>

	<?php if ( have_posts() ) : ?>
		<div class="post-grid" data-columns="3">
			<?php while ( have_posts() ) : the_post(); ?>
				<article id="post-<?php the_ID(); ?>" <?php post_class( 'post-grid__item' ); ?>>
					<a href="<?php the_permalink(); ?>">
						<?php if ( has_post_thumbnail() ) : ?>
							<?php the_post_thumbnail( 'medium_large' ); ?>
						<?php endif; ?>
						<h2 class="post-grid__title"><?php the_title(); ?></h2>
					</a>
					<?php the_excerpt(); ?>
				</article>
			<?php endwhile; ?>
		</div>
		<?php the_posts_pagination(); ?>
	<?php else : ?>
		<p>No posts found.</p>
	<?php endif; ?>
</main>

<?php get_footer(); ?>
  • The initial column count is set by data-columns="3" on the list and aria-pressed="true" on the 3-column button. To make 2 columns the default, change both together.
  • Match the id and class names of main to your theme's original template. The sidebar and content width are set by the theme's CSS, so it is safest to keep the outer structure of the original template and replace only the list part.
  • Because the buttons are <button type="button">, readers can also move to them with the Tab key and switch with Enter or Space.

Step 5: Check the result

  1. Open a list page and check that pressing the buttons changes the column count.
  2. Reload the page and check that it stays at the column count you chose.
  3. Narrow the browser to 781px or less (the device mode in the developer tools also works) and check that it becomes 1 column and the buttons disappear.

If it does not switch, check the following in the developer tools.

  • In "Network", check that layout-switch.css and layout-switch.js load with status 200. If you get 404, review where the files are and whether you used get_stylesheet_directory_uri() (child theme) or get_template_directory_uri() (parent theme).
  • In "Elements", check whether data-columns on .post-grid changes when you press a button. If it changes, the problem is on the CSS side; if not, it is on the JavaScript side.
  • If you use a caching plugin or a CDN, clear the cache before checking. When you fix the CSS or JavaScript, raising the version (1.0.0) in wp_enqueue_style() and wp_enqueue_script() makes it less likely that browsers keep the old file.

Related articles

References: wp_enqueue_script() (WordPress Developer Resources), grid-template-columns (MDN), aria-pressed (MDN)

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

お気軽にご相談ください

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