Adapting layout to context

If you’re hooked up on using transclusion in Kanka, you may have run into some issues where content looks good in its original form but gets messy when constrained to a post, a dashboard widget or a sidebar. This guide will teach you to use a CSS feature called container queries (MDN) to control layout based on the available space for a specific element (as opposed to media queries, which measure the browser window as a whole and are often used to provide different layouts to mobile and desktop users).

A brief explanation of containers

In simple terms, a CSS container allows you to choose any HTML element on a page, give it a name and say "I want to use this element’s state to determine the styling of its descendants". The most common use of this is looking at the container’s current width and applying one of several layouts to the elements within to make the best use of that space. There are other ways to use container queries, but we will keep things simple for this tutorial.

How to set up a container query

Let’s see what creating a CSS container looks like in practical terms. For this example, I will target div.entity-main-block, i.e. the area of an entity page that contains the entry and posts, and div.ability, which contains a single ability attached to the entity. Note that these two elements are present whether you are looking at the Abilities page or at abilities in a post on the Overview page, so we don’t need multiple selectors to cater to both. We only care about the size of that one container.

The first rule targets the container; it specifies the type of container it should be treated as and gives it a name of our choosing:

.entity-main-block {
  container-type: inline-size;
  container-name: ability-container;
}
/* "Inline-size" basically means you want to know the width of the container.
   You don’t need to worry about other possible values for this, but MDN describes them in detail. */

The second rule starts with a container query (note the @ denoting a query). You can think of it as a conditional statement: if this property on this container matches this condition, apply these CSS rules to its descendants.

@container ability-container (max-width: 399px) {
  /* Since ability-container is the container-name we gave to .entity-main-block,
     everything in here only applies when .entity-main-block is narrower than 400 pixels...
     ...and only if .entity-main-block is the target’s ancestor! */
  .ability h1 {
    font-size: 2em;
  }
  .ability img {
    margin-left: 0.5em;
  }
}

And that’s really all you need: set the element as a container, then use as many queries on it as you want to style its descendants based on various conditions. There’s one easy pitfall to keep in mind, though: container queries only affect the containing element’s descendants; the following would not work because .entity-main-block is not a descendant of a container named ability-container!

@container ability-container (max-width: 399px) {
  /* I want the whole main block to have a different background... but this is not the way to do it :( */
  .entity-main-block {
    background-color: #333;
  }
}

Therefore, always keep in mind that your container should be at least one level above all elements you want to style conditionally.

You can also combine conditions with the and, or or not operators:

@container ability-container (width > 399px) and (width < 1200px) {
  /* Everything in here only applies when .entity-main-block’s width is over 399 and under 1200 pixels */
}

A practical example

In one of my campaigns, I have Abilities for my player characters’ race and class powers. The system I’m using presents powers in fairly narrow blocks, which suits the two-column layout of its rulebook. Since I mimicked their design in my template, I also want to show them side by side where there is enough space to do so. I determined that I need about 350 pixels per column for the blocks not to look cramped and to allow a sufficient gap between columns. Therefore I will make a base rule for showing a single column, and additional rules at each increment of 350 pixels.

Side note: I could use media queries to work from the window’s size, but that wouldn’t tell me whether the Kanka sidebar is open or closed, nor whether any other CSS is changing the usual width of each part of the layout, so I might still end up wasting available space by making safe assumptions. What I really care about is the current width of my ability box, so container queries are the perfect tool.

/* First, declare the container; an element that exists on both the Abilities page
   and a post set to show Abilities, and that I don’t need to style conditionally */

.box-abilities {
	container: boxabilities / inline-size; /* Shorthand property for name followed by type */
}

/* Next, write the base rule that turns the flexbox directly under .box-abilities into a grid (defaulting to a single column for narrower screens) */

.box-abilities > .flex-col {
	display: grid;
}

/* Add a 2nd column on medium screens */
@container boxabilities (width > 700px) {
	.box-abilities > .flex-col {
		grid-template-columns: 1fr 1fr;
	}
}
/* Add a 3rd column on wider screens */
@container boxabilities (width > 1050px) {
	.box-abilities > .flex-col {
		grid-template-columns: 1fr 1fr 1fr;
	}
}

/* Notice that the Wide rule overwrites the Medium rule when both apply,
   because of the cascading logic (later rules trump earlier rules with equal specificity);
   therefore the Medium query doesn’t need to set a maximum width.
*/

And with just these few simple rules, my columns are set to make optimal use of available space whether I’m looking at the Abilities page on a comfortable desktop screen...

... or at the Overview page on a phone:

Conclusion

This only broached the surface of container queries and where they might be useful on Kanka, but it should give you a taste of where and how to use them in your own campaigns. If you’re struggling with your container queries or what elements to base them on, you can always find me on Kanka’s Discord for more specific pointers =)

Last updated