Creating an expandable and collapsible archive list in WordPress is a great way to provide users with an organized view of your site’s content, allowing them to explore posts by year and month. WordPress natively supports yearly or monthly archives, but with a bit of custom code, you can easily generate both within an expandable format. This guide will walk you through building a lightweight, responsive, and accessible expandable archive list using PHP, vanilla JavaScript, and CSS.
This method adheres to the most recent WordPress and PHP versions, PHP 8+, while following the WordPress Coding Standards (WPCS).
Table of Contents
- Step 1: Filter to Query Archives by Year
- Step 2: Display the Archive List
- Step 3: Expandable Archive List with Vanilla JavaScript
- Step 4: Style the Archive List
- Step 5: Enqueue Font Awesome Icons
Step 1: Filter to Query Archives by Year
We first need to create a custom filter to retrieve archive posts by year. By default, WordPress doesn’t provide a built-in way to show both yearly and monthly archives in an expandable format, so we will use the getarchives_where
filter to handle this.
/**
* Filter to query archives by year.
*
* @param string $where SQL WHERE clause.
* @param array $args Query arguments.
* @return string Filtered WHERE clause.
*/
function whiskey_getarchives_filter( string $where, array $args ) : string {
if ( isset( $args['year'] ) ) {
$where .= ' AND YEAR(post_date) = ' . intval( $args['year'] );
}
return $where;
}
add_filter( 'getarchives_where', 'whiskey_getarchives_filter', 10, 2 );
In this code, the whiskey_getarchives_filter
function checks for a year parameter in the arguments. If found, it appends an SQL WHERE
clause that limits the query to posts from that year.
Step 2: Display the Archive List
Now that we’ve created the filter to query by year, we can set up the HTML and PHP to display the expandable archive list. We’ll generate a list of years, and for each year, we’ll show a list of months (if available) with the corresponding post count. The output will be formatted as a definition list (<dl>
).
<dl class="tree-accordion">
<?php
$current_year = (int) date( 'Y' );
$years = range( $current_year, 1950 ); // Adjust the starting year accordingly.
foreach ( $years as $year ) {
?>
<dt>
<a href="#">
<i class="fa fa-fw fa-plus-square-o" aria-hidden="true"></i>
<?php echo esc_html( $year ); ?>
</a>
</dt>
<?php
$archive = wp_get_archives( [
'echo' => 0,
'show_post_count'=> 1,
'type' => 'monthly',
'year' => $year,
] );
$archive = explode( '</li>', $archive );
$links = [];
foreach ( $archive as $link ) {
$clean_link = trim( strip_tags( $link ) );
if ( ! empty( $clean_link ) ) {
$links[] = $clean_link;
}
}
$flip_links = array_reverse( $links );
if ( ! empty( $flip_links ) ) {
echo '<dd>';
foreach ( $flip_links as $link ) {
echo '<span>' . esc_html( $link ) . '</span>';
}
echo '</dd>';
} else {
echo '<dd class="tree-accordion-empty"></dd>';
}
}
?>
</dl>
Explanation
$current_year
and$years
: Generates a list of years starting from the current year and going back to 1950.wp_get_archives
: Retrieves the monthly archives for each year, displaying the post count.- HTML Structure: We use a
<dl>
element for better accessibility, with<dt>
elements for years and<dd>
elements for months.
Now, the code above is based on an older project of mine. Since then, I made some improvements to both the speed and accessibility of the queries. Instead of querying all the years back to 1950 (as I needed them for my project), you could limit the archive to only years where there are posts. This reduces the amount of data generated. Adding structured data helps search engines understand the content better. For an archive list, we could add Breadcrumb List Schema, which enhances the way search engines understand the relationship between archives.
You can try the code below:
<dl class="tree-accordion" role="list" itemscope itemtype="https://schema.org/BreadcrumbList">
<?php
global $wpdb;
$years_with_posts = $wpdb->get_col( "
SELECT DISTINCT YEAR(post_date)
FROM $wpdb->posts
WHERE post_status = 'publish'
ORDER BY post_date DESC
" );
$position = 1;
foreach ( $years_with_posts as $year ) {
?>
<dt role="listitem" itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">
<a href="#" aria-expanded="false" aria-controls="year-<?php echo esc_attr( $year ); ?>" itemprop="item">
<span itemprop="name">
<i class="fa fa-fw fa-plus-square-o" aria-hidden="true"></i>
<?php echo esc_html( $year ); ?>
</span>
</a>
<meta itemprop="position" content="<?php echo esc_attr( $position ); ?>" />
</dt>
<?php
$archives = wp_get_archives( [
'echo' => 0,
'show_post_count'=> 1,
'type' => 'monthly',
'format' => 'custom',
'before' => '<span>',
'after' => '</span>',
'year' => $year,
] );
if ( ! empty( $archives ) ) {
?>
<dd id="year-<?php echo esc_attr( $year ); ?>" aria-hidden="true">
<?php echo wp_kses_post( $archives ); ?>
</dd>
<?php } else { ?>
<dd id="year-<?php echo esc_attr( $year ); ?>" class="tree-accordion-empty" aria-hidden="true"></dd>
<?php
}
$position++;
}
?>
</dl>
Step 3: Expandable Archive List with Vanilla JavaScript
To make the archive list expandable and collapsible, we will use vanilla JavaScript. This will eliminate any need for external libraries like jQuery, ensuring better performance and reduced dependencies.
document.addEventListener('DOMContentLoaded', function () {
const allPanels = document.querySelectorAll('.tree-accordion > dd');
const links = document.querySelectorAll('.tree-accordion > dt > a');
// Hide all panels initially
allPanels.forEach(panel => {
panel.style.display = 'none';
});
// Add click event listeners to toggle panels
links.forEach(link => {
link.addEventListener('click', function (event) {
event.preventDefault();
const targetPanel = link.parentElement.nextElementSibling;
const isActive = targetPanel.classList.contains('active');
// Hide all panels
allPanels.forEach(panel => {
panel.classList.remove('active');
panel.style.display = 'none';
});
// Toggle the clicked panel
if (!isActive) {
targetPanel.classList.add('active');
targetPanel.style.display = 'block';
}
});
});
// Hide empty accordion items
document.querySelectorAll('.tree-accordion-empty').forEach(emptyEl => {
const previousEl = emptyEl.previousElementSibling;
if (previousEl) {
previousEl.style.display = 'none';
}
});
});
Explanation
- Event Listener: We use
addEventListener('click')
to handle toggle actions for each year link. - Toggle Mechanism: On click, the corresponding month list expands, while any previously expanded list is collapsed.
- Efficient DOM manipulation: Vanilla JavaScript is used for faster execution and better performance.
Step 4: Style the Archive List
Finally, we’ll add some basic CSS to style the archive list. Font Awesome is used for the plus/minus icons, which will visually indicate expandable sections.
.tree-accordion {
line-height: 1.5;
}
.tree-accordion dt a {
display: block;
text-decoration: none;
}
.tree-accordion .fa {
color: #666666;
}
.tree-accordion dd span {
display: block;
margin-left: 20px;
}
.tree-accordion dd {
margin: 0 0 0 20px;
}
This basic CSS will ensure the expandable list is displayed cleanly, with appropriate spacing and icons to enhance user experience.
Step 5: Enqueue Font Awesome Icons
To ensure the icons are available, we’ll enqueue the Font Awesome library in your theme’s functions.php
file:
function whiskey_enqueue_scripts() {
wp_enqueue_style( 'font-awesome', 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.6.0/css/all.min.css', [], '6.6.0' );
}
add_action( 'wp_enqueue_scripts', 'whiskey_enqueue_scripts' );
If you want to use minimal CSS and remove JavaScript altogether, you can use the <details>
element and put each year inside. It might not look as fancy, and it might still require a bit of JavaScript to only have one <details>
element open at a time. It all depends on your project and your implementation.
Also, the Font Awesome icons can be replaced with Unicode icons, or even emojis.