May 29, 2019

Flexible components with “size queries”

Overhead photograph of various fruit at a market.
Media queries are the foundation of many responsive sites, but there are often cases where they aren't quite the right tool for the job. How else might we handle responsive elements without using the sometimes misleading viewport width to scope our responsive styles?

Media queries are used in CSS to check various properties of the page. What is the device orientation? The screen resolution? Is the page about to be printed?

The most common query is for the width of the viewport. This value helps determine the amount of horizontal space available for page content, which influences how it should be presented. Most of the time the available space increases as the viewport does. As the viewport width increases, we can assume that the available space for our page does as well.

This works well most of the time, but there are some cases where the width of the viewport isn't a reliable measurement.

Let's say we have a site with a pretty common layout. A header, footer, sidebar, and main section.

╔═══════════════════╗
║       Header      ║
╠══════╦════════════╣
║      ║            ║
║ Side ║    Main    ║
║      ║            ║
╠══════╩════════════╣
║       Footer      ║
╚═══════════════════╝

As with most responsive sites, the layout adapts to the available space within the viewport. On narrower screens each section is stacked but as the viewport width increases, the sidebar and main content area are placed beside one another.

Small         Medium              Large                  Extra-large
╔════════╗    ╔══════════════╗    ╔═════════════════╗    ╔═══════════════════════╗
║ Header ║    ║    Header    ║    ║      Header     ║    ║         Header        ║
╠════════╣    ╠══════════════╣    ╠══════╦══════════╣    ╠══════╦════════════════╣
║  Main  ║    ║     Main     ║    ║      ║          ║    ║      ║                ║
╠════════╣    ╠══════════════╣    ║ Side ║   Main   ║    ║ Side ║      Main      ║
║  Side  ║    ║     Side     ║    ║      ║          ║    ║      ║                ║
╠════════╣    ╠══════════════╣    ╠══════╩══════════╣    ╠══════╩════════════════╣
║ Footer ║    ║    Footer    ║    ║      Footer     ║    ║         Footer        ║
╚════════╝    ╚══════════════╝    ╚═════════════════╝    ╚═══════════════════════╝

Since the elements handling layout are generally very near the "top" of the DOM structure, using the viewport dimensions to know when to change the layout is fairly accurate. Aside from perhaps a maximum width on the body or a primary container, the horizontal space available within the layout can be reasonably deduced solely based on the viewport width. However, this becomes a less useful metric for elements which are deeper in the page structure.

Let's focus on the 'Main' section of the layout for a moment. At 'small' we have about 400 pixels width of space. 'Medium' has about 640 pixels, but at 'large' it shrinks back down to 480 pixels. Since the viewport width has increased, we would expect that the width of the Main section would increase as well. However, when the sidebar is repositioned beside the main section the space available within the main section is reduced, even though the viewport width has increased. The horizontal space available in the main section has become disjointed from the rate at which the viewport width increases.

So here we see that using viewport dimensions doesn't really work too well. We could group our styles by breakpoint, so that 'small' and 'large' screens share one set of styles, while 'medium' and 'extra-large' screens share another. This begins to get a bit difficult to maintain as it increases the number of styles needed to cover these non-linear changes in layout, setting baseline styles which are overridden and reset multiple times when moving from 'small' to 'extra-large'.

These styles are also very specialized. We write them with the assumption that the sidebar causing this complexity will always be present. Later in the project's lifespan a special marketing page might be created with no sidebar at all. Our layout will continue to reorganize itself to accommodate a now absent sidebar. Would we then need another category of styles, one for pages with sidebars and one without? How do we maintain multiple copies of the same styles across a variety of increasingly specific contexts?

What if we used the actual space available to the element instead? If we use the width of the element's immediate parent to determine which styles should be applied, we can ignore some of the headaches caused by a shifting layout. This is the core idea behind container/element queries*.

* The terms "container query" and "element query" are similar, but don't quite mean the same thing. A very general explanation is that element queries measure the element in question, whereas container queries measure the immediate parent. There is lots of discussion on the definition of each and which to use, so keep an eye on that.

Solution and implementation

Unfortunately for us there are currently no CSS only methods to apply styles based on an element's width, so we have to get a bit creative.

The method we'll be using modifies a data-size attribute with size names relating to a rough pixel range. For example, if the element is between 400 and 799 pixels wide, the value medium would be added. We do have the ability to use exact pixel values instead of names, but we've found that in the vast majority of cases using named width ranges like 'small' and 'medium' are sufficient. In the event that an element needs more granular size ranges for a unique layout, they can be added on a per-component or even a per-element basis.

Since we're only measuring the width of the space available to each of our components, we'll name our plugin sizeQuery. When initializing our code on an element, it will look something like this:

$('.my-component').sizeQuery([
  { name: 'small', minWidth: 0 },
  { name: 'medium', minWidth: 400 },
  { name: 'large', minWidth: 800 },
]);

Instances of .my-component will be watched, and their size attribute updated based on the sizes provided, allowing us to write styles for that component at each size.

.my-component { /* ... */ }
.my-component[data-size~="small"] { /* ... */ }
.my-component[data-size~="medium"] { /* ... */ }
.my-component[data-size~="large"] { /* ... */ }

Note that the above selectors use a ~ in the attribute selector, which checks for the value in a space-separated list. This isn't necessary, but lets us write fewer selectors. Combined with a bit of logic in our JavaScript, it allows styles for smaller sizes to bubble up to the larger sizes much like what would happen when using mobile-first media queries. ie .my-component[data-size~="medium"] { ... }, instead of .my-component[data-size="small"], .my-component[data-size="medium"] { ... }.

The JavaScript used to measure and update attributes on our elements is relatively straightforward, so I'll let you explore the demo for specifics. In general though, this is what it does:

  • Find appropriate elements and bind an event handler which does the following when triggered via a viewport resize, custom event, etc does the following:
    • Measure the width of each element's parent.
    • Update each element's size attribute with size names up to and including their current size.

Demo

Check out the demo below to see it in action. Be sure to resize your browser window to see the responsive behaviour. Code can be viewed by selecting "Editor view" from the "Change View" menu.

Issues and Improvements

If the visitor disables JavaScript, the size attributes won't be updated. This can be smoothed over by adding a size attribute and value to the initial markup. The styles will then be static, but at least it will be somewhat readable. The default size should generally be the smallest one to ensure that if the available space is small, the content will be in a reasonable format.

Further Reading

There are plenty of alternative techniques, workarounds, discussions, and proposals out there. Projects like EQCSS parse your CSS and allow you to write @element queries much like you would a @media query to wrap context-specific styles. Other projects like responsive-elements add specific size classes to the elements in question, which are then used in CSS selectors.

I highly recommend taking a look at what is available, and see what's right for your project.

Cover photo by Jakub Kapusnak via Unsplash