Breakpoints in the HTML Source

March 2, 2024

Every client theme I build uses a base theme, but it's not always the same base theme and for various reasons they don't always have the same breakpoints. I've always been a little bit bothered by having to duplicate breakpoints in JavaScript because it feels redundant and a potential source of future issues / inconsistency—on multiple occasions I've inherited sites where the breakpoints aren't consistent across CSS and JavaScript, leading to unexpected behaviour at certain browser widths. My solution to this has been to start including the breakpoints as JSON in the HTML source so that any JavaScript that needs them can find them there rather than having them hardcoded in the script itself.

Drupal's breakpoint module makes breakpoint information easily accessible, but it obviously has to be enabled in order to do so, so step one is declaring a dependency on the module in the THEME.info.yml file (something that has been possible since Drupal 8.9.0):

dependencies:
 - drupal:breakpoint

 With the module enabled, hook_page_attachments_alter() can be used to generate the required markup:

/**
 * Implements hook_page_attachments_alter().
 *
 * Add breakpoints to the HTML source so that they can be referenced elsewhere
 * as required.
 */
function THEME_page_attachments_alter(array &$page) {
 $breakpoints = [];
 if ($breakpoints_data = \Drupal::service('breakpoint.manager')->getBreakpointsByGroup('THEME')) {
   foreach ($breakpoints_data as $breakpoint_plugin_data) {
     if (($breakpoint_data = $breakpoint_plugin_data?->getPluginDefinition()) && (isset($breakpoint_data['id'])) && ($breakpoint_data['id'])) {
       $breakpoint_id_parts = explode('.', $breakpoint_data['id']);
       $breakpoint_id = end($breakpoint_id_parts);
       $breakpoints[$breakpoint_id] = [];
       if (isset($breakpoint_data['mediaQuery']) && ($breakpoint_data['mediaQuery'])) {
         $breakpoints[$breakpoint_id] = _THEME_media_query_data($breakpoint_data['mediaQuery']);
       }
     }
   }
 }
 $attachments = [
   'breakpoints' => [
     '#type' => 'html_tag',
     '#tag' => 'script',
     '#attributes' => [
       'type' => 'application/json',
       'id' => 'theme-breakpoints',
     ],
     '#value' => json_encode($breakpoints),
   ],
 ];
 foreach ($attachments as $name => $data) {
   $page['#attached']['html_head'][] = [
     $data,
     $name,
   ];
 }
}

Most of what's going on here is parsing the data provided by the breakpoint manager service so that it can be included in a way that's easy to reference from JavaScript (because I'd much rather deal with complexity in PHP than in JavaScript). Note the _THEME_media_query_data() helper function—here's what that function does, including a link to the gist that I used as a starting point:

/**
 * Extract data from a media query string.
 *
 * See https://gist.github.com/JPustkuchen/ab6a7e7536da06a4bb5c403cea137c69
 *
 * @param string $media_query
 *   The media query string to process.
 */
function _THEME_media_query_data(string $media_query) {
 $pattern = '/\d*(min-width|max-width):\s*(\d+\s?)(px|em|rem)/';
 preg_match_all($pattern, $media_query, $matches, PREG_SET_ORDER, 0);
 $result = [];
 if (!empty($matches)) {
   if (count($matches) <= 2) {
     foreach ($matches as $match) {
       if (count($match) == 4) {
         // Convert hyphenated to camelCase.
         $name = str_replace('-', '', lcfirst(ucwords($match[1], '-')));
         $result[$name] = [
           'type' => $match[1],
           'dimension' => $match[2],
           'unit' => $match[3],
         ];
       }
     }
   }
 }
 return $result;
}

With these in place, every HTML page generated by this theme should now include something along the lines of:

<script type="application/json" id="theme-breakpoints">{"full":[],"large":{"maxWidth":{"type":"max-width","dimension":"1439","unit":"px"}},"medium":{"maxWidth":{"type":"max-width","dimension":"959","unit":"px"}},"small":{"maxWidth":{"type":"max-width","dimension":"639","unit":"px"}}}</script>

Here's the JSON prettified:

{
 "full": [],
 "large": {
   "maxWidth": {
     "type": "max-width",
     "dimension": "1439",
     "unit": "px"
   }
 },
 "medium": {
   "maxWidth": {
     "type": "max-width",
     "dimension": "959",
     "unit": "px"
   }
 },
 "small": {
   "maxWidth": {
     "type": "max-width",
     "dimension": "639",
     "unit": "px"
   }
 }
}

Now, rather than having to have those breakpoints hardcoded, any JavaScript in this or any child theme can access them using something like:

let breakpoints = {}
let breakpoint_data = null;
try {
 breakpoint_data = JSON.parse($('script#theme-breakpoints').text());
} catch (e) {}
if (breakpoint_data) {
 Object.entries(breakpoint_data).forEach(([key, val]) => {
   if (val?.maxWidth?.dimension) {
     breakpoints[key] = parseInt(val.maxWidth.dimension);
   }
 });
}

Note that this uses a bit of jQuery. Following Drupal's lead, I'm going to try to minimize my use of jQuery in the future—I'll update this post once I know I have a working vanilla JavaScript version.

A photo of the Arctic Ocean at Tuktoyaktuk Northwest Territories

The Arctic Ocean at Tuktoyaktuk Northwest Territories, July 2019