Everybody’s looking for Element Queries

What are Element Queries? At a high level, I’d describe them as pure, unfiltered, rocket fuel for hyper-responsive layouts and components. More technically, they are Media Queries scoped to individual elements. Element Queries would allow you to attach Media Query break-points based on the dimensions and characteristics of an element itself, instead of the page’s viewport.

Developers have wanted Element Queries for a long time. Here’s a list of articles that convey the need, along with a few attempts to make them real:

Visualizing the Problem

I’ve included a couple sketches from the Filament Group (with permission) that highlight the common layout issues we face in a world without Element Queries:

When the following schedule component is in its primary, full-width state, Media Queries work well for addressing different screen sizes/orientations
Responsive Calendar

Problems quickly arise when the same schedule component enters a secondary, partial-width state. Top-level viewport break-points are no longer relevant, and styling the component becomes extremely difficult.
Responsive Calendar

“So you’ve convinced me. Element Queries would be awesome, and developers want them – so why don’t browser vendors just give ’em the goods?” Well, there are a few tough problems to solve, chief among them is circularity. As Tab Atkins points out in his post, there is a problematic condition created if you apply the existing paradigm to Element Queries:

.container {
  float: left;
}
.child {
  width: 500px;
}
.container:min-width(450px) > .child {
  width: 400px;
}

See the issue? I’ll let Tab clarify the problem for us:

In this simple example, the container’s size is determined by the size of its contents, because it’s floating. By default, its only child is 500px wide. However, whenever the container is greater than 450px wide, the child is 400px wide.

In other words, if the child is 500px wide (thus making the container also 500px wide), then the child becomes 400px wide. But if the child is 400px wide (thus making the container also 400px wide), the child becomes 500px wide. Circular dependency!

Some Element Query hacks want to abuse you

Each of the posts I’ve linked to above adds to the chorus of developer need, and all are to be commended for highlighting the issues developers face. While there have been many attempts at creating something that resembles Element Queries, most rely on polling, observers, hacky resize listeners, hand-rolled measurement routines, and an assortment of odds and ends that simply fail to produce the same level of API fidelity and reliability that native Media Queries do. So what does this mean? Is all hope lost?

I’ve traveled the DOM and the HTML seas

After doing a ton of research and experimentation, I believe I have a better solution to Element Queries – in fact, it may be the best solution possible while we wait for some sort of native implementation of the feature. The mechanism I’ve derived not only avoids the issues highlighted above, it also provides the full Media Query API, scoped to individual elements. There are many factors to consider, but with a bit of out-side-the-box (literally) thinking, we’re giving Element Queries to everyone!

Oprah 'You Get' Element Queries Meme

Sweet Element Queries are made of this

Let’s talk about all the pieces we’ll need. I’ll start with the most basic ingredient: finding a way to associate a separate, scoped viewport with each of the elements we want to imbue with Element Query powers.

Picking a Viewport Surrogate

What are the options? There are basically two routes: you can either try arcane event juggling with resize listeners or DOM polling, or utilize something that creates its own browsing context. Tab Atkins highlights one classic element that does this: the iframe. Sure, I guess we could toss content into an iframe, but they are heavy elements (in terms of performance), and placing all your content into a separate browsing context brings with it its own lame set of hassles, like style/script boundaries, lack of auto-height content flexibility, and a host of others. But hey, let’s not give up yet, there are other candidates for the staring role in this Element Query thriller: embed and object.

The EMBED Element

The embed element can be used to, er, embed content within your pages. It was recently standardized in the HTML spec, and in my exploration of the spec, seems to be the ‘lightest’ (in terms of performance) of all the elements that create new browsing contexts. The flaw with embed elements, is the difficulty in accessing the content they contain from the parent document. For this reason, it’s a poor choice for this use-case. (here’s the embed element spec if you’re curious: http://www.w3.org/TR/html5/embedded-content-0.html#the-embed-element)

The OBJECT Element

Our other option, is the object tag. It can also be used to fetch and include content (or code) within your page, and still has a far ‘lighter’ create, load, and memory footprint than the iframe (closest to that of the img tag). This tag is interesting for a few reasons. Object tags create new browsing contexts, and most importantly, allow ownerDocument script access to their about:blank content. These suckers have been around for ages, here’s the spec: http://www.w3.org/TR/html5/embedded-content-0.html#the-object-element

The Conclusion: We’re going with option 2, the object element, for compatibility and (relative) ease in scripting its contents.

The Skinny on Performance

It’s widely recognized that the iframe element is a beast that consumes memory and ravages performance – this is true, they’re terrible. So how are object and embed elements different? Can’t you load things into them like an iframe? Sure, but their fundamental attributes and performance characteristics greatly differ from that of an iframe. To assure you that, in moderation, use of object elements is safe and will not get you burned at the stake for performance heresy, I’ve thrown together a perf test – you may find the results surprising: http://jsperf.com/element-create-load-footprint (Warning: some have stated the audio tag test – which has nothing to do with Element Queries – crashes Safari. This is inexplicable, as I am doing very basic create/load testing)

Basically, I wouldn’t use 1000 object elements in your app – but as the test above shows, I wouldn’t go putting a 1000 img elements into the DOM either. Use this Element Query code in a smart and deliberate way. Instead of putting an element query on 1000 li elements, put one on the ul – this is my official appeal to common sense, folks.

Bringing It All Together

Now you may be thinking “So put content in an object element? Wow, great advice Dan, that’s really original. Yawn.”, but here’s the trick: we’re not putting any content inside the object element! A few posts back I did a walk-through of how you can create a resize event sensor out of a few divs and well-placed scroll events – this is along those lines. I’ve created two distribution flavors for this mechanism: 1) a vanilla module that enables use without reliance on any additional dependencies, and 2) an easy-to-use Web Component created with the awesome X-Tag library.

