惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

B
Blog RSS Feed
云风的 BLOG
云风的 BLOG
爱范儿
爱范儿
WordPress大学
WordPress大学
博客园 - 三生石上(FineUI控件)
阮一峰的网络日志
阮一峰的网络日志
Martin Fowler
Martin Fowler
C
Check Point Blog
MongoDB | Blog
MongoDB | Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
人人都是产品经理
人人都是产品经理
博客园 - Franky
罗磊的独立博客
博客园 - 司徒正美
S
SegmentFault 最新的问题
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
V
V2EX
Last Week in AI
Last Week in AI
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
博客园 - 聂微东
大猫的无限游戏
大猫的无限游戏
博客园 - 叶小钗
小众软件
小众软件
美团技术团队

daverupert.com

I wish you the best in the Offline Introducing Microlighter Vibe Check №43 Before I go: Double your business with companies you like The new yet familiar CodePen 2.0 Finding curated public domain images A workaround for hiding empty slots in Chromium Spoons by Sam Vibe Check №42 The duality of language models in the browser 10,000-watt GPU meet 40-watt lump of meat I don’t want a screenshot of your Claude conversation When moving fast, talking is the first thing to break Inverted themes with light-dark() Ozempic dreams Before I go: People like it when other people make things People are not friction Smaller and dumber Priority of idle hands Magic Words Write about the future you want I’m swearing off APIs entirely Waiting for the power to go out The best version of my site so far… Focus rings with nested contrast-color()? Interpolate contrast-color() to manipulate lightness Using your design system colors with contrast-color() Algorithmic hover states with contrast-color() Twenty Twenty-Five Vibe Check №41
Shadow-piercing local component variables
Dave Rupert · 2026-08-17 · via daverupert.com

Stuart Robson’s post Solving CSS @layer Ordering in Design Systems with Design Token Metadata, a response to Chris Coyier’s Thinking Horizontally in CSS @layer, came to me at a serendipitous time. You know I love a good blog-and-response, but these posts on the topic of lowering the specificity of component-level tokens and scaling that out was a problem I was having with some of my web components.

Their @layer trick didn’t work for me, but here’s what did…

The local component variable problem

Let’s say we have a global stylesheet with our design system tokens.

/* theme.css */
:root {
	--system-bg: #00f1;
	--system-fg: #000;
	--system-font: Inter; sans-serif;
}

And then we set up our web component with some local component variables that use design system tokens as defaults.

<ui-card>
  <template shadowrootmode="open">
	  <style>
	  :host {
			/* Local component variables */
			--card-bg: var(--system-bg);
			--card-fg: var(--system-fg);
			--card-font: var(--system-font);

			/* Apply component styles */
		  background: var(--card-bg);
		  color: var(--card-fg);
		  font-family: var(--card-font);

			button {
				font-family: var(--card-font);
			}
	  }
	  </style>
	  <slot></slot>
  </template>
  
  Hello world
</ui-card>

This works great… Except… I sort of expected that if we added some of those component tokens to the stylesheet we could restyle the card.

/* theme.css - This doesn't work 😭 */
:root {
	/* ...system tokens go here... */

	--card-bg: #f001;
	--card-fg: #f00;
	--card-font: fantasy;
}

Because :host has more specificity than :root, that means :host wins. This confused even me, a web component front-end master. But Bramus from the Chrome team explained it to me as: If you redeclared the variables in a normal div, it wouldn’t be surprising that the values didn’t inherit. Ok. That tracks.

The canonical way to solve this –of course– is to pull out a bazooka and point it directly at the element:

/* theme.css - Works, but... */
ui-card {
	--card-font: fantasy;
}

This works and I often recommend direct styling the :host as the primary way to customize web components from the outside.

But a system is different than laser-guided fixes. Themes should be low-specificity by design. Using an HTMLElement as a selector is one of the strongest selectors in our CSS toolbox. That means we broke our one rule and if we ever want to change our mind, we’re on a one-way train to !important town. But –to me– the larger problem is that now our CSS and DOM are super-glued together, which is less ideal compared to a flat list of element-agnostic tokens.

What if other card-like elements use those tokens? Uh-oh, gotta update the global stylesheet. What if elements in the Light DOM also want to use those tokens? More work. We need a “Set it and forget it” way to handle themes. Also pinning tokens to the custom-element might not always work in nested component scenarios, but that’s a secondary issue.

There’s a handful of other approaches (see below) to this problem and they all have trade-offs. I was hoping the @layer solution would help, but @layer { :host {} } inside a shadowRoot is still more powerful than :root. Drat!

But is there a low-specificity way to win the specificity war with :host?

A refined solution in :defined

