Skip to content

Instantly share code, notes, and snippets.

@tomhodgins
Last active December 6, 2018 19:12
Show Gist options
  • Save tomhodgins/a45e1b670ca7384326d3f1ac5b2d6ebc to your computer and use it in GitHub Desktop.
Save tomhodgins/a45e1b670ca7384326d3f1ac5b2d6ebc to your computer and use it in GitHub Desktop.

CSS Element Query Syntax for 2018

This is a the current draft for a syntax for expressing element queries as an at-rule in CSS.

Inspired by ideas from Tommy Hodgins, Maxime Euzière, & Viktor Hubert

Table of Contents

Syntax

<element-query> = @element <selector-list> <condition-list> <event-list> { <scoped-stylesheet> }

<condition-list> = ( <feature> : <value>) [ and ( <feature> : <value> ) ]*

<feature> = width | height | characters | children | scroll-x | scroll-y | aspect-ratio | orientation

<value>: <integer> | <dimension> | <ratio> | <ident>

<event-list> = [ on <event> [ , <event> ]* ]

<event> = window.load | window.resize | window.input | window.scroll | window.click | self.resize | self.scroll | self.input | self.click | self.mutate | self.intersect | etc…

<scoped-stylesheet> = <css-stylesheet> plus :self, maybe :parent, and maybe :previous

Note: In addition to supporting things like min-width and max-width, possibly expand <condition-list> to support Media Queries-style range features like (400px < width < 1000px)

Examples

A basic example could look like this. This would target all <div> elements in a document and apply the inner <scoped-stylesheet> to each <div> that tests true to the conditions in the <condition-list>.

@element div (min-width: 500px) {
  :self {
    background: lime;
  }
}

In this case the selector :self allows us to target those elements matching the condition, leaving other <div> elements on the page alone.

An equivalent rule that specifies events might look like this:

@element div (min-width: 500px) on self.resize {
  :self {
    background: lime;
  }
}

In this case we're whitelisting the specific events we want to trigger a recalculation of these conditions rather than relying on the browser's best guess.

For an even more limited example, what if this scoped stylesheet only listened to the scroll events of <textarea> tags in the document?

@element textarea (min-scroll-y: 50%) on self.scroll {
  :self {
    background: hotpink;
  }
}

Perhaps Mutation Observer could be used to help listen for changes in characters and children:

@element input (min-characters: 5) on self.mutate {
  :self {
    background: red;
  }
}

While that should catch all of our changes, it should be equivalent to listening to self.input for this:

@element input (min-characters: 5) on self.input {
  :self {
    background: red;
  }
}

Which of course would also be caught by listening to window.input, even though that will be recalculating the conditions far more than we will likely need:

@element input (min-characters: 5) on window.input {
  :self {
    background: red;
  }
}

Scoped Stylesheets

The <scoped-stylesheet> is similar to a normal CSS stylesheet, with a few key differences:

  • the stylesheet is scoped to each elements in the <selector-list> passing the conditions in the <condition-list>
  • you can use new selectors :self, :parent, and :previous

Thus, an element query with a scoped stylesheet like this:

@element #example (min-width: 500px) {
  :self {}
  h2 {}
  :self p {}
  * {}
  * * {}
  :self.class {}
  .class:self {}
  div:self {}
  p :self {} /* not gonna work */
  :parent {}
  :parent :self {}
  :previous {}
}

Could be thought of like this, if you think of :is() being a selector that selects the element(s) in the document matching both a selector in the <selector-list> as well as all conditions on the <condition-list> in a uniquely identifying way.

This example uses an ID (#example) for clarity, but everywhere you see :is(#example) in the stylesheet below, keep in mind it's really like :is(:self). If the original selector had been div, it wouldn't be :is(div), because that would match every <div>, but only those <div> tags that match all of the conditions of the <condition-list>:

:is(#example) {}
:is(#example) h2 {}
:is(#example) p {}
:is(#example) * {}
:is(#example) * * {}
.class:is(#example) {}
.class:is(#example) {}
div:is(#example) {}
:is(#example) p :is(#example) {} /* not gonna work */
:has(:is(#example)) {}
:has(:is(#example)) :is(#example) {}
:has(+ :is(#example)) {}

The :self Selector

The :self selector refers to those element(s) in the document matching both a selector in the <selector-list> as well as all of the conditions in the <condition-list>.

The :parent Selector

The :parent selector refers to the parentElement of those element(s) in the document matching both a selector in the <selector-list> as well as all of the conditions in the <condition-list>.

The :previous Selector

The :previous selector refers to the previousElementSibling of those element(s) in the document matching both a selector in the <selector-list> as well as all of the conditions in the <condition-list>.

Conditions

There are a number of conditions that are possible to support, but by far the most significant for web design will be width. Here's a list of features, as well as some JavaScript tests to help explain what they do. Unlike JavaScript, these conditions should support CSS units.

Features

Width

The width feature tests a value against an element's offsetWidth.

@element div (min-width: 500px) {}

@element div (max-width: 500px) {}

@element div (500px <= width) {}

@element div (width <= 500px) {}

@element div (500px <= width < 1000px) {}
Min-Width Function
function minWidth(el, number) {
  return number <= el.offsetWidth
}
Max-Width Function
function maxWidth(el, number) {
  return number >= el.offsetWidth
}

Height

The height feature tests a value against an element's offsetHeight.

@element div (min-height: 500px) {}

@element div (max-height: 500px) {}

@element div (500px <= height) {}

@element div (height <= 500px) {}

@element div (500px <= height < 1000px) {}
Min-Height Function
function minHeight(el, number) {
  return number <= el.offsetHeight
}
Max-Height Function
function maxHeight(el, number) {
  return number >= el.offsetHeight
}

Characters

The characters feature tests a value against the length of an element's textContent or value.

@element div (min-characters: 5) {}

@element div (max-characters: 5) {}

@element div (5 <= characters) {}

@element div (characters <= 5) {}

@element div (5 <= characters < 10) {}
Min-Characters Function
function minCharacters(el, number) {
  return number <= ((el.value && el.value.length) || el.textContent.length)
}
Max-Characters Function
function maxCharacters(el, number) {
  return number >= ((el.value && el.value.length) || el.textContent.length)
}

Children

The children feature tests a value against the length of an element's children.

@element div (min-children: 5) {}

@element div (max-children: 5) {}

@element div (5 <= children) {}

@element div (children <= 5) {}

@element div (5 <= children < 10) {}
Min-Children Function
function minChildren(el, number) {
  return number <= el.children.length
}
Max-Children Function
function maxChildren(el, number) {
  return number >= el.children.length
}

Scroll-X

The scroll-x feature tests a value against an element's scrollLeft.

@element div (min-scroll-x: 50px) {}

@element div (max-scroll-x: 50px) {}

@element div (50 <= scroll-x) {}

@element div (scroll-x <= 50) {}

@element div (50 < scroll-x < 100) {}
Min-Scroll-X Function
function minScrollX(el, number) {
  return number <= el.scrollLeft
}
Min-Scroll-X Function
function maxScrollX(el, number) {
  return number >= el.scrollLeft
}

Scroll-Y

The scroll-y feature tests a value against an element's scrollTop.

@element div (min-scroll-y: 50px) {}

@element div (max-scroll-y: 50px) {}

@element div (50 <= scroll-y) {}

@element div (scroll-y <= 50) {}

@element div (50 < scroll-y < 100) {}
Min-Scroll-Y Function
function minScrollY(el, number) {
  return number <= el.scrollTop
}
Min-Scroll-Y Function
function maxScrollY(el, number) {
  return number >= el.scrollTop
}

Aspect-Ratio

The aspect-ratio feature tests a value against the ratio of an element's offsetWidth and offsetHeight.

@element div (min-aspect-ratio: 16/9) {}

@element div (max-aspect-ratio: 16/9) {}

@element div (16/9 <= aspect-ratio) {}

@element div (aspect-ratio <= 16/9) {}

@element div (4/3 < aspect-ratio < 16/9) {}
Min-Aspect-Ratio Function
function minAspectRatio(el, number) {
  return number <= el.offsetWidth / el.offsetHeight
}
Min-Aspect-Ratio Function
function maxAspectRatio(el, number) {
  return number >= el.offsetWidth / el.offsetHeight
}

Orientation

The aspect-ratio feature tests a value against the ratio of an element's offsetWidth and offsetHeight.

@element div (orientation: portrait) {}

@element div (orientation: square) {}

@element div (orientation: lanscape) {}
Orientation Function
function orientation(el, string) {
  switch (string) {
    case 'portrait': return el.offsetWidth < el.offsetHeight
    case 'square': return el.offsetWidth === el.offsetHeight
    case 'landscape': return el.offsetWidth > el.offsetHeight
  }
}

Events

In cases where features like width, height, aspect-ratio, orientation are concerned, using window.load, window.resize, and Resize Observer (via self.resize) could be left up to be the browser, unless specified by the user.

In cases where features like characters or children are used, events like window.load, window.input, and Mutation Observer (via self.mutate) could all work toward the same results.

Note: There should also be a way to trigger a recalculation of a stylesheet from JavaScript (Does this mean they would need a unique name of label?)

Feature Hints

When you use these features, these are events that help you make sure the styles recalculate at the right times:

First, for every feature, window.load seems like a great time to calculate what you need. Then, one or more of the following events can help:

Selector Hints

In addition, if things like :hover are used in the <selector-list> of the query, events like self.mouseenter and self.mouseleave can be listened to. For :focus it makes sense to listen to self.focus and self.blur. For :active it makes sense to listen to self.input, self.click, etc.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment