Skip to main content
A Frehner Site

Contextually Styling Components

Props are ok #

When working in a component design system, it's common to expose a property/attribute (I'll just call these props from now on) for your component that changes the way it looks. For example, variant="filled" color="primary", etc. There's nothing controversial about this design, given the component library you're building is meant to be general purpose, used in a lot of contexts, and easily extendible.

However, if you're working on a component library that is purpose built for a single area inside of a company, has strong opinions, wants to scale those decisions out quickly, offering props can lead to inconsistent outcomes; what if a designer or developer uses variant="filled" on an Alert component multiple times on a page, even if your docs say something like "Only use variant=filled Alerts at the top of a page, and use the default Alert styling everywhere else"?

Strong opinions, strongly enforced #

In an opinionated component design system, there's another way you could enforce your opinions: don't offer the prop at all. It seems crazy, but stay with me here; let's make it easy for our developers and designers to fall into the pit of success and stay within our guidelines by default! Within the code of our Alert component, we'll automatically apply the variant for the developer based on the context (or location) that the Alert finds itself in.

Example opinions #

I'm going to use the Material UI Alert component for this example (not for any particular reason; it was just the first component library I went to), and build some rules for how we want our Alert to be used:

Implementation Options #

There are several ways you could implement these opinions:

JavaScript Context #

We could use JavaScript context (e.g. React Context providers and consumers, or similar patterns in other libraries). You set up the Form and Section components to be context providers, and the Alert component to be a context consumer, and apply classes/styling based on the context.

But it feels heavy-handed to use JS, when CSS could do it too.

CSS Vars #

With CSS Custom Properties (variables), you can rely on the browser's CSS engine to apply contextual styles in this way. Here's a simple example of how it works: codepen link.

There are a couple of things I don't necessarily love about this solution, though:

  1. The opinions about styling have to live at the parent level, not at the component level. I would instead like for all the styles for a component to be co-located in the same CSS file.
  2. You must set n number of CSS vars for n number of customizations to the component (in the example above, we have to have 2 vars because we customize the background and the border). That doesn't scale too well.

For 2, you could use CSS Space Toggles to derive other properties based on "if in Form, set this style; if in Section, set this style". But I suspect that gets a bit messy, too.

CSS Containers #

Roma Komarov and Adam Argyle have written about how components can use CSS Container Queries to know where they are, and I find this solution extremely elegant; we get to defer to the browser's CSS engine to apply our styles, we can co-locate our component's CSS in one spot, and we can apply multiple properties in one go without adding multiple CSS vars. As an additional bonus, thanks to previous work, named Container Queries pierce the Shadow DOM, so even if you're building with web components, you can set and read named Container Queries inside and outside of shadow.

It could look like this:

.alert {
	background-color: orange;
}

@container --section {
	.alert {
		background-color: color-mix(in srgb, orange 15%, transparent);
	}
}

@container --form {
	.alert {
		background-color: transparent;
		border: 3px solid orange;
	}
}

I said 'could', because "condition-less" Container Queries aren't yet fully supported (at time of this writing). Here's a codepen for this; but depending on your browser (and the date of accessing this codepen), it's probably not working for you. We'll talk about this below in browser support, but for the time being let's implement a workaround with better support:

.alert {
	background-color: orange;
}

@container --section (min-width: 0px) {
	.alert {
		background-color: color-mix(in srgb, orange 15%, transparent);
	}
}

@container --form (min-width: 0px) {
	.alert {
		background-color: transparent;
		border: 3px solid orange;
	}
}

And the associated codepen, which should work in many more browsers. The downside of this version is that you have to set the parents as inline-size containers, and you have to add the min-width: 0px conditions. Not deal breakers by any means, but enough to make me excited about the ideal state above.

Browser support #

Fortunately, browser support is coming along for "condition-less" container queries: some over-excited and slightly foolish person has gone into both WebKit / Safari and Firefox to add support. So this codepen (it's same as the "ideal state" one above) works in Safari Technology Preview and should be coming very soon to a Firefox near you! I suspect Chrome is very close to shipping this as well.