After throwing every scoping method I could at the wall, I found a decent one-liner that allows us to preserve the inheritance in a way that’s web-component’y:

/* theme.css */
:root {
	--system-bg: #00f1;
	--system-fg: #000;
	--system-font: Inter; sans-serif;
}

:defined {
  --card-bg: #f001;
  --card-fg: #f00;
  --card-font: fantasy;
}

When you put <ui-card> on the page, it’s an HTMLUnknownElement. Because HTML is chill, it treats the unknown element as a <span> with a gigantic typo. The :defined pseudo-state triggers on customElement.define() and with that small change we’re able to pierce the shadow boundary and win the specificity war because :defined is more powerful than :host.

But… You know what else gets :defined? All native HTML elements! div:defined, video:defined, and every native element your browser supports also works with :defined.

That sneaky fact gives us some portability that the ui-card selector didn’t. If we need to re-use these tokens inside a Light DOM CSS class, we can also support that with :defined.

.ad-unit-block {
  /* Local component variables */
  --card-bg: var(--system-bg);
  --card-fg: var(--system-fg);
  --card-font: var(--system-font);
  
  /* Apply component styles */
  background: var(--card-bg);
  color: var(--card-fg);
  font-family: var(--card-font);
}

And that’s pretty handy if your default styles and system overrides need to go more places.

Full walkthru demo

See the Pen shadow-piercing local component variables by Dave Rupert (@davatron5000) on CodePen.

One small quirk :not(:defined) elements

As Chris and I discovered in a live recording of ShopTalk, you can run into a weird quirk hanging tokens off :defined if you have strong opinions about :not(:defined) custom-elements inheriting styles.

If an undefined custom-element has a :defined parent, variables from the parent will pierce the shadow boundary. There’s two ways that I found to fix this…

/* The not-has-not route */
:defined:not(:has(:not(:defined))) {
	--card-font: fantasy
}

/* The unset if has-not route */ 
:defined {
	--card-font: fantasy;
}

:has(:not(:defined)) {
	--card-font: unset;
}

I don’t love either of those approaches, so I probably won’t optimize for the “I put a fake undefined element on the page that uses our CSS variables but it must not inherit our CSS variables!” edge case.

Bramus told me about one potential future option that might help this exact situation, the already spec’d out was the inherit() function.

:host {
	--card-bg: inherit(--card-bg, var(--system-bg));
	background: var(--card-bg);
}

I like that. That could work. Let’s do that? Ok. We all agree. Ship it.

Either way, that’s the end of my post. If you’re curious about other ways to handle component-level variables, keep reading!


Alternatives considered

Below are a handful of different workarounds for local-component variables, each with trade-offs.

Make all root overrides !important

/* theme.css - Works but, too heavy-handed */
:root {
	--card-font: fantasy!important;
}

This works, but should be the nuclear option for specificity battles. Also makes it more difficult to override later.

Shipping all variables in the theme

/* theme.css */
:root {
	/* system theme */
	--system-bg: #00f1;
	--system-fg: #000;
	--system-font: Inter, sans-serif;

	/* custom overrides */
	--card-bg: #f001;
	--card-fg: var(--system-fg);
	--card-font: fantasy;
}

This works too. You’ll need to change your <ui-card> styles to directly access background: var(--card-bg). The problem here is that all variables are mandatory and must ship in the theme. Even if a product only uses or customizes just one component, they have to ship all the tokens for every component. And now your theme CSS will be ever-growing at the rate of num_options * num_components.

Optional variable fallback chains in shadowRoot

/* theme.css - Works but, can be confusing to CSS learners */
:host {
	font-family: var(--card-font, var(--system-font));
}

I’ve been down this path. It works well and the first variable in the fallback chain becomes totally optional, meaning we can ship less tokens in the production theme. Fallback chains (a ternary, basically) and Aliases (a DOM walker) have dramatically different performance profiles when it comes to style calculation costs, so you may want to consider this… but that’s for another post.

In practice though, I find the beauty of the CSS cascade gets lost on a lot of six-figure engineers (waves generally in the direction of Hacker News and popular CSS frameworks).

And this also works less good if you’re going to re-use a local variable token… which leads us to…

Private variables with fallback chains in shadowRoot

/* theme.css - Work but, too noisy */
:host {
	--_card-font: var(--card-font, var(--system-font));
	font-family: var(--_card-font);
	
	button {
		font-family: var(--_card-font);
	}
}

This is the fully exploded version of the previous pattern. We can re-use --_card-font on other elements inside our component. You can do this but it begins explode the surface area of the number of tokens we’re managing inside a component. Fallbacks were confusing enough, now we’re doing fallbacks with aliases. Uf.