Using Drupal 8's Breakpoints.yml in Javascript

Minimize repetition by using this configuration in your themes

The breakpoint module in Drupal 8 provides a standardized method of declaring breakpoints that can be used across modules and themes in a responsive design. Having your breakpoints exposed as configuration is helpful, but in the interest of minimizing repetition, it would be great to have this configuration available in your theme's styles and Javascript as well. The folks at Lullabot have released a node module called drupal-sass-breakpoints that exposes this configuration to your theme's Sass files, and here I'll walk through how to use this configuration in your theme's Javascript.

Configuring breakpoints

First, we need to configure our breakpoints by placing mytheme.breakpoints.yml in the root of our theme. Each breakpoint contains a key, a label, a valid CSS media query, a group (optional), a weight, and pixel resolution multipliers. Each of these attributes are described in detail in Drupal's breakpoint documentation. The breakpoints in this example come from Bootstrap, but can be adjusted based on your design. The most important point here is that the "weight" attribute should be set in terms of increasing min-widths (i.e., the lowest min-width should have the lowest weight, and vice versa).

mytheme.breakpoints.yml

all:
  label: All
  mediaQuery: 'only screen and (min-width: 0)'
  weight: 0
  group: MyWebsite
  multipliers:
    - 1x
    - 2x
xs:
  label: Extra Small
  mediaQuery: 'only screen and (min-width: 480px)'
  weight: 1
  group: MyWebsite
  multipliers:
    - 1x
    - 2x
sm:
  label: Small
  mediaQuery: 'only screen and (min-width: 768px)'
  weight: 2
  group: MyWebsite
  multipliers:
    - 1x
    - 2x
md:
  label: Medium
  mediaQuery: 'only screen and (min-width: 992px)'
  weight: 3
  group: MyWebsite
  multipliers:
    - 1x
    - 2x
lg:
  label: Large
  mediaQuery: 'only screen and (min-width: 1200px)'
  weight: 4
  group: MyWebsite
  multipliers:
    - 1x
    - 2x

With our breakpoints configured, they can now be used in other modules like responsive images.

Retrieving breakpoints

Next, we'll use a preprocess hook in mytheme.theme to retrieve these breakpoints and attach them to the settings object that is accessible to our theme's Javascript.

mytheme.theme

/**
 * Implements hook_page_attachments_alter().
 */
function mytheme_page_attachments_alter(array &$page) {

  $breakpoints = \Drupal::service('breakpoint.manager')->getBreakpointsByGroup('MyWebsite'); // this should match the group name from mytheme.breakpoints.yml
  if (!empty($breakpoints)) {
    $media_queries = array();
    foreach ($breakpoints as $breakpoint) {
      foreach ($breakpoints as $id => $breakpoint) {
        $media_queries[$id] = $breakpoint->getMediaQuery();
      }
    }
    $page['#attached']['drupalSettings']['responsive']['breakpoints'] = $media_queries;
  }
}

The breakpoint configuration is now accessible through either the drupalSettings object (by adding "core/drupalSettings" as a theme dependency in mytheme.libraries.yml) or via the settings parameter that gets passed to your Drupal behaviors.

The breakpoints are added to a property called "responsive," which isn't a part of the standard drupalSettings object—it can be called whatever you'd like. My preference is to not muddy the root object with non-standard properties, so any additional properties get grouped by functionality, though it could just as easily be grouped under a property called mytheme to signify it as being theme-specific.

We now have an object available in our Javascript that looks like this:

drupalSettings.responsive

{
  breakpoints: {
    all: "only screen and (min-width: 0)",
    xs: "only screen and (min-width: 480px)",
    sm: "only screen and (min-width: 768px)",
    md: "only screen and (min-width: 992px)",
    lg: "only screen and (min-width: 1200px)"
  }
}