UPDATE: I was contacted on Twitter by a fellow who suggested I use the addListener method of MediaQueryList objects to detect query matches on the object element sensors. I explored this route, but there are a few things that led me back to using dummy transitionend pings. I am happy to code up a version that uses MQL listeners, but here are a few things to consider:

  • It reduces browser support: you lose all of Android below version 4
  • MQL listeners are still shaky: https://bugs.webkit.org/show_bug.cgi?id=75903 – http://stackoverflow.com/questions/16694591/media-query-doesnt-change-css-when-maximizing-browser-and-switching-tabs/16844712#16844712
  • Hold on a bit longer!: They’ll be reliable soon, I’d estimate another 6-12 months

Both versions of the Element Query code work in any browser that supports CSS Transitions. This is a generous compatibility range that covers all “app-ready” browsers. Let me know if you encounter any issues using the GitHub issue areas under each of the repositories listed at the end of the article.

The Vanilla Module

The plain ol’ code module comes with the following features:

  • Two methods for manually attaching and detaching the Element Query object sensor – attachQuerySensor() and detachQuerySensor()
  • Automatic DOM ready attachment of all non-style/link elements that have a media="" attribute
  • Automatic bootstrapping of dynamically create elements when you add a media="" attribute
The HTML

Let’s imagine I have a section element that contains a ul. I want to style the list differently when the container becomes smaller than 300px. The HTML structure below is what this scenario would look like if the elements had already been parsed by the Element Query module (triggered by the presence of the media="" attribute):

<section media="query(small-width, (max-width: 300px))">

  <ul>
    <li>One</li>
    <li>Two</li>
    <li>Three</li>
  </ul>

  <object type="text/html" data="about:blank">
    <!--the html, head, and body elements are auto-generated by the browser -->
    <html>
      <head>
        <style>
          div {
            opacity: 0;
            transition: 0.001s opacity;
            <!-- the code adds prefixed properties too,
                 only showing standard for simplicity sake -->
          }
          @media (max-width: 300px) {
            [query-id="small-width"] {
              opacity: 1;
            }
          }
         </style>
      </head>
      <body>
        <div query-id="small-width"></div>
      </body>
    </html>
  </object>

</section>

So what’s that object element doing in there? How’s that going to help with anything? The media="" attribute on the section element is how you declare your Element Query rules. There are two arguments for the media attribute’s query() function syntax. The first is an identifying query name that will be added to the matched-media="" attribute when its corresponding CSSMediaRule is matched within the object sensor. As the user, all you need to worry about is adding queries to the media attribute, and styling based on the matched-media attribute, all the rest happens automatically.

The CSS

Once a query is matched, the name you gave the query will be added to the matched-media attribute. It’s a space separated list, so if multiple are matched, you would use it in CSS just like other attributes of this sort, class="" for instance. Here’s a post by Chris Coyier on all the ways you can style elements based on their attribute values: http://css-tricks.com/attribute-selectors/. Here’s an example of what writing CSS against an Element Query match looks like, based on the HTML example content above:

section[matched-media~="small-width"] {
  font-size: 50%; /* small text for a wee lil element! */
}

/*** Example for styling multiple matches: ***/

section[matched-media~="small-width medium-height"] {
  ...
}

A few caveats with this version:

  • If you set the innerHTML of an element, and that HTML contains an element that has the media="" attribute, it will not be element-quererized. This is something that is automatic when you use the Web Component version below. To augment the element with the query sensor, you’ll need to call the window method attachQuerySensor(ELEMENT_REF), passing the element as the first argument.
  • You must take care not to inadvertently remove the object element from the element-quererized parent. If you do this, it will no longer be able to sense changes and determine query matches. The most common ways this can occur are using innerHTML to blow-out content, or a DOM library method to empty an element. One strategy to avoid this, is always using a single child element inside the target element and using that for content CRUDing.

The Element Query Web Component

To make things even easier, I’ve created a Web Component Custom Element called x-querybox using X-Tag. This component utilizes the same mechanism described above, but also provides enhanced ergonomics, matched media listeners, and automatic retention of the object sensor when doing DOM manipulation – and since we’re using X-Tag, it’s actually smaller in size than the vanilla version! Let’s explore how it’s used:

The HTML (same as above, besides the custom element)
<x-querybox media="query(small-width, (max-width: 300px))">

  <ul>
    <li>One</li>
    <li>Two</li>
    <li>Three</li>
  </ul>

</x-querybox>
The CSS (also the same)
x-querybox[matched-media~="small-width"] {
  font-size: 50%; /* small text for a wee lil querybox! */
}
Moar goodies! The mediachange custom event
document.querySelector('x-querybox').addEventListener('mediachange', function(e){
  // the event detail property is an array of the active element queries
  if (e.detail.indexOf('small-width') > -1) {
    // the 'small-width' query is active, do some smally-widthy stuff!
  }
});

Element Queries want to be used by you

Checkout the demo, and go grab the code. Feel free to contribute to either of the repositories using their respective GitHub Issues area. Happy queryin’ folks!

Demo

The demo is based on the Web Component version of the code to show both the basic and extended features. It’s only meant to give you a general idea of what is possible. The shapes are style with percentage units, so you can resize the window to expand them, or grow them with a tap (via :hover CSS styles). As the shapes progress through their size changes, you’ll notice the text and background colors change to indicate they have hit a new break-point – this is all based on their individual, element-scoped queries. If you open the console, you will notice I am logging all the `mediachange` events that occur. The demo is more impressive on a larger screen, where you can test all the query changes: Element Queries Demo

Repos

I have two different repositories on my Github profile, one for the vanilla version, and one for the x-querybox X-Tag Web Component: