This is part of a larger idea/prototype thought experiment.

sections with optional affordances

In my blog post Design Affordance Controls, I describe ways which I believe that a few components in ARIA/common toolbox patterns are really design affordances that one would like to choose to employ to sections of content. This page illustrates one take on that, illustrating a <spicy-sections> element.


What is it?

This element allows authors to provide normal sectioning content and offer optional affordances to be provided at different times (mostly, when media query conditions are true). The same content can also be presented as (and mean/act like) a series of independent collapses, a set of exclusive collapses (single select accordions/responsive tabs) and all manner of tabs. To grok the basic idea, it might help to check out the demo page first. This page assumes you have the basic idea and gets into details.

  <!-- any heading here --!>
  <h2>Stuff that would go in a summary</h2>

  <!-- details content, with an optional container --!>
  <div>Stuff that would go below a summary</div>

  <!-- possibly more pairs of headings and content --!>

How do you define which affordances to show/when?

Pretty much everything about this idea requires somehow tying a media query's state to the employment (or not) of some kind of UI Affordances. To do this requires somehow serializing this desire. While it is among the most critical aspects to solve if we attempt to standardize something, how we actually serialize it isn't as important for understanding the basic ideas or demonstrating uses - so here, we are punting a little bit until we can agree on other aspects. Therefore, the approach we've taken here may (probably 'will') bear little resemblance to an actual solution. .

This is what works in the custom element, right now: One can express it via an attribute (mq-affordances) or in CSS via a Custom Property (--const-mq-affordances). In both cases the value is serialized as a MediaAffordancesList. You can read more about this serialization and see the many notes and caveats there, but the simplest form of this lets one express that they would like sections to provide collapse affordances when the media is screen and the max-width is 800px like this:

[screen and (max-width: 800px)] collapse


Can I style things based on the employed affordance?

Yes. When we are finally "there" this would be a pseudo-classes, in the custom element version too via Custom States. The benefit of those is that only the element can set them. For now though we are not there, and this uses attributes. When the affordance is employed it sets the affordance attribute. Note that this is non reflective and setting it will only screw things up for you, so just don't.

/* currently, here */
[affordance="collapse"] h3 {
  /*style your heading when collapse affordances are employed */

/* future state of custom element */
:state(affordance-mode-collapse) h3 {
  /*style your heading when collapse affordances are employed */

/* a native vision something like this...  */
:affordance(collapse) h3 {
  /*style your heading when collapse affordances are employed */

Shadow DOM bits

Some affordances have bits which are only present when the affordance itself is present.

The tab-bar mode projects your headings into a single Tab Bar container. This element exposes, conveniently enough, as ::part(tab-bar), just incase you want to style it. Inside that is also a ::part(tab-list) which contains the actual heading elements. Between these you can build lots of different styles of tabs, overflow handlings and so on.

Both collapse and exclusive-collapse modes project your heading's text into a button element which becomes a disclosure button for the content. The button is exposed, conveniently enough as ::part(disclosure-button) on the heading when it is employed. The indicator is rendered with before and after pseudo elements of the part, which is an svg data url background that is rotated. You can override it. In CSS, these only render if there is a content property, so you can simply set one to an empty string and unset the other to remove it thereby moving the indicator to the left or right... and so on.

Programmatic API

Can I know when the affordance that is employed changes?

Yes. You can use the .observeAffordanceChange(callback) to register a callback when the affordances change. It is passed the newly employed mode(s) (modes actually, but ignore that for now, it will always be a Set, either empty if no affordances are employed or containing the name of the one that is).

  .observeAffordanceChange((matched) => {
    // logs undefined if no affordances are employed, or the name of the one that is
Eww... why is it a Set?

Aka, "shouldn't this just be a string"? Yes, probably... or at least the first argument to it should be a string. This is part of a base API surface I'm thinking about and the short answer is "it's a work in progress" and it's taking me too long to keep making changes and documenting something and then revisiting, so I'm just presenting what it is now.

Shouldn't this be .addEventListener?

Probably? It is this way mainly because this is just a custom element and using an event listener makes it seem like I've thought through the questions that that implies: bubbling, canceling and so on... I haven't, so this is what we have. It is pretty simplistic.


A key idea to remember is that these elements contain two conceptual 'controllers' which maintain state independently - one that is "checkbox-like" (independent states) and one that is "radiobutton-like" (group state). These states always exist whether their affordances are shown, or not. All APIs about states of affordances are are available via .affordanceState properties of either the container element, or headings.

Can I tell which kind of controller is employed right now?

Yes, the container element's .affordanceState contains a .currentMode property tells you this.

// returns one of: "", "exclusive", "non-exclusive" or undefined

Can I tell which specific affordance is employed right now?

Yes, the container element's .affordanceState contains a .current property tells you this.

// returns one of: "", "collapse", "exclusive-collapse" "tab-bar" or undefined

headings (labels) and content

The container element's .affordanceState provides a convenience .getLabels() method which returns a true array containing all of the labels. You can use this to programmatically access the individual label's .affordanceState properties and do all manner of useful things...

let af = document.querySelector('spicy-sections').affordanceState;

// which are expanded on screen now?
af.getLabels().filter(l => l.affordanceState.expanded)

// which is expanded exclusively (shown or not)
af.getLabels().find(l => l.affordanceState.exclusiveExpanded)

// which are expanded non-exclusively (shown or not)
af.getLabels().filter(l => l.affordanceState.nonExclusiveExpanded)

The content associated with labels are their .nextElementSibling.

States vs defaults

The default state of all non-exclusive affordances are not expanded (collapsed). The default state of first exclusive affordances is expanded, the others are not. You can modify this by setting the boolean default-activate attribute on relevant headings. This has the effect of activating them from their default state (just imagine clicking them if they were visible). In non-exclusive mode, this will cause them to be initially expanded, in exclusive mode, the last one (if several are marked) will be expanded.

Additionally, one can add a defaults-on-match attribute to force the provided defaults to be restored each time the affordances are shown. This can be useful for patterns that always want to collapse a panel like the side bar of a web site when the screen gets small to get it out of the way.

Headings: Activation and expansion

Different controls do different things when they are "interacted with", but in all cases it is the label (heading) that is interacted with. For example, clicking the same heading on an exclusive control twice in sequence does nothing, it is radiobutton-like. But clicking a different one has an effect of both showing that, and hiding the others. Conversely, the non-exclusive collapse mode causes that element to toggle between the expanded or collapsed states. Because of this, we deal with 'activation' in the API of the currently deployed state, both setting and listening.

"Activating" the current affordance

You can programmatically activate the currently employed affordance by calling the generic .activate() method.

  .querySelector('spicy-sections h3')

Additional information

A <spicy-sections> is an example of am element like this. However, it's not the only one, probably. I have related needs sometimes, so it might be helpful to provide a base abstraction. Here, a <spicy-sections> extends a MediaAffordancesElement. This is a base class for element which might claim to provide some affordances and can know when various media conditions are met. You can read more about all of the details of that if you are interested.