• UX
  • audio
  • data
  • code

Where size comes from

tl;dr

The web, famously, is a medium that forces designers to be flexible. From Dao to Responsive Web Design to Viewports.fyi, we are constantly reminded that we have no idea where, how, or on what our creations will end up.

I’ve been thinking about what that means for how I structure my CSS and how I compose my components, and here’s where I’ve landed: it’s generally a good idea for size to be imposed from the outside in rather than giving a component its own intrinsic size. In other words, components should work in any container. Why? Because, starting with the outermost container — the viewport — we don’t necessarily know how big any container will be.

What this looks like is that I pretty much always want components to behave like block-level elements: taking up all available space in the inline axis, and taking what they need in the block axis. Beyond that, sizing should be imposed on components by layouts (aka the “composition layer”).

CSS grid in particular is great for layout, and usually takes an outside-in approach. Relying on components’ intrinsic sizes breaks that paradigm and can make it harder to re-compose existing components in new contexts.

An example

“How wide should this sidebar be?”

This type of question is a natural one to hear, either from designers working in the design tool of their choice, or from developers building components. But I want to discuss a somewhat different question:

“Where should this size come from?”

See, there are two ways to approach sizing a component:

Which approach makes the most sense? For a long time I wasn’t sure how to think about this. Knowing which to choose can seem especially tricky when building components in isolation, like as part of a design system. Knowing when to stay flexible and when to specify a size can be subtle.

Let’s take an example. Say you have a site with a sidebar, as in this image from Penpot.

Screenshot of an artboard from a design app. On the left is a sidebar with an off-white background color containing primary navigation. On the left is a big white area for the rest of the site.

One way to build it is to include the sidebar’s width as part of the sidebar itself:

.sidebar {
	inline-size: 14rem;
	grid-column: sidebar;
}

Then drop it into the first column of a layout like this:

.layout {
	display: grid;
	grid-template-columns: [sidebar-start] auto [sidebar-end main-start] 1fr [main-end];
}

Looking at those grid columns, the sidebar area is automatically sized to fit its content, which is set to 14rem by the sidebar itself, and the rest of the page takes up the rest of the space. Seems normal for a sidebar on a page, right, so, what’s the problem?

It may look like you’re being flexible by making it so someone can drop in any component into that layout, and the layout will put it in the right place. And you’re right. But is that kind of flexibility necessary from what CUBE CSS calls the “composition layer”?

I would suggest that, in general, components need to be able to live in multiple contexts, but compositions — components in a particular arrangement — are created for more specific contexts. Of course it’s not black and white, and you can create bits of reusable composition (like with utility classes), but in broad strokes that’s how I tend to think about things these days.

Picking up the sidebar example, what happens if now the designer says that on small viewports[1] the sidebar needs to slide out from the side and cover the whole screen.

Looping video of a menu sliding out and back from the left side of a mobile-sized viewport.

With the setup above, that means having to change both the component (get rid of its constrained width) and the layout (put it over the whole screen). I have to change not just the layout, but the component itself, too.

What’s the alternative? Well, I like to take my cues from the web platform itself. For most components, that means doing what block-level elements do in normal flow.

By default, block-level elements (like paragraphs) take up as much space as they can on the inline axis (i.e., horizontally in horizontal languages), and are sized by their content in the block axis (i.e., vertically in horizontal languages).

Since CSS was originally designed — by people way smarter than me back when I was in 7th grade — to be as flexible as possible and work in as many contexts as possible, that seems like a good foundation. My default, then, is to create components as block-level elements: they fill the available inline space, and use as much block space as they need.

With our sidebar, it’s going to look pretty silly by itself, especially in a big viewport, but that’s ok! As long as it resizes in a reasonable way. Its size is going to come from context — from the layout that contains it.

The sidebar shown out of context, by itself on a page. As the viewport is resized, it gets narrower or wider. Its height is constant, based on its contents.

Once we put it in a layout, all is well.[2] In most cases, I reach for a grid layout. Grid layouts are designed to be outside-in. In other words, rather than relying on the intrinsic sizes of its children, a grid layout places its children, often imposing sizes on those children. This makes it perfect for composition. We can author components without having to care about their sizes when composed, then let the composition layer take care of it.

.layout {
	display: grid;
	grid-template-columns: [sidebar-start] 14rem [sidebar-end main-start] 1fr [main-end];
}

[data-layout-slot="sidebar"] {
	grid-template-column: sidebar;
}

And to lay everything out differently on mobile, we just need to change the layout and add[3] a bit of translation and animation.

@media (width < 33rem) {
	.layout {
		grid-template-columns: [sidebar-start main-start] 1fr [sidebar-end main-end];

		& [data-layout-slot="sidebar"] {
			translate: -100% 0;
			transition: translate 150ms;

			&.open {
				translate: 0 0;
			}
		}
	}
}

Note that to assign the sidebar to the right place in the layout I’m using the [data-layout-slot="sidebar"] selector, not just .sidebar. That’s because I want to be clear that these declarations aren’t intended to describe anything about the innate nature of a sidebar; rather, they tell whatever is in the sidebar area of our layout how to behave as part of that layout.

I don’t always do that, but I think it’s illustrative. Even if I don’t use a separate pretend “slot” selector, I find that this is a great use case for nesting. The ruleset with the plain old .sidebar selector describes a sidebar, and the ruleset nested under .layout (whether it’s using [data-layout-slot="sidebar"] or .sidebar) describes how to place a sidebar within that layout.


Here’s the full example. If you view the CodePen you’ll note that I also have a few small utility compositions in addition to the main .layout. I’m also using CSS @layers because layers make it easy to ensure that if a composition needs to alter a component, it can do it without fighting specificity battles.

See the Pen Where size comes from demo by Noah (@noleli) on CodePen.

Of course none of this is hard and fast. There may be cases when it makes sense for a component to have an assigned size (either a specific <length-percentage> value or a keyword like fit-content). I would usually consider it it up to the layout/composition layer to determine those values (much like the layout sets the sidebar width in the example above), but even if not, it’s good practice to make sure a consuming app can easily override or revert that size so a layout can manage the component if it needs to.

One final thought is that all of this discussion is about block-level components. Inline components — things that go inline with text (e.g., form controls, icons, buttons) — are pretty much always sized by their content.


  1. What the designer actually said was “on mobile”, but you translated that in your head to “on narrow viewports”. ↩︎

  2. Note that, while I’m mostly talking about inline size here, by default, grid layouts stretch items to fill a track in both axes, so if the layout fills the page in the block axis, so will the sidebar. ↩︎

  3. Something something mobile first. I don’t feel particularly strongly about that, and in this case we worked our example starting with bigger viewports, and that’s ok. ↩︎