Weekly class timetable grid
Turn a flat list of sessions into an accessible day-by-time table with a shortcode, escaped output and empty cell handling.
WordPressLayoutstimetableschedulegridshortcode
Live preview
Code
<?php
/**
* [acme_timetable] weekly grid.
*
* @package Acme_Theme
*/
defined( 'ABSPATH' ) || exit;
/** Sessions: day (1 = Monday), start time, title, coach. Replace with real data. */
function acme_timetable_sessions(): array {
return apply_filters( 'acme_timetable_sessions', array(
array( 'day' => 1, 'time' => '07:00', 'title' => 'Morning flow', 'coach' => 'Ana' ),
array( 'day' => 1, 'time' => '18:00', 'title' => 'Strength', 'coach' => 'Ben' ),
array( 'day' => 3, 'time' => '18:00', 'title' => 'Mobility', 'coach' => 'Ana' ),
array( 'day' => 5, 'time' => '07:00', 'title' => 'Intervals', 'coach' => 'Cleo' ),
) );
}
function acme_timetable_shortcode(): string {
wp_enqueue_style( 'acme-timetable', get_template_directory_uri() . '/assets/timetable.css', array(), '1.0.0' );
$grid = array();
foreach ( acme_timetable_sessions() as $s ) {
$grid[ $s['time'] ][ (int) $s['day'] ][] = $s;
}
ksort( $grid );
$days = array( 1 => 'Mon', 2 => 'Tue', 3 => 'Wed', 4 => 'Thu', 5 => 'Fri', 6 => 'Sat', 7 => 'Sun' );
$html = '<div class="acme-tt-wrap" tabindex="0"><table class="acme-tt"><thead><tr><th scope="col"><span class="screen-reader-text">' . esc_html__( 'Time', 'acme-theme' ) . '</span></th>';
foreach ( $days as $label ) {
$html .= '<th scope="col">' . esc_html( $label ) . '</th>';
}
$html .= '</tr></thead><tbody>';
foreach ( $grid as $time => $row ) {
$html .= '<tr><th scope="row">' . esc_html( $time ) . '</th>';
foreach ( array_keys( $days ) as $n ) {
$html .= '<td>';
foreach ( $row[ $n ] ?? array() as $s ) {
$html .= sprintf( '<div class="acme-tt__class"><strong>%s</strong><span>%s</span></div>', esc_html( $s['title'] ), esc_html( $s['coach'] ) );
}
$html .= '</td>';
}
$html .= '</tr>';
}
return $html . '</tbody></table></div>';
}
add_shortcode( 'acme_timetable', 'acme_timetable_shortcode' );
.acme-tt-wrap { overflow-x: auto; }
.acme-tt { width: 100%; min-width: 640px; border-collapse: collapse; text-align: center; }
.acme-tt th, .acme-tt td { padding: .6rem; border: 1px solid #e3e3e3; vertical-align: top; }
.acme-tt thead th { background: #1c1c1c; color: #fff; }
.acme-tt tbody th { background: #f4f4f4; font-weight: 600; white-space: nowrap; }
.acme-tt__class { padding: .4rem; border-radius: 6px; background: #ffe9d6; margin-bottom: .3rem; }
.acme-tt__class span { display: block; font-size: .8rem; color: #555; }
How it works
What it solves
Gyms and studios publish the same weekly schedule over and over. A grid with days across and start times down is the format people expect, and it should be readable by screen readers and scroll sideways on small screens.
How it works
Sessions come from a filterable PHP array so another plugin or option can replace them. The code groups sessions by time and weekday into a two-level array, sorts the times, then prints a table with proper th scope attributes for both axes. Cells without a class stay empty rather than being omitted, so the columns line up.
The wrapper has overflow-x: auto and tabindex="0" so keyboard users can scroll it.
Adapting it
Feed the filter from a custom post type or repeater field. Add a data attribute for the class type and colour cells with CSS, or add a small script to filter by coach. Switch the day labels to wp_date( 'D' ) for localised names. If sessions can overlap, loop over them as the code already does.
Pitfalls
- Sort times as
H:istrings, which sort correctly when zero padded. - Do not use a table purely for layout elsewhere, but this is genuine tabular data.
- Provide a visible caption or heading near the table for context.
- Sunday-first locales may need a different order.
How to use it
Add the PHP and CSS files, then require inc/timetable.php from functions.php.
Place [acme_timetable] on a page.
Hook acme_timetable_sessions to return your own list of sessions.
Licence: GPL-2.0-or-later. This snippet comes from WordPress code and is GPL-2.0-or-later.