I wrote this PHP script to dynamically generate a topic header layout using CSS grid. The $columns
array defines each column’s properties—like its CSS class, size, content, and how many rows or columns it spans. The script then processes these definitions to calculate grid placement and generate the necessary CSS styles.
I designed it to be flexible so that third-party scripts can easily add new columns or rows just by modifying the $columns
array. However, since the script involves complex grid calculations, I’d appreciate a second pair of eyes—especially from someone better at math—to verify that the row and column positioning works as intended.
/** * Topic header column definitions. * * @var array $columns * Each column has: * - `class`: The CSS class name. * - `content`: The displayed content. * - `size`: The column width in CSS grid format. * - `rowspan`: Number of rows the column should span. * - `colspan`: Number of columns the column should span. * - `row_number`: The row in which the element starts (1-based). */ $columns = [ 'icon' => [ 'class' => 'topic_icon', 'content' => ['header' => ''], 'size' => '2em', 'rowspan' => 2, 'colspan' => 1, 'row_number' => 1, ], 'info' => [ 'class' => 'info', 'content' => ['header' => '{subject} / {starter}'], 'size' => '1fr', 'rowspan' => 1, 'colspan' => 2, 'row_number' => 1, ], 'author' => [ 'class' => 'topic_author', 'content' => ['header' => ''], 'size' => 'auto', 'rowspan' => 1, 'colspan' => 1, 'row_number' => 2, ], 'stats' => [ 'class' => 'topic_stats', 'content' => ['header' => '{replies} / {views}'], 'size' => '10%', 'rowspan' => 2, 'colspan' => 1, 'row_number' => 1, ], 'lastpost' => [ 'class' => 'lastpost', 'content' => ['header' => '{last_post}'], 'size' => '28%', 'rowspan' => 2, 'colspan' => 1, 'row_number' => 1, ], ]; $grid_rows = []; $num_rows = 1; $grid_sizes = []; $row_numbers = []; $indexed_grid_areas = []; $column_index = 0; // Tracks column position $column_numbers = []; $prev_name = ''; foreach ($columns as $name => $column) { $row_index = $column['row_number'] - 1; // Convert to zero-based index if (!isset($grid_rows[$row_index])) { $grid_rows[$row_index] = []; } for ($i = 0; $i < $column['colspan']; $i++) { for ($y = 0; $y < $column['rowspan']; $y++) { $current_row = $row_index + $y; $row_numbers[$current_row][$name] = ($column_numbers[$row_index] ?? 0); if ($row_numbers[$current_row][$name] === 0) { $row_numbers[$current_row][$name] = (get_adjacent_value($row_numbers[$current_row], $name) ?? -1) + 1; } $column_index = $row_numbers[$current_row][$name]; $grid_rows[$current_row][$column_index + $i] = $name; } // Add column sizes only if we're on the first row. if ($column['row_number'] === 1) { $grid_sizes[] = $column['size']; } } // Move to the next available column //~ $column_index = $row_numbers[$row_index][$column['colspan']; $column_numbers[$row_index] = ($column_numbers[$row_index] ?? $row_numbers[$row_index][$name]) + $column['colspan']; $num_rows = max($num_rows, $row_index + $column['rowspan']); $prev_name = $name; } var_export($row_numbers); // Fill empty grid areas. for ($y = 0; $y < $num_rows; $y++) { for ($i = 0, $n = count($grid_sizes); $i < $n; $i++) { $indexed_grid_areas[$y][$i] = $grid_rows[$y][$i] ?? '.'; } } // Convert rows into CSS grid template areas. $grid_areas_str = implode('" "', array_map(fn($r) => implode(' ', $r), $indexed_grid_areas)); echo ' <style>'; foreach ($columns as $name => $column) { echo ' .' . $column['class'] . ' { grid-area: ' . $name . '; }'; } echo ' #topic_header, .topic_container { --grid-template-columns:' . implode(' ', $grid_sizes) . '; --grid-template-areas: "' . $grid_areas_str . '"; } </style>'; /** * Get the previous or next value in an associative array based on a given key. * * @param array $array The associative array. * @param mixed $current_key The key to search for. * @param string $direction "before" to get the previous value, "after" to get the next value. * * @return mixed|null The found value if exists, otherwise null. */ function get_adjacent_value(array $array, $current_key, string $direction = "before") { reset($array); $previous_value = null; while (key($array) !== null) { $key_in_loop = key($array); $value = current($array); if ($direction === "before" && $key_in_loop === $current_key) { return $previous_value; } next($array); if ($direction === "after" && $key_in_loop === $current_key) { return current($array) !== false ? current($array) : null; } $previous_value = $value; } return null; }
Output
<style> .topic_icon { grid-area: icon; } .info { grid-area: info; } .topic_author { grid-area: author; } .topic_stats { grid-area: stats; } .lastpost { grid-area: lastpost; } #topic_header, .topic_container { --grid-template-columns:2em 1fr 1fr 10% 28%; --grid-template-areas: "icon info info stats lastpost" "icon author . stats lastpost"; } </style>