This object says what our breakpoints are, but doesn't give any information about which ones are applicable to a user's device. So next, we'll add some logic to keep track of which breakpoints are active, and emit events when the list of active breakpoints changes due to browser resizing or orientation change. Depending on your project's browser support, you may need to add the matchMedia polyfill (included in Drupal core) to your list of dependencies in mytheme.libraries.yml.

Drupal.behaviors.breakpoints = {
  attach: function (context, settings) {

    var breakpoints = settings.responsive.breakpoints;

    var handleWindowLoad = function () {
      Object.keys(breakpoints).forEach(function (bp) {
        if (window.matchMedia(breakpoints[bp]).matches) {
          settings.responsive.activeBreakpoints[bp] = true;
          $.event.trigger('breakpointActivated', bp);
        } else {
          settings.responsive.activeBreakpoints[bp] = false;
        }
      });
    };

    var handleResize = function () {
      Object.keys(breakpoints).forEach(function (bp) {
        if (window.matchMedia(breakpoints[bp]).matches) {
          // if it wasn't already active, mark it as active
          if (settings.responsive.activeBreakpoints[bp] !== true) {
            settings.responsive.activeBreakpoints[bp] = true;
            $.event.trigger('breakpointActivated', bp);
          }
        } else {
          // if it was active, mark it as not active
          if (settings.responsive.activeBreakpoints[bp] === true) {
            settings.responsive.activeBreakpoints[bp] = false;
            $.event.trigger('breakpointDeactivated', bp);
          }
        }
      });
    };

    // Handle the intial load
    $(window).on('load', handleWindowLoad);

    // handle resize events - throttled with underscore.js (optional - requires core/underscore be added as a dependency in mytheme.libraries.yml)
    $(window).on('resize', _.throttle(handleResize, 200));
  }
};

There are two parts to this code. On initial load, we loop through each of our breakpoints and check them against the user's device using matchMedia—broadcasting an event for each "active" breakpoint. Then, if a user resizes their browser, we recheck the breakpoints and broadcast an event for any that have changed status.

Now, when loading the page on a 1024 pixel-wide screen, the drupalSettings.responsive object would look like this:

{
  activeBreakpoints: {
    all: true,
    xs: true,
    sm: true,
    md: true,
    lg: false
  },
  breakpoints: {
    all: "only screen and (min-width: 0)",
    xs: "only screen and (min-width: 480px)",
    sm: "only screen and (min-width: 768px)",
    md: "only screen and (min-width: 992px)",
    lg: "only screen and (min-width: 1200px)"
  }
}

The final step is to capture these events and do something with them.

Example usage

Let's say we have a page with several images and the design calls for them to stack vertically below the SM breakpoint, but work as a carousel at or above the SM breakpoint.

Here is how we can use our new properties and events to initialize and destroy the carousel as needed:

var carousel;

Drupal.behaviors.exampleCarousel = {
  attach: function(context, settings) {

    var handleBreakpointActivated = function (e, breakpoint) {
      // SM breakpoint and above, initialize carousel
      if (breakpoint === 'sm') {
        if (!carousel) {
          // carousel config goes here.
        }
      }
    };

    var handleBreakpointDeactivated = function (e, breakpoint) {
      // below SM breakpoint, destroy carousel and stack the images
      if (breakpoint === 'sm') {
        if (carousel) {
          // disable carousel & remove reference here.
        }
      }
    };

    $(window).on('breakpointActivated', handleBreakpointActivated);
    $(window).on('breakpointDeactivated', handleBreakpointDeactivated);
  }
};

An alternative

As an alternative to the above solution, the "breakpointActivated" and "breakpointDeactivated" events could be combined into a single "activeBreakpointsChanged" event. This has the benefit of only firing a single event on page load, and only requires a single event listener in your behaviors. The drawback is that you need to keep track of both the old active breakpoints and new active breakpoints in order to determine which ones have changed. That means more complicated logic in your event handlers. I haven't done any performance testing between the two methods, so the two event method wins based on readability.